Compare commits
333 Commits
atomic-rep
...
sample-dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a34eea89 | ||
|
|
6303690433 | ||
|
|
2d603393ef | ||
|
|
0430591332 | ||
|
|
435f9acbad | ||
|
|
79583b9892 | ||
|
|
e6fa8f9058 | ||
|
|
855b32452b | ||
|
|
8e4632208a | ||
|
|
5b9522cfc9 | ||
|
|
2ab84940ed | ||
|
|
370671017c | ||
|
|
427fbc1c82 | ||
|
|
96f9d6c14e | ||
|
|
198ebcf5d1 | ||
|
|
9797a74c21 | ||
|
|
091c4a68d8 | ||
|
|
470df0bf7f | ||
|
|
6bf0ada3c3 | ||
|
|
784c26ac7a | ||
|
|
8ff9805d9d | ||
|
|
14f1e19a91 | ||
|
|
ee80dea28a | ||
|
|
d521d628ca | ||
|
|
fcca920e95 | ||
|
|
e9f1142287 | ||
|
|
e21201cb4b | ||
|
|
4a3cdd0e51 | ||
|
|
8e3ac99a85 | ||
|
|
c288e4d9f3 | ||
|
|
80ada89391 | ||
|
|
f02b79a621 | ||
|
|
d1a7cec69c | ||
|
|
dee6226ea9 | ||
|
|
152ca0e500 | ||
|
|
39cf70c334 | ||
|
|
20efd0e6f8 | ||
|
|
75f669b227 | ||
|
|
6f14f039aa | ||
|
|
83cbaae024 | ||
|
|
e0e4ade04e | ||
|
|
42ce333d51 | ||
|
|
09e16f6fc8 | ||
|
|
cb6075a569 | ||
|
|
a2cdc1345d | ||
|
|
0bde1fc9eb | ||
|
|
320307f765 | ||
|
|
81db2adb17 | ||
|
|
d2905f89e2 | ||
|
|
9b0e2817ca | ||
|
|
2b126f5ddb | ||
|
|
d1e8e441f6 | ||
|
|
caac0cb646 | ||
|
|
8c2c556ad3 | ||
|
|
093308d23d | ||
|
|
da9125f8b0 | ||
|
|
176f631782 | ||
|
|
d95e990284 | ||
|
|
03e9e1c63f | ||
|
|
801445b1e1 | ||
|
|
10f892ff25 | ||
|
|
e5a0255909 | ||
|
|
c393df2a72 | ||
|
|
585f49ddb8 | ||
|
|
19420a2b49 | ||
|
|
9b8df36dde | ||
|
|
6f5b24c6ed | ||
|
|
7eb6bf61ce | ||
|
|
793e5130d1 | ||
|
|
263534f965 | ||
|
|
b829cb7d6d | ||
|
|
6e0f5059ae | ||
|
|
e9256fda39 | ||
|
|
7d19cb3d68 | ||
|
|
616cfb1adb | ||
|
|
e0e95863e5 | ||
|
|
af442cbf6c | ||
|
|
d952d779a0 | ||
|
|
d7e839dad5 | ||
|
|
0660f2a88c | ||
|
|
bf68fbd480 | ||
|
|
688ef2ce21 | ||
|
|
6a5a27bf0e | ||
|
|
803c79c4f9 | ||
|
|
22525bad0e | ||
|
|
de043add81 | ||
|
|
8006c2c68e | ||
|
|
f826a28fc2 | ||
|
|
bec3d589db | ||
|
|
ade34797d8 | ||
|
|
5d8cc872fa | ||
|
|
b93e99dcd6 | ||
|
|
bdb34a3309 | ||
|
|
e57313a1f4 | ||
|
|
423a02fa45 | ||
|
|
92181ab153 | ||
|
|
bcf8ddb1e2 | ||
|
|
b355cc240c | ||
|
|
1c5e14884d | ||
|
|
2051da0be7 | ||
|
|
baf69d50f1 | ||
|
|
dd40ac5f74 | ||
|
|
85f486c2ea | ||
|
|
450ab94d9b | ||
|
|
639aa29c4a | ||
|
|
bb97466c43 | ||
|
|
9e88fb35fd | ||
|
|
29ef29e99a | ||
|
|
f1ca96647c | ||
|
|
8441ffc233 | ||
|
|
9f5e92e431 | ||
|
|
bd9c63c55d | ||
|
|
4b97c42702 | ||
|
|
926e572588 | ||
|
|
4dcc6bc9fa | ||
|
|
85e2e8cd6a | ||
|
|
2c21fa99a0 | ||
|
|
9d19c4b69c | ||
|
|
99d15b0416 | ||
|
|
32a328a054 | ||
|
|
d839a4d4e6 | ||
|
|
9cdf2340a8 | ||
|
|
3a220a796c | ||
|
|
2a8e19b222 | ||
|
|
51142efc8b | ||
|
|
b1e835415c | ||
|
|
514f9e0575 | ||
|
|
34954c8d64 | ||
|
|
eb8109de09 | ||
|
|
cfb86ef85e | ||
|
|
59aa80bd9a | ||
|
|
830b8faf9b | ||
|
|
3c0f8181ac | ||
|
|
489edea4ad | ||
|
|
fc2ff55743 | ||
|
|
f3d161382d | ||
|
|
4c00bc4982 | ||
|
|
82dd157aca | ||
|
|
f901b5ae5f | ||
|
|
91486ae41c | ||
|
|
63910c89d6 | ||
|
|
184cb81275 | ||
|
|
15b90a0281 | ||
|
|
1eacf8e674 | ||
|
|
db6013d82a | ||
|
|
fef3ba6e11 | ||
|
|
4aaf880a46 | ||
|
|
2500b207d3 | ||
|
|
71a3226267 | ||
|
|
a160d8b7ae | ||
|
|
e1d3c48e90 | ||
|
|
f016befaef | ||
|
|
1504b86572 | ||
|
|
2d98cd74b7 | ||
|
|
a871940545 | ||
|
|
a6291809a6 | ||
|
|
6c515648da | ||
|
|
e9f0cb0e8a | ||
|
|
dd8e0728b6 | ||
|
|
8c46de0168 | ||
|
|
d93af458dc | ||
|
|
9be788d110 | ||
|
|
89dc03f2c7 | ||
|
|
28689319f6 | ||
|
|
177a70ef78 | ||
|
|
15e5ca6c86 | ||
|
|
40c4d7f46e | ||
|
|
29f9a88f00 | ||
|
|
846425172d | ||
|
|
c53ac13ec5 | ||
|
|
17ce608985 | ||
|
|
2596b73ebc | ||
|
|
2834f048a3 | ||
|
|
840f2eca2a | ||
|
|
f85e17245a | ||
|
|
3c97894b93 | ||
|
|
15709fa8eb | ||
|
|
6a8f24ccdb | ||
|
|
44b576c96c | ||
|
|
707ad07238 | ||
|
|
f0cb822bc7 | ||
|
|
9402bd68b9 | ||
|
|
9d112756ff | ||
|
|
15bc33492b | ||
|
|
697ac34dae | ||
|
|
a91f3d8636 | ||
|
|
56469a29db | ||
|
|
c8bbe47189 | ||
|
|
30f3acd49a | ||
|
|
7649bba353 | ||
|
|
7c344da791 | ||
|
|
398f85181c | ||
|
|
b9464c30e9 | ||
|
|
1c42af3740 | ||
|
|
16ab6a833a | ||
|
|
f7f864d6a1 | ||
|
|
199e069c4d | ||
|
|
c4b3e79200 | ||
|
|
8fe94af974 | ||
|
|
f7b7bda730 | ||
|
|
1f93ba5c6c | ||
|
|
ee6f4ac163 | ||
|
|
7d16a878b0 | ||
|
|
7603ad8a9d | ||
|
|
4879d7d614 | ||
|
|
4bad50337f | ||
|
|
c059d8d585 | ||
|
|
321e2d0d55 | ||
|
|
862daf338a | ||
|
|
2d281d2c84 | ||
|
|
88a36a6a6d | ||
|
|
4e7d6f4584 | ||
|
|
a5759d7681 | ||
|
|
cb506458ee | ||
|
|
7a4b2fb07e | ||
|
|
cce183e7ce | ||
|
|
8386526374 | ||
|
|
c1f4bff840 | ||
|
|
9a8e0a088e | ||
|
|
a583764b74 | ||
|
|
d84764c51e | ||
|
|
7866f7e20b | ||
|
|
359a8dacc6 | ||
|
|
9c477a75f3 | ||
|
|
1235d9a8ee | ||
|
|
7e899cff28 | ||
|
|
54ed754436 | ||
|
|
2cf943207c | ||
|
|
9521828422 | ||
|
|
d12c549a1f | ||
|
|
af54379afa | ||
|
|
e5f9d2c412 | ||
|
|
f94ec2cce8 | ||
|
|
1299f251a1 | ||
|
|
715174d314 | ||
|
|
12ea8d42b5 | ||
|
|
32c0bf9e06 | ||
|
|
e36669d9c6 | ||
|
|
9526eaa937 | ||
|
|
be3a46b854 | ||
|
|
8db8b43059 | ||
|
|
5b19999dfc | ||
|
|
69353015bb | ||
|
|
a4fb40e496 | ||
|
|
c27b0a35f4 | ||
|
|
61f7c7f243 | ||
|
|
250058711c | ||
|
|
03aaf47d77 | ||
|
|
4ac74859b0 | ||
|
|
28640cfd93 | ||
|
|
27fbc96829 | ||
|
|
ee28134c9b | ||
|
|
de322a10aa | ||
|
|
d9d8c688e3 | ||
|
|
f52d2f09f5 | ||
|
|
215c0bc197 | ||
|
|
80e690534a | ||
|
|
dacca2446d | ||
|
|
53884aebf5 | ||
|
|
49926dbabf | ||
|
|
cfca498700 | ||
|
|
69b63e89ea | ||
|
|
45b2f147e8 | ||
|
|
48863d24c3 | ||
|
|
6d146ab82d | ||
|
|
8a9a0a162b | ||
|
|
d68187e1fc | ||
|
|
6655faa047 | ||
|
|
6ec4282d39 | ||
|
|
34d35b84ca | ||
|
|
bd33c86a8d | ||
|
|
97ba53dfe6 | ||
|
|
9423ca83c0 | ||
|
|
7f147bdf15 | ||
|
|
f8d3566137 | ||
|
|
b3e900b24b | ||
|
|
589a665eb4 | ||
|
|
c451e286bc | ||
|
|
9961c354af | ||
|
|
e0d2407749 | ||
|
|
23bda114fa | ||
|
|
3522a5ccf2 | ||
|
|
347c5ef47c | ||
|
|
6bf8b21cdf | ||
|
|
036de06748 | ||
|
|
dac5a001cd | ||
|
|
907a416009 | ||
|
|
0b27a96fb0 | ||
|
|
3fbfeddf3d | ||
|
|
153e039685 | ||
|
|
bcfe9e44a1 | ||
|
|
06c664ae79 | ||
|
|
bb04cccad2 | ||
|
|
ce4af92c59 | ||
|
|
df1366e128 | ||
|
|
bee426d232 | ||
|
|
0e0f5bb92d | ||
|
|
7efd8d3627 | ||
|
|
e63d82ef3e | ||
|
|
c6c8ec0d3c | ||
|
|
70ec6d8615 | ||
|
|
b426fe7eee | ||
|
|
a016ecca40 | ||
|
|
0c9e3d55f9 | ||
|
|
c6dd2765b0 | ||
|
|
9a0fdd429a | ||
|
|
16e5553ed6 | ||
|
|
7d2e51946a | ||
|
|
244a19da32 | ||
|
|
a6684ac137 | ||
|
|
bf2bb2421a | ||
|
|
cc1f9f038f | ||
|
|
a43261f2c6 | ||
|
|
5fb43f2663 | ||
|
|
98f6cd824e | ||
|
|
65e527cdb3 | ||
|
|
b777f58d48 | ||
|
|
564e642f8a | ||
|
|
4543a7fd9b | ||
|
|
b9f6cdb29d | ||
|
|
49864834cd | ||
|
|
79f06ef9d0 | ||
|
|
4afee8dbed | ||
|
|
b770bd58ab | ||
|
|
0b179262b4 | ||
|
|
826d632136 | ||
|
|
a07d90935a | ||
|
|
be6c90d5ce | ||
|
|
d8694f1a3a | ||
|
|
1ba18765d0 | ||
|
|
9cf856f2a9 | ||
|
|
ba74c6bff3 | ||
|
|
743f4ff9d7 |
@@ -18,7 +18,14 @@
|
||||
:use {:level :warning}
|
||||
:warn-on-reflection {:level :warning}
|
||||
|
||||
:metabase/defsetting-must-specify-export {:level :warning}
|
||||
:metabase/defsetting-must-specify-export {:level :warning}
|
||||
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests {:level :warning}
|
||||
:metabase/mbql-query-first-arg {:level :warning}
|
||||
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning}
|
||||
:metabase/validate-deftest {:level :warning}
|
||||
:metabase/validate-logging {:level :warning}
|
||||
:metabase/warn-about-schema= {:level :warning}
|
||||
|
||||
;;
|
||||
;; disabled linters
|
||||
@@ -34,6 +41,7 @@
|
||||
;; :docstring-leading-trailing-whitespace {:level :warning}
|
||||
;; :docstring-no-summary {:level :warning}
|
||||
;; :shadowed-var {:level :warning}
|
||||
;; :def-fn {:level :warning}
|
||||
|
||||
:unresolved-symbol
|
||||
{:exclude
|
||||
@@ -162,6 +170,15 @@
|
||||
metabase-enterprise.sandbox.api.table table
|
||||
metabase-enterprise.test met
|
||||
metabase.analytics.stats stats
|
||||
metabase.analyze.query-results qr
|
||||
metabase.analyze.classifiers.category classifiers.category
|
||||
metabase.analyze.classifiers.core classifiers
|
||||
metabase.analyze.classifiers.name classifiers.name
|
||||
metabase.analyze.classifiers.no-preview-display classifiers.no-preview-display
|
||||
metabase.analyze.classifiers.text-fingerprint classifiers.text-fingerprint
|
||||
metabase.analyze.fingerprint.fingerprinters fingerprinters
|
||||
metabase.analyze.fingerprint.insights insights
|
||||
metabase.analyze.fingerprint.schema fingerprint.schema
|
||||
metabase.api.activity api.activity
|
||||
metabase.api.alert api.alert
|
||||
metabase.api.automagic-dashboards api.magic
|
||||
@@ -262,6 +279,7 @@
|
||||
metabase.legacy-mbql.schema mbql.s
|
||||
metabase.legacy-mbql.util mbql.u
|
||||
metabase.lib.util lib.util
|
||||
metabase.lib.util.match lib.util.match
|
||||
metabase.models.activity activity
|
||||
metabase.models.application-permissions-revision a-perm-revision
|
||||
metabase.models.bookmark bookmark
|
||||
@@ -289,6 +307,7 @@
|
||||
metabase.models.timeline timeline
|
||||
metabase.models.timeline-event timeline-event
|
||||
metabase.native-query-analyzer query-analyzer
|
||||
metabase.native-query-analyzer.parameter-substitution nqa.sub
|
||||
metabase.plugins.initialize plugins.init
|
||||
metabase.public-settings public-settings
|
||||
metabase.public-settings.premium-features premium-features
|
||||
@@ -342,14 +361,7 @@
|
||||
metabase.server.protocols server.protocols
|
||||
metabase.shared.util shared.u
|
||||
metabase.shared.util.currency currency
|
||||
metabase.sync.analyze.classifiers.category classifiers.category
|
||||
metabase.sync.analyze.classifiers.name classifiers.name
|
||||
metabase.sync.analyze.classifiers.no-preview-display classifiers.no-preview-display
|
||||
metabase.sync.analyze.classifiers.text-fingerprint classifiers.text-fingerprint
|
||||
metabase.sync.analyze.fingerprint fingerprint
|
||||
metabase.sync.analyze.fingerprint.fingerprinters fingerprinters
|
||||
metabase.sync.analyze.fingerprint.insights insights
|
||||
metabase.sync.analyze.query-results qr
|
||||
metabase.sync.analyze.fingerprint sync.fingerprint
|
||||
metabase.sync.field-values field-values
|
||||
metabase.sync.interface i
|
||||
metabase.sync.schedules sync.schedules
|
||||
@@ -402,12 +414,7 @@
|
||||
ring.adapter.jetty9.servlet servlet
|
||||
saml20-clj.core saml
|
||||
toucan.db db
|
||||
toucan.models models}}
|
||||
|
||||
:metabase/validate-deftest {:level :warning}
|
||||
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
|
||||
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests {:level :warning}
|
||||
:metabase/warn-about-schema= {:level :warning}}
|
||||
toucan.models models}}}
|
||||
|
||||
:lint-as
|
||||
{clojure.core.logic/defne clj-kondo.lint-as/def-catch-all
|
||||
@@ -571,6 +578,19 @@
|
||||
metabase.test/with-temp hooks.toucan2.tools.with-temp/with-temp
|
||||
metabase.test/with-temp-file hooks.metabase.test.util/with-temp-file
|
||||
metabase.test/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values
|
||||
metabase.util/format-color hooks.metabase.util/format-color
|
||||
metabase.util.log/trace hooks.metabase.util.log/info
|
||||
metabase.util.log/tracef hooks.metabase.util.log/infof
|
||||
metabase.util.log/debug hooks.metabase.util.log/info
|
||||
metabase.util.log/debugf hooks.metabase.util.log/infof
|
||||
metabase.util.log/info hooks.metabase.util.log/info
|
||||
metabase.util.log/infof hooks.metabase.util.log/infof
|
||||
metabase.util.log/warn hooks.metabase.util.log/info
|
||||
metabase.util.log/warnf hooks.metabase.util.log/infof
|
||||
metabase.util.log/error hooks.metabase.util.log/info
|
||||
metabase.util.log/errorf hooks.metabase.util.log/infof
|
||||
metabase.util.log/fatal hooks.metabase.util.log/info
|
||||
metabase.util.log/fatalf hooks.metabase.util.log/infof
|
||||
metabase.util.malli.registry/def hooks.metabase.util.malli.registry/def}
|
||||
|
||||
:macroexpand
|
||||
@@ -631,7 +651,7 @@
|
||||
:name source-namespaces}
|
||||
{:pattern "(?:(?:^metabase\\.cmd.*)|(?:^build.*)|(?:^metabuild-common.*)|(?:^release.*)|(?:^i18n.*)|(?:^lint-migrations-file$))"
|
||||
:name printable-namespaces}
|
||||
{:pattern "^metabase\\.lib\\.*"
|
||||
{:pattern "^metabase\\.(?:(?:lib)|(?:legacy-mbql))\\.*"
|
||||
:name metabase-lib}
|
||||
;;
|
||||
;; QP source namespaces: `metabase.query-processor.*`, `metabase-enterprise.sandbox.query-processor.*`, and
|
||||
@@ -643,12 +663,10 @@
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
{:linters
|
||||
{:inline-def {:level :off}
|
||||
:missing-docstring {:level :off}
|
||||
:private-call {:level :off}
|
||||
:metabase/defsetting-must-specify-export {:level :off}
|
||||
:hooks.metabase.test.data/mbql-query-first-arg {:level :error}
|
||||
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning}}
|
||||
{:inline-def {:level :off}
|
||||
:missing-docstring {:level :off}
|
||||
:private-call {:level :off}
|
||||
:metabase/defsetting-must-specify-export {:level :off}}
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{clojure.core/defmacro hooks.clojure.core/non-thread-safe-form-should-end-with-exclamation
|
||||
@@ -689,19 +707,23 @@
|
||||
metabase.test/with-temp {:message "Don't use application database inside MLv2 code"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{toucan.db {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.models {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.util.test {:message "Don't use application database inside MLv2 code"}
|
||||
toucan2.core {:message "Don't use application database inside MLv2 code"}}}}
|
||||
{metabase.util.malli.schema {:message "Prefer metabase.lib.schema schemas in MLv2 code"}
|
||||
toucan.db {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.models {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.util.test {:message "Don't use application database inside MLv2 code"}
|
||||
toucan2.core {:message "Don't use application database inside MLv2 code"}}}}
|
||||
|
||||
qp-and-driver-source-namespaces
|
||||
{:linters
|
||||
{:discouraged-namespace
|
||||
{toucan.db {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.models {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.util.test {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan2.core {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}
|
||||
{metabase.util.malli.schema {:message "Prefer metabase.lib.schema or metabase.legacy-mbql.schema schemas in QP/driver code"}
|
||||
toucan.db {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.models {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.util.test {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan2.core {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}
|
||||
|
||||
:discouraged-var
|
||||
{metabase.db.query/query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
{medley.core/map-keys {:message "Use clojure.core/update-keys"}
|
||||
medley.core/map-vals {:message "Use clojure.core/update-vals"}
|
||||
metabase.db.query/query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
metabase.db.query/reducible-query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}}}}}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
(ns hooks.clojure.test
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]))
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.string :as str]
|
||||
[hooks.common]))
|
||||
|
||||
(def ^:private disallowed-parallel-forms
|
||||
"Things you should not be allowed to use inside parallel tests. Besides these, anything ending in `!` not whitelisted
|
||||
@@ -106,18 +108,8 @@
|
||||
clojure.core.async/to-chan!
|
||||
clojure.core.async/to-chan!!
|
||||
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
|
||||
metabase.query-processor.store/store-database!})
|
||||
|
||||
(defn- node->qualified-symbol [node]
|
||||
(try
|
||||
(when (hooks/token-node? node)
|
||||
(let [sexpr (hooks/sexpr node)]
|
||||
(when (symbol? sexpr)
|
||||
(when-let [resolved (hooks/resolve {:name sexpr})]
|
||||
(symbol (name (:ns resolved)) (name (:name resolved)))))))
|
||||
;; some symbols like `*count/Integer` aren't resolvable.
|
||||
(catch Exception _
|
||||
nil)))
|
||||
metabase.query-processor.store/store-database!
|
||||
next.jdbc/execute!})
|
||||
|
||||
(defn- warn-about-disallowed-parallel-forms [form]
|
||||
(letfn [(error! [form message]
|
||||
@@ -125,7 +117,7 @@
|
||||
:message message
|
||||
:type :metabase/validate-deftest)))
|
||||
(f [form]
|
||||
(when-let [qualified-symbol (node->qualified-symbol form)]
|
||||
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
|
||||
(cond
|
||||
(disallowed-parallel-forms qualified-symbol)
|
||||
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture" qualified-symbol))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns hooks.common
|
||||
(:require [clj-kondo.hooks-api :as hooks]
|
||||
[clojure.pprint]))
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.pprint]))
|
||||
|
||||
(defn with-macro-meta
|
||||
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
|
||||
@@ -369,3 +370,33 @@
|
||||
arg])
|
||||
body))]
|
||||
{:node node*}))
|
||||
|
||||
(defn node->qualified-symbol [node]
|
||||
(try
|
||||
(when (hooks/token-node? node)
|
||||
(let [sexpr (hooks/sexpr node)]
|
||||
(when (symbol? sexpr)
|
||||
(when-let [resolved (hooks/resolve {:name sexpr})]
|
||||
(symbol (name (:ns resolved)) (name (:name resolved)))))))
|
||||
;; some symbols like `*count/Integer` aren't resolvable.
|
||||
(catch Exception _
|
||||
nil)))
|
||||
|
||||
(defn format-string-specifier-count
|
||||
"Number of things like `%s` in a format string, not counting newlines (`%n`) or escaped percent signs (`%%`). For
|
||||
checking the number of args to something that takes a format string."
|
||||
[format-string]
|
||||
(count (re-seq #"(?<!%)%(?![%n])" format-string)))
|
||||
|
||||
(comment
|
||||
;; should be 1
|
||||
(format-string-specifier-count "%s %%")
|
||||
|
||||
;; should be 2
|
||||
(format-string-specifier-count "%s %%%n%s")
|
||||
|
||||
;; should be 0
|
||||
(format-string-specifier-count "%n%%%%")
|
||||
|
||||
;; should be 1
|
||||
(format-string-specifier-count "%-02d"))
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
metabot-get-prompt-templates-url
|
||||
metabot-prompt-generator-token-limit
|
||||
multi-setting-read-only
|
||||
notebook-native-preview-shown
|
||||
notebook-native-preview-sidebar-width
|
||||
notification-link-base-url
|
||||
num-metabot-choices
|
||||
openai-api-key
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
(when-not ((some-fn symbol? nil?) (hooks/sexpr table))
|
||||
(hooks/reg-finding! (assoc (meta table)
|
||||
:message "First arg to mbql-query should be either a table name symbol or nil."
|
||||
:type ::mbql-query-first-arg)))
|
||||
:type :metabase/mbql-query-first-arg)))
|
||||
(let [result (replace-$id-special-tokens query)
|
||||
;; HACK I'm not sure WHY it works but I ran into https://github.com/clj-kondo/clj-kondo/issues/1773 when
|
||||
;; trying to get this working -- for some magical reason wrapping the whole thing in a `do` form seems to fix
|
||||
|
||||
17
.clj-kondo/hooks/metabase/util.clj
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns hooks.metabase.util
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[hooks.common]))
|
||||
|
||||
(defn format-color [{:keys [node], :as x}]
|
||||
(let [[_format-color _color format-string-node & args] (:children node)]
|
||||
(when (api/string-node? format-string-node)
|
||||
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string-node))
|
||||
actual-arg-count (count args)]
|
||||
(when-not (= expected-arg-count actual-arg-count)
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message (format "metabase.util/format-color format string expects %d arguments instead of %d."
|
||||
expected-arg-count
|
||||
actual-arg-count)
|
||||
:type :format))))))
|
||||
x)
|
||||
157
.clj-kondo/hooks/metabase/util/log.clj
Normal file
@@ -0,0 +1,157 @@
|
||||
(ns hooks.metabase.util.log
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as api]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :as walk]
|
||||
[hooks.common]))
|
||||
|
||||
;;;; Checks for both log + logf
|
||||
|
||||
(defn- disallow-i18n [node]
|
||||
(walk/postwalk
|
||||
(fn [x]
|
||||
(when-let [qualified-symbol (hooks.common/node->qualified-symbol x)]
|
||||
(when (and (= (namespace qualified-symbol) "metabase.util.i18n")
|
||||
(or (str/includes? (name qualified-symbol) "tru")
|
||||
(str/includes? (name qualified-symbol) "trs")))
|
||||
(api/reg-finding! (assoc (meta x)
|
||||
:message "do not i18n the logs!"
|
||||
:type :metabase/validate-logging)))))
|
||||
node))
|
||||
|
||||
(defn- check-position-of-exception-arg
|
||||
"Tell people to do (info e \"whatever\") instead of (info \"whatever\" e)."
|
||||
[node]
|
||||
(let [[f _first-arg & args] (:children node)]
|
||||
(doseq [arg args]
|
||||
(when (and (api/token-node? arg)
|
||||
(= (api/sexpr arg) 'e))
|
||||
(api/reg-finding! (assoc (meta arg)
|
||||
:message (format "pass exceptions as the first arg to %s" (api/sexpr f))
|
||||
:type :metabase/validate-logging))))))
|
||||
|
||||
(defn- common-checks [node]
|
||||
(check-position-of-exception-arg node)
|
||||
(disallow-i18n node))
|
||||
|
||||
(defn- check-for-i18n-format-specifiers
|
||||
"Make sure people aren't using things like {0} in their log messages."
|
||||
[node string-node]
|
||||
(when (re-find #"\{\d+\}" (api/sexpr string-node))
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message "this looks like an i18n format string. Don't use identifiers like {0} in logging."
|
||||
:type :metabase/validate-logging))))
|
||||
|
||||
;;; log only
|
||||
|
||||
(defn- check-not-format-string
|
||||
"Make sure we're not using something like log/trace with a format string."
|
||||
[{[f] :children, :as node} string-node]
|
||||
(when (pos? (hooks.common/format-string-specifier-count (api/sexpr string-node)))
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message (format "%s used with a format string, use %s instead."
|
||||
(api/sexpr f)
|
||||
(str (api/sexpr f) "f"))
|
||||
:type :metabase/validate-logging))))
|
||||
|
||||
(defn- warn-against-log+format-instead-of-logf
|
||||
"Tell people to use things like infof instead of info + format."
|
||||
[{[f] :children, :as node} [x :as args]]
|
||||
(when (and (= (count args) 1)
|
||||
(api/list-node? x))
|
||||
(let [[y] (:children x)]
|
||||
(when (and (api/token-node? y)
|
||||
(= (hooks.common/node->qualified-symbol y) 'clojure.core/format))
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message (format "Use %s instead of %s + format"
|
||||
(str (api/sexpr f) "f")
|
||||
(api/sexpr f))
|
||||
:type :metabase/validate-logging))))))
|
||||
|
||||
(defn info
|
||||
"Valid log, debug, etc."
|
||||
[{:keys [node], :as x}]
|
||||
(common-checks node)
|
||||
(let [[_f & args] (:children node)
|
||||
[_e & [x :as args]] (if (and (api/token-node? (first args))
|
||||
(api/string-node? (second args)))
|
||||
args
|
||||
(cons nil args))]
|
||||
(when (api/string-node? x)
|
||||
(check-for-i18n-format-specifiers node x)
|
||||
(check-not-format-string node x))
|
||||
(warn-against-log+format-instead-of-logf node args))
|
||||
x)
|
||||
|
||||
;;;; logf only
|
||||
|
||||
(defn- check-format-string-arg-count
|
||||
"Make sure the number of args passed to something like tracef match the number of format string specifiers."
|
||||
[{[f] :children, :as node} format-string args]
|
||||
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string))
|
||||
actual-arg-count (count args)]
|
||||
(when (zero? expected-arg-count)
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message (format "Don't use %s with no format string arguments, use %s instead."
|
||||
(api/sexpr f)
|
||||
(str/replace (str (api/sexpr f)) #"f$" ""))
|
||||
:type :metabase/validate-logging)))
|
||||
(when-not (= expected-arg-count actual-arg-count)
|
||||
(api/reg-finding! (assoc (meta node)
|
||||
:message (format "log format string expects %d arguments instead of %d."
|
||||
expected-arg-count
|
||||
actual-arg-count)
|
||||
:type :metabase/validate-logging)))))
|
||||
|
||||
|
||||
|
||||
(defn infof
|
||||
"Valid logf, debugf, etc."
|
||||
[{:keys [node], :as x}]
|
||||
(common-checks node)
|
||||
(let [[_f & args] (:children node)
|
||||
[_e format-string & args] (if (api/string-node? (first args))
|
||||
(cons nil args)
|
||||
args)]
|
||||
;; TODO -- maybe we can error if you did not pass a format string, e.g. (log/infof 1 2) is obviously incorrect.
|
||||
(when (api/string-node? format-string)
|
||||
(check-for-i18n-format-specifiers node format-string)
|
||||
(check-format-string-arg-count node format-string args)))
|
||||
x)
|
||||
|
||||
(comment
|
||||
;; should fail, missing a format arg
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf "Migration lock was acquired after %d retries.")))})
|
||||
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf e "Migration lock was acquired after %d retries.")))})
|
||||
|
||||
;; should fail, too many format args
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf "Migration lock was acquired after %d retries." 1 2)))})
|
||||
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf e "Migration lock was acquired after %d retries." 1 2)))})
|
||||
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf "Migration lock was acquired after {0} retries." 1)))})
|
||||
|
||||
;; should fail, has format args but is warn rather than warnf
|
||||
(info
|
||||
{:node (api/parse-string
|
||||
(str '(log/warn e "Migration lock was acquired after %d retries." 1 2)))})
|
||||
|
||||
;; should fail -- should not use i18n/tru or i18n/trs
|
||||
(infof
|
||||
{:node (api/parse-string
|
||||
(str '(log/warnf (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1))))})
|
||||
|
||||
(info
|
||||
{:node (api/parse-string
|
||||
(str '(log/warn (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1))))}))
|
||||
@@ -35,6 +35,7 @@
|
||||
(eval . (put 'methodical/defmulti 'clojure-doc-string-elt 2))
|
||||
(eval . (put 'mi/define-simple-hydration-method 'clojure-doc-string-elt 3))
|
||||
(eval . (put 'mi/define-batched-hydration-method 'clojure-doc-string-elt 3))
|
||||
(eval . (put 'mr/def 'clojure-doc-string-elt 2))
|
||||
(eval . (put 'mu/defn 'clojure-doc-string-elt 2))
|
||||
;; Define custom indentation for functions inside metabase.
|
||||
;; This list isn't complete; add more forms as we come across them.
|
||||
@@ -84,6 +85,7 @@
|
||||
(clojure-indent-style . always-align)
|
||||
(cljr-favor-prefix-notation . nil)
|
||||
(cljr-insert-newline-after-require . t)
|
||||
(cljr-print-right-margin . 118)
|
||||
(clojure-docstring-fill-column . 118)
|
||||
(cider-preferred-build-tool . clojure-cli)
|
||||
(cider-default-cljs-repl . shadow-select)
|
||||
|
||||
210
.eslintrc
@@ -1,210 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"strict": [2, "never"],
|
||||
"no-undef": 2,
|
||||
"no-var": 1,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"no-empty": [1, { "allowEmptyCatch": true }],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "moment",
|
||||
"message": "Moment is deprecated, please use dayjs"
|
||||
},
|
||||
{
|
||||
"name": "moment-timezone",
|
||||
"message": "Moment is deprecated, please use dayjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"curly": [1, "all"],
|
||||
"eqeqeq": [1, "smart"],
|
||||
"import/no-default-export": 2,
|
||||
"import/no-named-as-default": 0,
|
||||
"import/no-commonjs": 1,
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"orderImportKind": "asc",
|
||||
"caseInsensitive": false
|
||||
},
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"warnOnUnassignedImports": false
|
||||
}
|
||||
],
|
||||
"no-console": [2, { "allow": ["warn", "error", "errorBuffer"] }],
|
||||
"react/no-is-mounted": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/display-name": 1,
|
||||
"react/prop-types": 2,
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 0,
|
||||
"react/no-find-dom-node": 0,
|
||||
"react/no-children-prop": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/jsx-no-target-blank": 2,
|
||||
"react/jsx-key": 2,
|
||||
"react/forbid-component-props": [2, { "forbid": ["sx"] }],
|
||||
"react-hooks/exhaustive-deps": [
|
||||
"warn",
|
||||
{ "additionalHooks": "(useSyncedQueryString|useSafeAsyncFunction)" }
|
||||
],
|
||||
"prefer-const": [1, { "destructuring": "all" }],
|
||||
"no-useless-escape": 0,
|
||||
"no-only-tests/no-only-tests": [
|
||||
"error",
|
||||
{
|
||||
"block": [
|
||||
"describe",
|
||||
"it",
|
||||
"context",
|
||||
"test",
|
||||
"tape",
|
||||
"fixture",
|
||||
"serial",
|
||||
"Feature",
|
||||
"Scenario",
|
||||
"Given",
|
||||
"And",
|
||||
"When",
|
||||
"Then",
|
||||
"describeWithSnowplow",
|
||||
"describeEE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"complexity": ["error", { "max": 54 }]
|
||||
},
|
||||
"globals": {
|
||||
"before": true,
|
||||
"cy": true,
|
||||
"Cypress": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"commonjs": true,
|
||||
"jest": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": ["react", "no-only-tests"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"settings": {
|
||||
"import/internal-regex": "^metabase/|^metabase-lib/",
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"typescript": true
|
||||
}
|
||||
},
|
||||
"import/ignore": ["\\.css$"],
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"legacyDecorators": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js", "*.jsx", "*.ts", "*.tsx"],
|
||||
"rules": {
|
||||
"no-unconditional-metabase-links-render": "error",
|
||||
"no-literal-metabase-strings": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*"
|
||||
],
|
||||
"rules": {
|
||||
"no-unconditional-metabase-links-render": "off",
|
||||
"no-literal-metabase-strings": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"prefer-rest-params": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true,
|
||||
"destructuredArrayIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
// This was introduced in 6.0.0
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extends": [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react"
|
||||
],
|
||||
"plugins": ["jest", "jest-dom", "testing-library"],
|
||||
"files": [
|
||||
"*.unit.spec.ts",
|
||||
"*.unit.spec.tsx",
|
||||
"*.unit.spec.js",
|
||||
"*.unit.spec.jsx"
|
||||
],
|
||||
"rules": {
|
||||
"jest/valid-title": ["error", { "ignoreTypeOfDescribeName": true }],
|
||||
// In unit tests, you should use `act(() => {})` to invoke things causing state updates
|
||||
// and rerenders to remove lengthy warnings from jest. Disabling the rule allows this.
|
||||
"testing-library/no-unnecessary-act": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
224
.eslintrc.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// `postcss-modules` lints css modules class names, but it currently crashes
|
||||
// eslint on vscode. If you use webstorm or want to run the lint for the cli, you
|
||||
// can use this flag to enable it. This is set to true in CI
|
||||
const shouldLintCssModules =
|
||||
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
strict: [2, "never"],
|
||||
"no-undef": 2,
|
||||
"no-var": 1,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
args: "none",
|
||||
varsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"no-empty": [1, { allowEmptyCatch: true }],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "moment",
|
||||
message: "Moment is deprecated, please use dayjs",
|
||||
},
|
||||
{
|
||||
name: "moment-timezone",
|
||||
message: "Moment is deprecated, please use dayjs",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
curly: [1, "all"],
|
||||
eqeqeq: [1, "smart"],
|
||||
"import/no-default-export": 2,
|
||||
"import/no-named-as-default": 0,
|
||||
"import/no-commonjs": 1,
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
orderImportKind: "asc",
|
||||
caseInsensitive: false,
|
||||
},
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
warnOnUnassignedImports: false,
|
||||
},
|
||||
],
|
||||
"no-console": [2, { allow: ["warn", "error", "errorBuffer"] }],
|
||||
"react/no-is-mounted": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/display-name": 1,
|
||||
"react/prop-types": 2,
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 0,
|
||||
"react/no-find-dom-node": 0,
|
||||
"react/no-children-prop": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/jsx-no-target-blank": 2,
|
||||
"react/jsx-key": 2,
|
||||
"react/forbid-component-props": [2, { forbid: ["sx"] }],
|
||||
"react-hooks/exhaustive-deps": [
|
||||
"warn",
|
||||
{ additionalHooks: "(useSyncedQueryString|useSafeAsyncFunction)" },
|
||||
],
|
||||
"prefer-const": [1, { destructuring: "all" }],
|
||||
"no-useless-escape": 0,
|
||||
"no-only-tests/no-only-tests": [
|
||||
"error",
|
||||
{
|
||||
block: [
|
||||
"describe",
|
||||
"it",
|
||||
"context",
|
||||
"test",
|
||||
"tape",
|
||||
"fixture",
|
||||
"serial",
|
||||
"Feature",
|
||||
"Scenario",
|
||||
"Given",
|
||||
"And",
|
||||
"When",
|
||||
"Then",
|
||||
"describeWithSnowplow",
|
||||
"describeEE",
|
||||
],
|
||||
},
|
||||
],
|
||||
complexity: ["error", { max: 54 }],
|
||||
...(shouldLintCssModules
|
||||
? {
|
||||
"postcss-modules/no-undef-class": "error",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
globals: {
|
||||
before: true,
|
||||
cy: true,
|
||||
Cypress: true,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
commonjs: true,
|
||||
jest: true,
|
||||
"jest/globals": true,
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
plugins: ["react", "no-only-tests", "postcss-modules"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
settings: {
|
||||
"import/internal-regex": "^metabase/|^metabase-lib/",
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
typescript: true,
|
||||
},
|
||||
},
|
||||
"import/ignore": ["\\.css$"],
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"postcss-modules": {
|
||||
baseDir: "./frontend/src",
|
||||
},
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true,
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
|
||||
rules: {
|
||||
"no-unconditional-metabase-links-render": "error",
|
||||
"no-literal-metabase-strings": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
],
|
||||
rules: {
|
||||
"no-unconditional-metabase-links-render": "off",
|
||||
"no-literal-metabase-strings": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"prefer-rest-params": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
// This was introduced in 6.0.0
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react",
|
||||
],
|
||||
plugins: ["jest", "jest-dom", "testing-library"],
|
||||
files: [
|
||||
"*.unit.spec.ts",
|
||||
"*.unit.spec.tsx",
|
||||
"*.unit.spec.js",
|
||||
"*.unit.spec.jsx",
|
||||
],
|
||||
rules: {
|
||||
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
11
.github/CODEOWNERS
vendored
@@ -1,3 +1,14 @@
|
||||
# Before changing this file, make sure you have read how we use it:
|
||||
# https://www.notion.so/metabase/Pull-Request-Code-Review-Guide-374f05df889748b69d67af753f9bc9b8?pvs=4#36550c04ceab40b5a8b8bee6264b090c
|
||||
# TL;DR: if @camsaul hasn't been involved you're probably doing it wrong
|
||||
|
||||
src/metabase @camsaul
|
||||
modules @camsaul
|
||||
enterprise/backend @camsaul
|
||||
enterprise/backend/src/metabase_enterprise/advanced_permissions @noahmoss
|
||||
enterprise/backend/src/metabase_enterprise/audit_app @noahmoss
|
||||
enterprise/backend/src/metabase_enterprise/sso @noahmoss
|
||||
enterprise/backend/src/metabase_enterprise/task/truncate_audit_table.clj @noahmoss
|
||||
enterprise/backend/src/metabase_enterprise/internal_user.clj @noahmoss
|
||||
src/metabase/**/*permissions* @noahmoss
|
||||
src/metabase/integrations @noahmoss
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,5 +1,9 @@
|
||||
> [!IMPORTANT]
|
||||
> If you are merging into master, please add either a `backport` or a `no-backport` label to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71) in the Metabase Branching Strategy document for more details.
|
||||
> For those employed by Metabase: if you are merging into master, please add either a `backport` or a `no-backport` label
|
||||
> to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this
|
||||
> PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71)
|
||||
> in the Metabase Branching Strategy document for more details. If you're not employed by Metabase, this section does not
|
||||
> apply to you, and the label will be taken care of by your reviewer.
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
|
||||
4
.github/file-paths.yaml
vendored
@@ -23,10 +23,12 @@ frontend_sources: &frontend_sources
|
||||
- "**/tsconfig*.json"
|
||||
- "package.json"
|
||||
- "babel.config.json"
|
||||
- ".eslintrc"
|
||||
- ".eslintrc.js"
|
||||
- "postcss.config.js"
|
||||
- "webpack.config.js"
|
||||
- "webpack.embedding-sdk.config.js"
|
||||
- "webpack.static-viz.config.js"
|
||||
- ".nvmrc"
|
||||
|
||||
frontend_specs: &frontend_specs
|
||||
- *shared_specs
|
||||
|
||||
3
.github/team.json
vendored
@@ -7,7 +7,6 @@
|
||||
"ranquild",
|
||||
"uladzimirdev",
|
||||
"kamilmielnik",
|
||||
"oleggromov",
|
||||
"nemanjaglumac",
|
||||
"romeovs"
|
||||
]
|
||||
@@ -15,7 +14,7 @@
|
||||
{
|
||||
"name": "Embedding",
|
||||
"label": ".Team/Embedding",
|
||||
"members": ["WiNloSt", "deniskaber", "oisincoveney", "npretto"]
|
||||
"members": ["WiNloSt", "deniskaber", "oisincoveney", "npretto", "heypoom"]
|
||||
},
|
||||
{
|
||||
"name": "BEC",
|
||||
|
||||
39
.github/workflows/embedding-sdk.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Embedding SDK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "release-**"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sdk-build:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'embedding-sdk-build')
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: "cljs"
|
||||
- name: Build SDK bundle
|
||||
run: yarn run build-embedding-sdk
|
||||
- name: Generate SDK package.json
|
||||
run: yarn run embedding-sdk:generate-package "$GITHUB_SHA"
|
||||
- name: Generate .npmrc for Github npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: "https://npm.pkg.github.com"
|
||||
- name: Publish package
|
||||
run: yarn run embedding-sdk:publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
32
.github/workflows/mbql-library-changelog.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Update changelog for MBQL library changes
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
paths:
|
||||
- src/metabase/lib/js.cljs
|
||||
jobs:
|
||||
mbql-library-changelog-check:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !contains( github.event.pull_request.labels.*.name, '.metabase-lib/no-change' ) }}
|
||||
steps:
|
||||
# - uses: jesusvasquez333/verify-pr-label-action@v1.4.0
|
||||
# with:
|
||||
# github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
# valid-labels: '.metabase-lib/no-change'
|
||||
# disable-reviews: true
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for updated MBQL library changelog
|
||||
run: |
|
||||
if [[ $(git show HEAD -- docs/developers-guide/mbql-library-changelog.md) ]]; then
|
||||
echo "Library and changelog updated together - all good"
|
||||
else
|
||||
echo "metabase.lib.js namespace changed but docs/developers/guide/mbql-library-changelog.md did not"
|
||||
echo "Update the changelog with the changes made to the library's API."
|
||||
echo "If the edits did not change the API, add the `.metabase-lib/no-change` label."
|
||||
exit 1
|
||||
fi
|
||||
260
.github/workflows/pre-release.yml
vendored
@@ -5,11 +5,11 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Metabase version (e.g. v0.46.3)'
|
||||
description: "Metabase version (e.g. v0.46.3)"
|
||||
type: string
|
||||
required: true
|
||||
commit:
|
||||
description: 'A full-length commit SHA-1 hash'
|
||||
description: "A full-length commit SHA-1 hash"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
@@ -91,21 +91,21 @@ jobs:
|
||||
edition: [oss, ee]
|
||||
java-version: [11, 17]
|
||||
steps:
|
||||
- name: Prepare JRE (Java Run-time Environment)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-package: jre
|
||||
java-version: ${{ matrix.java-version }}
|
||||
distribution: 'temurin'
|
||||
- run: java -version
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Launch uberjar (and keep it running)
|
||||
run: java -jar ./metabase.jar &
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
- name: Prepare JRE (Java Run-time Environment)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-package: jre
|
||||
java-version: ${{ matrix.java-version }}
|
||||
distribution: "temurin"
|
||||
- run: java -version
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Launch uberjar (and keep it running)
|
||||
run: java -jar ./metabase.jar &
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
|
||||
run-sanity-check:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -132,43 +132,43 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-artifact.outputs.commit }}
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
- name: Prepare Cypress environment
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
- name: Run Snowplow micro
|
||||
uses: ./.github/actions/run-snowplow-micro
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact for ${{ matrix.edition }}
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Get the version info
|
||||
run: |
|
||||
jar xf metabase.jar version.properties
|
||||
mv version.properties resources/
|
||||
- name: Run a few important Cypress tests as sanity check
|
||||
run: |
|
||||
mkdir -p ./target/uberjar && cp metabase.jar ./target/uberjar/metabase.jar
|
||||
yarn test-cypress-run --folder onboarding
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-artifact.outputs.commit }}
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
- name: Prepare Cypress environment
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
- name: Run Snowplow micro
|
||||
uses: ./.github/actions/run-snowplow-micro
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact for ${{ matrix.edition }}
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Get the version info
|
||||
run: |
|
||||
jar xf metabase.jar version.properties
|
||||
mv version.properties resources/
|
||||
- name: Run a few important Cypress tests as sanity check
|
||||
run: |
|
||||
mkdir -p ./target/uberjar && cp metabase.jar ./target/uberjar/metabase.jar
|
||||
yarn test-cypress-run --folder onboarding
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-failed-tests-recording
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-failed-tests-recording
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
containerize:
|
||||
needs: [release-artifact, run-sanity-check]
|
||||
@@ -185,71 +185,71 @@ jobs:
|
||||
outputs:
|
||||
image_tag: ${{ steps.target.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Get the version
|
||||
run: |
|
||||
if [[ "${{ matrix.edition }}" == "ee" ]]; then
|
||||
echo "VERSION=${{ needs.release-artifact.outputs.ee_version }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION=${{ needs.release-artifact.outputs.oss_version }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-artifact.outputs.commit }}
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Move the Uberjar to the context dir
|
||||
run: mv metabase.jar bin/docker/.
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Build ${{ matrix.edition }} container
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: bin/docker/.
|
||||
platforms: linux/amd64
|
||||
network: host
|
||||
tags: localhost:5000/local-metabase:${{ env.VERSION }}
|
||||
no-cache: true
|
||||
push: true
|
||||
- name: Get the version
|
||||
run: |
|
||||
if [[ "${{ matrix.edition }}" == "ee" ]]; then
|
||||
echo "VERSION=${{ needs.release-artifact.outputs.ee_version }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION=${{ needs.release-artifact.outputs.oss_version }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-artifact.outputs.commit }}
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Retrieve uberjar artifact
|
||||
with:
|
||||
name: metabase-${{ matrix.edition }}-uberjar
|
||||
- name: Move the Uberjar to the context dir
|
||||
run: mv metabase.jar bin/docker/.
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Build ${{ matrix.edition }} container
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: bin/docker/.
|
||||
platforms: linux/amd64
|
||||
network: host
|
||||
tags: localhost:5000/local-metabase:${{ env.VERSION }}
|
||||
no-cache: true
|
||||
push: true
|
||||
|
||||
- name: Launch container
|
||||
run: docker run --rm -dp 3000:3000 localhost:5000/local-metabase:${{ env.VERSION }}
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 3
|
||||
- name: Launch container
|
||||
run: docker run --rm -dp 3000:3000 localhost:5000/local-metabase:${{ env.VERSION }}
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
|
||||
- name: Retag and push container image to ${{ vars.DOCKERHUB_OWNER }} staging Docker Hub repo
|
||||
id: target
|
||||
run: |
|
||||
# SHA-1 truncated to 7 digits should be enough
|
||||
SHORT_HASH=${COMMIT:0:7}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
|
||||
- name: Retag and push container image to ${{ vars.DOCKERHUB_OWNER }} staging Docker Hub repo
|
||||
id: target
|
||||
run: |
|
||||
# SHA-1 truncated to 7 digits should be enough
|
||||
SHORT_HASH=${COMMIT:0:7}
|
||||
|
||||
IMAGE_TAG=$VERSION-$SHORT_HASH
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
IMAGE_TAG=$VERSION-$SHORT_HASH
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Retag local image for staging"
|
||||
docker tag localhost:5000/local-metabase:$VERSION $CONTAINER_IMAGE
|
||||
echo "Retag local image for staging"
|
||||
docker tag localhost:5000/local-metabase:$VERSION $CONTAINER_IMAGE
|
||||
|
||||
echo "Pushing container image $CONTAINER_IMAGE ..."
|
||||
docker push $CONTAINER_IMAGE
|
||||
echo "Pushing container image $CONTAINER_IMAGE ..."
|
||||
docker push $CONTAINER_IMAGE
|
||||
|
||||
echo "Finished!"
|
||||
shell: bash
|
||||
env:
|
||||
COMMIT: ${{ needs.release-artifact.outputs.commit }}
|
||||
echo "Finished!"
|
||||
shell: bash
|
||||
env:
|
||||
COMMIT: ${{ needs.release-artifact.outputs.commit }}
|
||||
|
||||
verify-docker-pull:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -261,22 +261,22 @@ jobs:
|
||||
env:
|
||||
IMAGE_TAG: ${{ needs.containerize.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Login to Docker Hub # authenticated, to avoid being rate-throttled
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
|
||||
- name: Pull the container image
|
||||
run: |
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
echo "Pulling container image $CONTAINER_IMAGE ..."
|
||||
docker pull $CONTAINER_IMAGE
|
||||
echo "Successful!"
|
||||
- name: Launch container
|
||||
run: |
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
docker run --rm -dp 3000:3000 $CONTAINER_IMAGE
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 3
|
||||
- name: Login to Docker Hub # authenticated, to avoid being rate-throttled
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
|
||||
- name: Pull the container image
|
||||
run: |
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
echo "Pulling container image $CONTAINER_IMAGE ..."
|
||||
docker pull $CONTAINER_IMAGE
|
||||
echo "Successful!"
|
||||
- name: Launch container
|
||||
run: |
|
||||
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
|
||||
docker run --rm -dp 3000:3000 $CONTAINER_IMAGE
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 3
|
||||
|
||||
1
.gitignore
vendored
@@ -51,6 +51,7 @@
|
||||
/resources/frontend_client/embed.html
|
||||
/resources/frontend_client/index.html
|
||||
/resources/frontend_client/public.html
|
||||
/resources/embedding-sdk
|
||||
/resources/i18n/*.edn
|
||||
/resources/instance_analytics.zip
|
||||
/resources/license-backend-third-party.txt
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<style>
|
||||
:root {
|
||||
--default-font-family: "Lato";
|
||||
--color-brand: #509ee3;
|
||||
--color-brand-alpha-04: rgba(80, 158, 227, 0.04);
|
||||
--color-brand-alpha-88: rgba(80, 158, 227, 0.88);
|
||||
--color-focus: #cbe2f7;
|
||||
--mb-default-font-family: "Lato";
|
||||
--mb-color-brand: #509ee3;
|
||||
--mb-color-brand-alpha-04: rgba(80, 158, 227, 0.04);
|
||||
--mb-color-brand-alpha-88: rgba(80, 158, 227, 0.88);
|
||||
--mb-color-focus: #cbe2f7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
(update override :file (partial str u/project-root-directory))))
|
||||
;; doesn't find the usage in fingerprinters, which is a macro emitting a defmethod. The quoting changes the
|
||||
;; shape of the seq so the spec doesn't match it
|
||||
[{:file "/src/metabase/sync/analyze/fingerprint/fingerprinters.clj"
|
||||
[{:file "/src/metabase/analyze/fingerprint/fingerprinters.clj"
|
||||
:message "Error generating fingerprint for {0}"}]))
|
||||
|
||||
(defn- strip-roots
|
||||
|
||||
76
bin/embedding-sdk/fixup-types-after-compilation.js
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-env node */
|
||||
/* eslint-disable import/no-commonjs, import/order, no-console */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const SDK_DIST_DIR_PATH = path.resolve("./resources/embedding-sdk/dist");
|
||||
const SDK_PACKAGE_NAME = "@metabase/embedding-sdk-react";
|
||||
|
||||
/*
|
||||
* This script replaces all custom aliases in Embedding SDK generated ".d.ts" files so that this imports could be resolved
|
||||
* in the host app
|
||||
* */
|
||||
|
||||
// this map should be synced with "tsconfig.sdk.json"
|
||||
const REPLACES_MAP = {
|
||||
"metabase-enterprise/": `${SDK_PACKAGE_NAME}/dist/enterprise/frontend/src/metabase-enterprise/`,
|
||||
"metabase-lib/": `${SDK_PACKAGE_NAME}/dist/frontend/src/metabase-lib/`,
|
||||
"metabase-lib": `${SDK_PACKAGE_NAME}/dist/frontend/src/metabase-lib`,
|
||||
"metabase-shared/": `${SDK_PACKAGE_NAME}/dist/frontend/src/metabase-shared/`,
|
||||
"metabase-types/": `${SDK_PACKAGE_NAME}/dist/frontend/src/metabase-types/`,
|
||||
|
||||
"metabase/": `${SDK_PACKAGE_NAME}/dist/frontend/src/metabase/`,
|
||||
|
||||
"cljs/": `${SDK_PACKAGE_NAME}/dist/target/cljs_release/`,
|
||||
};
|
||||
|
||||
const traverseFilesTree = dir => {
|
||||
try {
|
||||
const results = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach(file => {
|
||||
file = path.join(dir, file);
|
||||
const stat = fs.statSync(file);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results.push(...traverseFilesTree(file));
|
||||
} else if (file.endsWith(".d.ts")) {
|
||||
// Is a ".d.ts" file
|
||||
results.push(file);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(`Error when walking dir ${dir}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const replaceAliasedImports = filePath => {
|
||||
let fileContent = fs.readFileSync(filePath, { encoding: "utf8" });
|
||||
|
||||
Object.entries(REPLACES_MAP).forEach(([alias, replacement]) => {
|
||||
fileContent = fileContent
|
||||
.replaceAll(`from "${alias}`, `from "${replacement}`)
|
||||
.replaceAll(`import("${alias}`, `import("${replacement}`)
|
||||
.replace(
|
||||
// replace dynamic imports using alias, with possible relative paths - "../../" and "frontend/src/"
|
||||
// import("(../)*(frontend/src/)*<alias>
|
||||
new RegExp(
|
||||
`import\\("(\\.\\.\/)*(frontend\/src\/)*${alias.replace("/", "\\/")}`,
|
||||
"gi",
|
||||
),
|
||||
`import("${replacement}`,
|
||||
);
|
||||
});
|
||||
|
||||
fs.writeFileSync(filePath, fileContent, { encoding: "utf-8" });
|
||||
console.log(`Edited file: ${filePath}`);
|
||||
};
|
||||
|
||||
const fixupTypesAfterCompilation = () => {
|
||||
const dtsFilePaths = traverseFilesTree(SDK_DIST_DIR_PATH);
|
||||
|
||||
dtsFilePaths.forEach(replaceAliasedImports);
|
||||
};
|
||||
|
||||
fixupTypesAfterCompilation();
|
||||
73
bin/embedding-sdk/generate-sdk-package-json.js
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-env node */
|
||||
/* eslint-disable import/no-commonjs, import/order, no-console */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const IGNORED_PACKAGES = ["react", "react-dom"];
|
||||
|
||||
function filterOutReactDependencies(object) {
|
||||
const result = {};
|
||||
|
||||
Object.entries(object).forEach(([packageName, version]) => {
|
||||
if (!IGNORED_PACKAGES.includes(packageName)) {
|
||||
result[packageName] = version;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateSdkPackage() {
|
||||
let maybeCommitHash = process.argv[2];
|
||||
|
||||
if (maybeCommitHash) {
|
||||
// get short commit hash
|
||||
maybeCommitHash = maybeCommitHash.slice(0, 7);
|
||||
}
|
||||
|
||||
const mainPackageJson = fs.readFileSync(
|
||||
path.resolve("./package.json"),
|
||||
"utf-8",
|
||||
);
|
||||
const mainPackageJsonContent = JSON.parse(mainPackageJson);
|
||||
|
||||
const sdkPackageTemplateJson = fs.readFileSync(
|
||||
path.resolve(
|
||||
"./enterprise/frontend/src/embedding-sdk/package.template.json",
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const sdkPackageTemplateJsonContent = JSON.parse(sdkPackageTemplateJson);
|
||||
|
||||
const todayDate = new Date().toJSON().slice(0, 10).replaceAll("-", "");
|
||||
|
||||
const mergedContent = {
|
||||
...sdkPackageTemplateJsonContent,
|
||||
dependencies: filterOutReactDependencies(
|
||||
mainPackageJsonContent.dependencies,
|
||||
),
|
||||
resolutions: filterOutReactDependencies(mainPackageJsonContent.resolutions),
|
||||
version: maybeCommitHash
|
||||
? `${sdkPackageTemplateJsonContent.version}-${todayDate}-${maybeCommitHash}`
|
||||
: sdkPackageTemplateJsonContent.version,
|
||||
};
|
||||
|
||||
const mergedContentString = JSON.stringify(mergedContent, null, 2);
|
||||
|
||||
console.log("Generated SDK package.json:");
|
||||
console.log(mergedContentString);
|
||||
|
||||
const sdkDistDirPath = path.resolve("./resources/embedding-sdk");
|
||||
if (!fs.existsSync(sdkDistDirPath)) {
|
||||
fs.mkdirSync(sdkDistDirPath);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(path.join(sdkDistDirPath), "package.json"),
|
||||
mergedContentString,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
generateSdkPackage();
|
||||
19
deps.edn
@@ -37,7 +37,7 @@
|
||||
org.slf4j/slf4j-api]}
|
||||
com.draines/postal {:mvn/version "2.0.5"} ; SMTP library
|
||||
com.github.seancorfield/honeysql {:mvn/version "2.6.1126"} ; Honey SQL 2. SQL generation from Clojure data maps
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925" } ; Talk to JDBC DBs
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925"} ; Talk to JDBC DBs
|
||||
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.6"} ; Telemetry library
|
||||
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.4"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
|
||||
com.google.guava/guava {:mvn/version "33.1.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
|
||||
@@ -76,13 +76,12 @@
|
||||
io.github.eerohele/pp {:git/tag "2024-01-04.60" ; super fast pretty-printing library
|
||||
:git/sha "a428751"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
io.github.metabase/macaw {:mvn/version "0.1.23"} ; Parse native SQL queries
|
||||
io.github.metabase/macaw {:mvn/version "0.1.26"} ; Parse native SQL queries
|
||||
;; The 2.X line of Resilience4j requires Java 17, so we cannot upgrade this dependency until that is our minimum JVM version
|
||||
io.github.resilience4j/resilience4j-retry {:mvn/version "1.7.1" #_ "must be 1.7.1"} ; Support for retrying operations
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} ; prometheus jvm collector
|
||||
io.prometheus/simpleclient_jetty {:mvn/version "0.16.0"} ; prometheus jetty collector
|
||||
javax.servlet/servlet-api {:mvn/version "2.5"} ; used by ring's multipart-params (file upload)
|
||||
joda-time/joda-time {:mvn/version "2.12.5"}
|
||||
kixi/stats {:mvn/version "0.5.5" ; Various statistic measures implemented as transducers
|
||||
:exclusions [org.clojure/data.avl]}
|
||||
lambdaisland/uri {:mvn/version "1.19.155"} ; Used by openai-clojure
|
||||
@@ -102,8 +101,7 @@
|
||||
:exclusions [lambdaisland/uri]} ; OpenAI
|
||||
net.i2p.crypto/eddsa {:mvn/version "0.3.0"} ; ED25519 key support (optional dependency for org.apache.sshd/sshd-core)
|
||||
net.redhogs.cronparser/cron-parser-core {:mvn/version "3.5" ; describe Cron schedule in human-readable language
|
||||
:exclusions [joda-time/joda-time ; exclude joda time 2.3 which has outdated timezone information
|
||||
org.slf4j/slf4j-api]}
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
net.sf.cssbox/cssbox {:mvn/version "5.0.1" ; HTML / CSS rendering
|
||||
:exclusions [org.slf4j/slf4j-api
|
||||
junit/junit]}
|
||||
@@ -127,8 +125,8 @@
|
||||
:exclusions [org.slf4j/slf4j-api
|
||||
org.slf4j/jcl-over-slf4j]}
|
||||
org.apache.xmlgraphics/batik-all {:mvn/version "1.17"} ; SVG -> image
|
||||
org.bouncycastle/bcpkix-jdk18on {:mvn/version "1.77"} ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors
|
||||
org.bouncycastle/bcprov-jdk18on {:mvn/version "1.77"}
|
||||
org.bouncycastle/bcpkix-jdk18on {:mvn/version "1.78"} ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors
|
||||
org.bouncycastle/bcprov-jdk18on {:mvn/version "1.78"}
|
||||
org.clj-commons/hickory {:mvn/version "0.7.4" ; Parse HTML into Clojure data structures
|
||||
:exclusions [org.jsoup/jsoup]}
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} ; LDAP client
|
||||
@@ -309,7 +307,8 @@
|
||||
|
||||
;; alias for CI-specific options.
|
||||
:ci
|
||||
{:jvm-opts ["-Xmx2g"
|
||||
{:jvm-opts ["-Xmx12g"
|
||||
"-Xms12g"
|
||||
;; normally CircleCI sets `CI` as an env var, so this is mostly to replicate that locally. Hawk will not
|
||||
;; print the progress bar in output when this is set. Progress bars aren't very CI friendly
|
||||
"-Dci=TRUE"]}
|
||||
@@ -530,9 +529,7 @@
|
||||
["metabase\\.driver\\.mysql.*"
|
||||
"metabase\\.driver\\.postgres.*"]}
|
||||
;; different port from `:test` so you can run it at the same time as `:test`.
|
||||
:jvm-opts ["-Dmb.jetty.port=3002"
|
||||
;; default heap size is 1GB, we were getting OOM on CI so we need to bump this
|
||||
"-Xmx4g"]}
|
||||
:jvm-opts ["-Dmb.jetty.port=3002"]}
|
||||
|
||||
;; clj -T:cljfmt fix '{:paths ["src/metabase/events/view_log.clj"]}'
|
||||
:cljfmt
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
;; - [Getting started with backend development](https://github.com/metabase/metabase/blob/master/docs/developers-guide/devenv.md#backend-development)
|
||||
;; - [Additional notes on using tools.deps](https://github.com/metabase/metabase/wiki/Migrating-from-Leiningen-to-tools.deps)
|
||||
;; - [Use the dev-scripts repo to run various local DBs](https://github.com/metabase/dev-scripts)
|
||||
;; - [If you're on mac and need a VM to run Windows, Linux.. etc checkout UTM](https://mac.getutm.app/)
|
||||
;; - [Other tips](https://github.com/metabase/metabase/wiki/Metabase-Backend-Dev-Secrets)
|
||||
;; - If you're on a Mac and need a VM to run Windows or Linux, [check out UTM](https://mac.getutm.app/)
|
||||
;;
|
||||
;; ## Important Parts of the Codebase
|
||||
;;
|
||||
@@ -31,6 +30,20 @@
|
||||
;; - [Liquibase](https://docs.liquibase.com/concepts/changelogs/changeset.html) for database migrations
|
||||
;; - [Compojure](https://github.com/weavejester/compojure) on top of [Ring](https://github.com/ring-clojure/ring) for our API
|
||||
;;
|
||||
;; ## Other Helpful Things
|
||||
;;
|
||||
;; [Tips on our Github wiki](https://github.com/metabase/metabase/wiki/Metabase-Backend-Dev-Secrets)
|
||||
;;
|
||||
;; ### The Dev Debug Page
|
||||
;; If you want an easy way to GET/POST to an endpoint and display the results in a webpage, check out the [Dev Debug
|
||||
;; Page](https://github.com/metabase/metabase/pull/40580). Cherry-pick the commit from that PR, modify `DevDebug.jsx` as
|
||||
;; you see fit ([here](https://github.com/metabase/metabase/commit/4c5723f44424dca2a68a753b83e31ec8129da0fb) is an
|
||||
;; example from the ParseSQL project), and then play with the results at `/dev_debug`. *Don't forget to remove the
|
||||
;; commit before merging to `master`!*
|
||||
;;
|
||||
;; ### Lifecycle of a Query
|
||||
;; Dan wrote a nice guide [here](https://www.notion.so/metabase/Lifecycle-of-a-query-58e212402b7e444d937aba7757f9ec06?pvs=4)
|
||||
;;
|
||||
;; <hr />
|
||||
|
||||
|
||||
@@ -251,8 +264,7 @@
|
||||
([]
|
||||
(migrate! :up))
|
||||
([direction & [version]]
|
||||
(mdb/migrate! (mdb/db-type) (mdb/data-source)
|
||||
direction version)))
|
||||
(mdb/migrate! (mdb/data-source) direction version)))
|
||||
|
||||
(methodical/defmethod t2.connection/do-with-connection :model/Database
|
||||
"Support running arbitrary queries against data warehouse DBs for easy REPL debugging. Only works for SQL+JDBC drivers
|
||||
|
||||
@@ -53,6 +53,7 @@ Metabase's reference documentation.
|
||||
- [Commands](./installation-and-operation/commands.md)
|
||||
- [Supported browsers](./installation-and-operation/supported-browsers.md)
|
||||
- [Privacy](./installation-and-operation/privacy.md)
|
||||
- [About the anonymous usage data we collect](./installation-and-operation/information-collection.md)
|
||||
|
||||
### Databases
|
||||
|
||||
@@ -90,6 +91,7 @@ Metabase's reference documentation.
|
||||
- [Sharing answers](./questions/sharing/answers.md)
|
||||
- [Alerts](./questions/sharing/alerts.md)
|
||||
- [Public sharing](./questions/sharing/public-links.md)
|
||||
- [Exporting data](./questions/sharing/exporting-results.md)
|
||||
|
||||
### Dashboards
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ Metabase gives you the ability to automatically cache question results that meet
|
||||
|
||||
If your questions share a common model, you can enable [model caching](../data-modeling/models.md#model-caching) instead.
|
||||
|
||||
## Caching doesn't work with data sandboxing
|
||||
|
||||
Just something to keep in mind: if someone in a [sandboxed](../permissions/data-sandboxes.md) group views a table, Metabase will skip the cached results and query the table directly, returning only the results the person is allowed to see.
|
||||
|
||||
## Enabling global caching
|
||||
|
||||
1. Go to **Admin settings** > **Caching** (in the sidebar).
|
||||
|
||||
@@ -165,7 +165,7 @@ Sections include:
|
||||
|
||||
### Dashboard width
|
||||
|
||||
You change the width of a dashboard.
|
||||
You can change the width of a dashboard.
|
||||
|
||||

|
||||
|
||||
@@ -236,9 +236,9 @@ If your Metabase administrator has enabled [public sharing](../questions/sharing
|
||||
|
||||
Public links can be viewed by anyone, even if they don't have access to Metabase. You can also use the public embedding code to embed your question or dashboard in a simple web page or blog post. Check out examples of simple apps with embedded dashboards in our [embedding-reference-apps repository](https://github.com/metabase/embedding-reference-apps). To learn more about [embedding](../embedding/introduction.md), check out our article on [How to use Metabase to deliver analytics to your customers](https://www.metabase.com/blog/external-facing-analytics), as well as an article on how to combine branding, Single Sign-On, full app embedding, and data sandboxing to deliver [multi-tenant, self-service analytics](https://www.metabase.com/blog/embedding).
|
||||
|
||||
## Exporting dashboards as PDFs
|
||||
## Exporting results from a dashboard
|
||||
|
||||
To export a dashboard as a PDF, wait until the dashboard is finished loading, then click on the three dot menu (**...**) in the upper right and select **Export as PDF**.
|
||||
See [Exporting results](../questions/sharing/exporting-results.md#exporting-results-of-a-dashboard).
|
||||
|
||||
## Configuring a dashboard through its URL
|
||||
|
||||
|
||||
@@ -29,14 +29,57 @@ Let's say we want to email a dashboard. We'll click on the **Email it** option i
|
||||
|
||||
For emails, we can:
|
||||
|
||||
- **Add subscribers**. Add email addresses to register subscribers. On Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise), admins can limit email recipients to [approved domains for notifications](../configuring-metabase/email.md#approved-domains-for-notifications) and [configure which recipients Metabase suggests](../configuring-metabase/email.md#suggest-recipients-on-dashboard-subscriptions-and-alerts).
|
||||
- **Determine frequency and timing**. Tell Metabase how often it should send the dashboard (daily, weekly, or monthly), and what time of day to send the dashboard.
|
||||
- **Send email now** sends an email to all subscribers.
|
||||
- **Skip updates without results**. If there are no results, we can tell Metabase to skip sending the email.
|
||||
- **Attach results**. Tell Metabase if it should attach results to the email as a file, in addition to displaying the table in the email body. You can choose between CSV and XLSX file formats. The attached file will include up to 2000 rows by default. If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [MB_UNAGGREGATED_QUERY_ROW_LIMIT](../configuring-metabase/environment-variables.md#mb_unaggregated_query_row_limit).
|
||||
### Add subscribers
|
||||
|
||||
Add email addresses to register subscribers. On Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise), admins can limit email recipients to [approved domains for notifications](../configuring-metabase/email.md#approved-domains-for-notifications) and [configure which recipients Metabase suggests](../configuring-metabase/email.md#suggest-recipients-on-dashboard-subscriptions-and-alerts).
|
||||
|
||||
### Determine frequency and timing
|
||||
|
||||
Tell Metabase how often it should send the dashboard:
|
||||
|
||||
- Hourly
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
|
||||
And what time of day to send the dashboard.
|
||||
|
||||
### Send email now
|
||||
|
||||
A button that sends an email to all subscribers each time you touch it.
|
||||
|
||||
### Don't send if there aren't results
|
||||
|
||||
If there are no results, you can tell Metabase to skip sending the email.
|
||||
|
||||
### Set filter values for when this gets sent
|
||||
|
||||
Here you can set filters to apply when Metabase sends the subscription.
|
||||
|
||||
### Attach results
|
||||
|
||||
Tell Metabase if it should attach results to the email as a file, in addition to displaying the table in the email body.
|
||||
|
||||
If you've added filters to your dashboard and set default values for those filters, Metabase will apply those default values to your subscriptions, filtering the results of all questions that are connected to those filters when the subscriptions are sent. To learn more, check out [dashboard filters](./filters.md).
|
||||
|
||||
#### File format
|
||||
|
||||
You can choose between the following file formats:
|
||||
|
||||
- .csv
|
||||
- .xlsx
|
||||
- .json
|
||||
|
||||
#### Use unformatted values in attachments
|
||||
|
||||
If you check this box, Metabase will drop any visualization settings applied to the questions' results (e.g., date formatting) and send only the raw, unformatted results.
|
||||
|
||||
#### Questions to attach
|
||||
|
||||
Here you can specify which questions Metabase should attach results for.
|
||||
|
||||
The attached files will include up to 2000 rows by default. If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [MB_UNAGGREGATED_QUERY_ROW_LIMIT](../configuring-metabase/environment-variables.md#mb_unaggregated_query_row_limit).
|
||||
|
||||
## Slack subscription options
|
||||
|
||||
For Slack subscriptions, you can set up a subscription for a channel (like #general), or for a single person via their Slack username.
|
||||
|
||||
@@ -4,7 +4,7 @@ redirect_from:
|
||||
- /docs/latest/administration-guide/19-formatting-settings
|
||||
---
|
||||
|
||||
# Formatting defaults
|
||||
# Formatting defaults
|
||||
|
||||
There are Metabase users around the world, each with different preferences for how dates, times, numbers, and currencies should be formatted and displayed. Metabase allows you to customize these formatting options at three different levels:
|
||||
|
||||
@@ -38,3 +38,20 @@ Currency field formatting settings include all the same options as in the global
|
||||
## Question-level formatting
|
||||
|
||||
Lastly, you can override all formatting settings in any specific saved question or dashboard card by clicking on the gear to open up the visualization options. To reset any overridden setting to the default, just click on the rotating arrow icon next to the setting's label. This will reset the setting to the field-level setting if there is one; otherwise it will be reset to the global default.
|
||||
|
||||
Formatting options vary depending on the type of visualization:
|
||||
|
||||
- [Combo chart](../questions/sharing/visualizations/combo-chart.md)
|
||||
- [Detail](../questions/sharing/visualizations/detail.md)
|
||||
- [Funnel](../questions/sharing/visualizations/funnel.md)
|
||||
- [Gauge](../questions/sharing/visualizations/gauge.md)
|
||||
- [Line, Bar, and area charts](../questions/sharing/visualizations/line-bar-and-area-charts.md)
|
||||
- [Maps](../questions/sharing/visualizations/map.md)
|
||||
- [Numbers](../questions/sharing/visualizations/numbers.md)
|
||||
- [Pie or donut chart](../questions/sharing/visualizations/pie-or-donut-chart.md)
|
||||
- [Pivot table](../questions/sharing/visualizations/pivot-table.md)
|
||||
- [Progress bar](../questions/sharing/visualizations/progress-bar.md)
|
||||
- [Scatter plot or bubble chart](../questions/sharing/visualizations/scatterplot-or-bubble-chart.md)
|
||||
- [Tables](../questions/sharing/visualizations/table.md)
|
||||
- [Trend](../questions/sharing/visualizations/trend.md)
|
||||
- [Waterfall chart](../questions/sharing/visualizations/waterfall-chart.md)
|
||||
@@ -182,7 +182,7 @@ To set up model caching:
|
||||
1. [Enable model caching in Metabase](#enable-model-caching-in-metabase).
|
||||
2. [Create a schema to store cached models](#create-a-schema-to-store-cached-models).
|
||||
|
||||
> Model caching doesn't work with [data sandboxing](../configuring-metabase/caching.md#caching-doesnt-work-with-data-sandboxing).
|
||||
> Model caching doesn't work with [data sandboxing](../permissions/data-sandboxes.md).
|
||||
|
||||
### Enable model caching in Metabase
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Uploading CSV data is best suited for ad hoc analysis of spreadsheet data. If yo
|
||||
|
||||
- [PostgreSQL](../databases/connections/postgresql.md)
|
||||
- [MySQL](../databases/connections/mysql.md)
|
||||
- [Redshift](../databases/connections/redshift.md)
|
||||
|
||||
## Enabling uploads
|
||||
|
||||
|
||||
@@ -13,6 +13,47 @@ title: Driver interface changelog
|
||||
efficient way possible. This is currently only required for drivers that support the `:uploads` feature, and has
|
||||
a default implementation for JDBC-based drivers.
|
||||
|
||||
- New feature `:window-functions` has been added. Drivers that implement this method are expected to implement the
|
||||
cumulative sum (`:cum-sum`) and cumulative count (`:cum-count`) aggregation clauses in their native query language.
|
||||
For non-SQL drivers (drivers not based on our `:sql` or `:sql-jdbc` drivers), this feature flag is set to `false` by
|
||||
default; the old (broken) post-processing implementations of cumulative aggregations will continue to be used. (See
|
||||
issues [#13634](https://github.com/metabase/metabase/issues/13634) and
|
||||
[#15118](https://github.com/metabase/metabase/issues/15118) for more information on why the old implementation is
|
||||
broken.)
|
||||
|
||||
Non-SQL drivers should be updated to implement cumulative aggregations natively if possible.
|
||||
|
||||
The SQL implementation uses `OVER (...)` expressions. It will automatically move `GROUP BY` expressions like
|
||||
`date_trunc()` into a `SUBSELECT` so fussy databases like BigQuery can reference plain column identifiers. The
|
||||
actual SQL generated will look something like
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
created_at_month,
|
||||
sum(sum(total) OVER (ORDER BY created_at_month ROWS UNBOUNDED PRECEDING) AS cumulative_sum
|
||||
FROM (
|
||||
SELECT
|
||||
date_trunc('month', created_at) AS created_at_month,
|
||||
total
|
||||
FROM
|
||||
my_table
|
||||
) source
|
||||
GROUP BY
|
||||
created_at_month
|
||||
ORDER BY
|
||||
created_at_month
|
||||
```
|
||||
|
||||
Non-SQL drivers can use
|
||||
`metabase.query-processor.util.transformations.nest-breakouts/nest-breakouts-in-stages-with-cumulative-aggregation`
|
||||
if they want to leverage the same query transformation. See the default `:sql` implementation of
|
||||
`metabase.driver.sql.query-processor/preprocess` for an example of using this transformation when needed.
|
||||
|
||||
You can run the new tests in `metabase.query-processor-test.cumulative-aggregation-test` to verify that your driver
|
||||
implementation is working correctly.
|
||||
|
||||
- `metabase.driver.common/class->base-type` no longer supports Joda Time classes. They have been deprecated since 2019.
|
||||
|
||||
## Metabase 0.49.1
|
||||
|
||||
- Another driver feature has been added: `describe-fields`. If a driver opts-in to supporting this feature, The
|
||||
|
||||
@@ -476,9 +476,9 @@ describe("CollectionHeader", () => {
|
||||
collection,
|
||||
});
|
||||
|
||||
userEvent.clear(screen.getByDisplayValue("Old name"));
|
||||
userEvent.type(screen.getByPlaceholderText("Add title"), "New title");
|
||||
userEvent.tab();
|
||||
await userEvent.clear(screen.getByDisplayValue("Old name"));
|
||||
await userEvent.type(screen.getByPlaceholderText("Add title"), "New title");
|
||||
await userEvent.tab();
|
||||
|
||||
expect(onUpdateCollection).toHaveBeenCalledWith({
|
||||
...collection,
|
||||
|
||||
24
docs/developers-guide/mbql-library-changelog.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: MBQL Library changelog
|
||||
---
|
||||
|
||||
# MBQL Library Changelog
|
||||
|
||||
Changes made to the library API for manipulating MBQL queries, found in `metabase.lib.js`. The latest API documentation
|
||||
can be found [here](https://metabase-dev-docs.github.io/metabase/metabase.lib.js.html).
|
||||
|
||||
This library is mainly used by Metabase's own frontend, but it is treated as a proper API surface which is versioned
|
||||
and documented in this changelog.
|
||||
|
||||
## Metabase 0.50.0
|
||||
|
||||
- Created this file and began versioning this API.
|
||||
- New function `as-returned` has been added. It handles a tricky case when adding filters or expressions to a query
|
||||
with aggregations.
|
||||
|
||||
Suppose we have a query with aggregations in its last stage. When adding a filter or expression to that stage, it's
|
||||
applied **before** the aggregations. That may be the desired behavior, but if we want a filter or custom expression
|
||||
based on the aggregations and breakouts in the last stage, there was no good support in this API.
|
||||
|
||||
`as-returned` looks at the query and stage, and shifts to a later stage if necessary. If a later stage is needed but
|
||||
we were already on the last stage, a new empty stage is appended.
|
||||
@@ -35,6 +35,7 @@ This guide contains detailed information on how to work on Metabase codebase.
|
||||
- [Autogenerated API docs](../api-documentation.md)
|
||||
- [Working with the Metabase API](https://www.metabase.com/learn/administration/metabase-api.html)
|
||||
- [Driver Interface Changelog](driver-changelog.md)
|
||||
- [MBQL Library Changelog](mbql-library-changelog.md)
|
||||
|
||||
## Database drivers
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ You'll embed the full Metabase application in your app. Once logged in, people c
|
||||
|
||||
## [Static embedding](./static-embedding.md)
|
||||
|
||||
Also known as Signed Embedding, Static embedding offers drill-through with custom destinations, so you can define what happens when people click on a chart, like sending people to another chart or URL–all while securing the underlying data.
|
||||
Also known as Signed Embedding, Static embedding is a secure way to embed charts and dashboards. Static embeds are view only; people won't be able to drill-through charts and tables.
|
||||
|
||||
## [Parameters for signed embeds](./static-embedding-parameters.md)
|
||||
## [Parameters for static embeds](./static-embedding-parameters.md)
|
||||
|
||||
You can pass parameters between Metabase and your website via the embedding URL to specify how Metabase items should look and behave inside the iframe on your website.
|
||||
|
||||
|
||||
@@ -12,19 +12,26 @@ Events and timelines are a way to capture that chronological knowledge and make
|
||||
|
||||
## Events
|
||||
|
||||

|
||||
|
||||
An event is basically a date + a title + a description + an icon. You can add events to Metabase to show important milestones, launches, or anything else, right alongside your data.
|
||||
|
||||
Metabase will only display events on time series charts when viewing an individual question. Metabase won't display events on charts in dashboard cards.
|
||||
|
||||
## Timelines
|
||||
|
||||
You can group events into timelines, and associate those timelines with [collections](collections.md). For example, you may want to have a timeline that contains important email or sales dates, or an outages timeline that tracks downtime. You can move events between timelines, and move timelines from collection to collection.
|
||||
Timelines are groups of events associated with a [collection](collections.md).
|
||||
|
||||
Collections can have timelines, and timelines can contain events. In practice what this means is that events you've added to a timeline will show up on time series questions stored in the same collection as that timeline. Events are invisible on dashboards
|
||||

|
||||
|
||||
## Event and timeline permissions
|
||||
For example, you may want to have a timeline that contains important email or sales dates, or an outages timeline that tracks downtime. You can move events between timelines, and move timelines from collection to collection.
|
||||
|
||||
Anyone with [view access](../permissions/collections.md) to a collection can view the collection's events and timelines.
|
||||
Collections can have timelines, and timelines can contain events. In practice what this means is that events you've added to a timeline will show up on time series questions stored in the same collection as that timeline.
|
||||
|
||||
- If you don't explicitly create a timeline yet, but you do create events, Metabase will automatically create a timeline for you (which acts as the default timeline for the collection).
|
||||
- You can have multiple timelines for the same collection.
|
||||
- Timelines associated with collections do not apply to that collection's sub-collections.
|
||||
|
||||
Anyone with curate access to a collection can add events and timelines to that collection.
|
||||
|
||||
### Adding events when viewing a collection
|
||||
|
||||
@@ -34,14 +41,14 @@ When viewing a [collection](collections.md), you can view, add, or edit events b
|
||||
|
||||
Once you create an event, the event will show up in charts in the collection, provided:
|
||||
|
||||
- the date of the event falls within the chart's time range, and
|
||||
- the timeline is visible (more on that [below](#adding-events-when-viewing-a-question))
|
||||
- The date of the event falls within the chart's time range.
|
||||
- The timeline is visible (more on that [below](#adding-events-when-viewing-a-question)).
|
||||
|
||||
You'll see an icon along the x-axis that plots the event. A vertical line will extend from the event to show when the data plotted on the chart intersects with the event.
|
||||
|
||||

|
||||
|
||||
### Adding events when viewing a question
|
||||
## Adding events when viewing a question
|
||||
|
||||
If your question is a time series, you can click on the **Calendar** in the bottom right of the question, and Metabase will open the timeline sidebar. Metabase will list any timelines and their events that fall in the range of your time series. You can:
|
||||
|
||||
@@ -49,24 +56,73 @@ If your question is a time series, you can click on the **Calendar** in the bott
|
||||
- Add a new event (even if you haven't saved the question yet).
|
||||
- Edit, move, or archive an event.
|
||||
|
||||
## Archiving and deleting
|
||||
## Viewing events and timelines on a chart from a different collection
|
||||
|
||||
### Archiving events
|
||||
If you're viewing a question with a time series chart from a _different_ collection, you can temporarily apply a timeline to the chart by clicking on the **calendar** icon in the bottom right of the question and selecting the timeline and events you want to display.
|
||||
|
||||
You can archive both events and timelines to get them out of the way, or unarchive them to resurrect them.
|
||||
These selections are temporary; if you reload the question, Metabase will drop the events from the chart.
|
||||
|
||||
To archive an event, click on the **...** next to the event and select **Archive event**.
|
||||
### To hide the timeline and its events on a chart
|
||||
|
||||
To view archived events, navigate to the relevant collection and click on the **Calendar** icon, then the **...** menu to **View archived events**.
|
||||
To temporarily hide the events from a chart:
|
||||
|
||||
### Archiving timelines
|
||||
1. Click on the **calendar** icon in the bottom right.
|
||||
2. Uncheck the timeline or event.
|
||||
|
||||
To archive a timeline, click on the **...** next to the timeline and select **Edit timeline details**. Metabase will pop up an edit modal; click on **Archive timeline and all events**.
|
||||
Your selections will reset on reload.
|
||||
|
||||
To resurrect an archived timeline, click on the **Calendar** icon in the relevant collection, then click on the **...** menu. Next to the timeline you want to unarchive, click on the **...** menu and select **Unarchive timeline**.
|
||||
To permanently hide the timeline and its events, you'll need to [archive the timeline](#archiving-timelines). Alternatively, you could move the question to a different collection.
|
||||
|
||||
To permanently delete an event or timeline, you must first archive the event or timeline. Then you can delete the archived events from the **View archived events** modal, or timelines from the **View archived timelines** modal.
|
||||
## Edit an event
|
||||
|
||||
## Disabling events for a question
|
||||

|
||||
|
||||
If a collection includes timelines, those events will show up for any time series in that collection (provided the timeline and the question's date range overlap). If you want to disable the visibility of events by default for a particular question, you'll need to move that question to a collection that lacks timelines and events.
|
||||
To edit an event:
|
||||
|
||||
1. Click the calendar icon in the top right of a collection.
|
||||
2. Go to the timeline that contains the event, click on the event's three-dot menu (**...**).
|
||||
3. Select:
|
||||
- **Edit event**: its title, description, and icon.
|
||||
- **Move event**: to another timeline.
|
||||
- **Archive event**: to hide the event from charts.
|
||||
|
||||
## Archiving timelines
|
||||
|
||||
To archive a timeline:
|
||||
|
||||
1. Go to the timeline's collection and click on the **calendar** icon in the top right.
|
||||
2. Select the timeline, then click on the three-dot menu (**...**). If the collection only has one timeline, click on the three-dot menu (**...**).
|
||||
3. Select **Edit timeline details**.
|
||||
4. Click on the red **Archive timeline and all events**.
|
||||
|
||||
### View archived events and timelines
|
||||
|
||||
> Archived events and timelines can only be viewed from the collection. They don't show up in the [main archive](../exploration-and-organization/history.md).
|
||||
|
||||
To view (and resurrect) archived timelines and events:
|
||||
|
||||
1. Click on the **Calendar** icon in the relevant collection.
|
||||
2. Click on the three-dot menu **...** menu.
|
||||
3. Select **View archived timelines**. Metabase will display archived events or timelines.
|
||||
4. If you want to unarchive an item, click on the three-dot menu **...** next to the event or timeline and select the **Unarchive** option.
|
||||
|
||||
To permanently delete an archived event or timeline, click on the three-dot menu (**...**) and select **Delete**.
|
||||
|
||||
Then you can delete the archived events from the **View archived events** modal, or timelines from the **View archived timelines** modal.
|
||||
|
||||
## Event and timeline permissions
|
||||
|
||||
Event and timeline permissions depend on your [collection permissions](../permissions/collections.md).
|
||||
|
||||
- **View access**: you can view the collection's events and timelines. You can also temporarily apply timelines and events to time series in other collections.
|
||||
- **Curate access**. Anyone with curate access to a collection can add events and timelines to that collection.
|
||||
|
||||
### Make a timeline and its events available for everyone
|
||||
|
||||
If you want the event and timeline to be available to everyone, create the timeline in a collection that the [All Users group](../people-and-groups/managing.md#all-users) has access to, as by default everyone is in the All Users group.
|
||||
|
||||
Note that for questions outside of that collection, you can only temporarily apply those timelines to charts. The timelines won't appear automatically when you reload the chart.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Keeping your Metabase organized](https://www.metabase.com/learn/administration/same-page)
|
||||
|
||||
@@ -4,7 +4,7 @@ title: History
|
||||
|
||||
# History
|
||||
|
||||
For questions, dashboards, and models, Metabase keeps a version history for the previous fifteen versions of that item. You can view changes, and revert to previous versions.
|
||||
For questions, dashboards, and models, Metabase keeps a version history for the previous fifteen versions of that item. You can view changes, and revert to previous versions.
|
||||
|
||||
You can also archive, unarchive, and permanently delete outdated items.
|
||||
|
||||
@@ -52,6 +52,8 @@ Archiving a collection archives all of the collection's contents as well. You ca
|
||||
2. Click the `...` beside the "Collections" header in the sidebar.
|
||||
3. Click **View archive**.
|
||||
|
||||
To view archived [events and timelines](./events-and-timelines.md), go to the relevant unarchived collection, click on the collection's **Calendar** icon, then the three-dot menu (**...**) and select the option to view the archived events or timelines.
|
||||
|
||||
## Unarchiving items
|
||||
|
||||
1. Open the main Metabase sidebar.
|
||||
|
||||
BIN
docs/exploration-and-organization/images/edit-an-event.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 137 KiB |
BIN
docs/exploration-and-organization/images/timeline-sidebar.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
@@ -1,104 +1,46 @@
|
||||
---
|
||||
title: About the information we collect
|
||||
title: About the anonymous usage data we collect
|
||||
redirect_from:
|
||||
- /docs/latest/information-collection
|
||||
---
|
||||
|
||||
# About the information we collect
|
||||
# About the anonymous usage data we collect
|
||||
|
||||
Metabase uses Google Analytics and Snowplow to collect anonymous usage information from the installed servers that enable this feature. Below is a representative list of the events we have instrumented, as well as the information we collect about the user performing the action and the instance being used.
|
||||
If you're self-hosting Metabase and you've opted in to provide us with anonymous usage data (thank you!), Metabase will phone home some data collected via Google Analytics and Snowplow.
|
||||
|
||||
We also will phone home some anonymized metrics from the metabase application server on a nightly basis. Note that we don't collect any usernames, any emails, the server IP, database details of any kind or any personally identifiable information in this process.
|
||||
**[We only collect anonymous Metabase data; we don't collect any of your data](https://www.metabase.com/security).** We don't collect any usernames, emails, server IPs, database details of any kind, or any personally identifiable information (PII).
|
||||
|
||||
While this list of anonymous information we collect might seem long, it’s useful to compare this to other alternatives. With a typical SaaS platform, not only will this information be collected, but it will also be accompanied by information about your data, how often it is accessed, the specific queries that you use, specific numbers of records all tied to your company and current plan.
|
||||
This anonymous data helps us understand how people are actually using Metabase, which in turn helps us prioritize what to work on next.
|
||||
|
||||
We collect this information to improve your experience and the quality of Metabase. We use the information you voluntarily share to understand how our users are actually using our product, what kinds of features to prioritize, and how many items to target in our design process. For example, without knowing what the distribution of the number of accounts on each instance in our install base is, we can't know whether the tools we provide can't scale until someone complains. And even then, we only hear complaints and not the people who are quietly happily using us. We're striving to create the best product possible.
|
||||
## Examples of the anonymous data we collect and how we use it
|
||||
|
||||
If you prefer not to provide us with this anonymous usage data, you can always go to your instance’s admin section and toggle off the option for `Anonymous Tracking`.
|
||||
In a nutshell: we track some basic events to find opportunities for improving the product and evaluating project outcomes. We _don't_ use the data for sales or advertising (and the data's anonymous, so we couldn't use it for that anyway).
|
||||
|
||||
If you're in the process of setting up your Metabase, an admin can also toggle off tracking during the `Usage Data Preferences` onboarding step. We collect a few anonymous events before that point, but won't do so anymore if you choose to opt out.
|
||||
We collect things like:
|
||||
|
||||
In the list below, we spell out exactly why we collect each bit of information.
|
||||
- Events in the product (for example, when people create models)
|
||||
- Some performance data
|
||||
- The number and types of DBs you've connected to your Metabase
|
||||
- The number of questions and dashboards and other items you've created
|
||||
|
||||
## Example questions we want to answer
|
||||
This anonymous data helps us figure out things like:
|
||||
|
||||
- Is our query interface working?
|
||||
- Are users stopping halfway through a question?
|
||||
- Are users using filters?
|
||||
- Are users using groupings?
|
||||
- How often are users using bare rows vs other aggregation options?
|
||||
- Are people clicking on column headings to sort or manually adding a sort clause?
|
||||
- How often are users writing SQL instead of using the query interface?
|
||||
- Are these queries written by a select group of analysts or is the entire company SQL literate?
|
||||
- Are people using dashboards as a starting point for queries?
|
||||
- How many clicks are there on dashboard cards?
|
||||
- How many of these clicks result in modified queries that are executed?
|
||||
- How often are questions saved?
|
||||
- How often are saved questions added to dashboards?
|
||||
- Which features you're using
|
||||
- Where people get stuck
|
||||
- How performance for key workflows (like querying or loading) changes over time
|
||||
|
||||
## What we will do with the answers to these questions
|
||||
## Opting out of anonymous usage data collection for self-hosted Metabases
|
||||
|
||||
- Prioritize improvements in the query interface vs the SQL interface
|
||||
- Optimize the product for common usage patterns
|
||||
- Stay on top of browser incompatibilities
|
||||
- Optimize our dashboards for either passive consumption or as a starting point for further exploration depending on how they are being used
|
||||
If you're self-hosting Metabase, you can opt out of providing us with your anonymous usage data:
|
||||
|
||||
While we will closely follow reported issues and feature requests, we aim to make as many of our users happy and provide them with improvements in features that matter to them. Allowing us to collect information about your instance gives your users a vote in future improvements in a direct way.
|
||||
1. Click on the gear icon.
|
||||
2. Select **Admin settings**.
|
||||
3. Go to the **Settings** tab.
|
||||
4. Click **General**
|
||||
5. Toggle the **Anonymous tracking** option.
|
||||
|
||||
## The data we collect
|
||||
If you're in the process of setting up your Metabase, you can also toggle off tracking during the `Usage Data Preferences` onboarding step. We collect a few anonymous events before that point, but won't do so anymore if you choose to opt out.
|
||||
|
||||
NOTE: We never capture any specific details in any of our tracking methodology such as user details, table names, field names, etc. Collected data is limited to the types of actions users are taking with the product.
|
||||
|
||||
## Google Analytics Events
|
||||
|
||||
| Category | Action | Why we collect this |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Links and Page Views | General website tracking of what pages are most used | This provides better understanding of what parts of the application are liked and used by customers so we know what's popular and potentially what needs more improvement. |
|
||||
| Dashboards | When the dashboard dropdown is used, when dashboards are created and updated, what types of edits occur such as adding/removing cards and repositioning. | We use this information to understand how dashboards are being used and what types of activities users most commonly do on their dashboards. |
|
||||
| Pulses | When pulses are created and updated, what types of pulses are created, and how many cards typically go in a pulse. | This is used to have a sense for how teams are structuring their push based communication. When and where is information most often sent and how much information allows Metabase to continue improving features around push based data interactions. |
|
||||
| Query Builder | When questions are saved and viewed along with what types of choices are made such as chart types and query clauses used. | Helps the Metabase team understand the basic patterns around how users are accessing their data. |
|
||||
| SQL Query | When a SQL query is saved or run. | This mostly just gives us a sense for when users are bypassing the GUI query interface. We never capture the actual SQL written. |
|
||||
| Admin Settings | We capture some very basic stats about when settings are updated and if there are ever errors. We also capture non-intrusive settings such as the chosen timezone. | We use this information to make sure that users aren't having problems managing their Metabase instance and it provides us some sense for the most common configuration choices so we can optimize for those cases. |
|
||||
| Databases | We simply capture when databases are created or removed and what types of databases are being used | This helps Metabase ensure that we spend the most time and attention on the types of databases that are most popular to users. |
|
||||
| Table Metadata | The saving and updates on tables, fields, segments, and metrics are all counted, along with a few other details such as what types of special metadata choices are made. | We use this data to help ensure that Metabase provides an appropriate set of options for users to describe their data and also gives us a sense for how much time users spend marking up their schemas. |
|
||||
|
||||
## Snowplow Events
|
||||
|
||||
| Category | Action | Why we collect this |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Set up | General clickthrough tracking of what steps are completed vs. abandoned | This provides us with a better understanding of how we can improve the on-ramp for new Metabase users. |
|
||||
| Database configuration | Whether your database connection attempt is successful vs. unsuccessful, which database you connect with. We don't collect any of your database credentials here. | Helps us understand which databases are most popular and performing as expected so we can invest in improving how Metabase works with them. |
|
||||
| Dashboard | When you create a new dashboard or add a new question to a dashboard. | We can use this to determine if Metabase is adding value to your organization, and therefore if we're doing our jobs correctly. |
|
||||
| Account | When a new user is created or a new instance is set up. | This powers basic reporting to help us understand how your Metabase team is growing. |
|
||||
|
||||
## Server-side Analytics
|
||||
|
||||
| Metric | An example of why we collect this |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Number of Users/Admins and whether SSO is enabled | To understand which auth methods are being used, and whether to prioritize features that scale with number of users. |
|
||||
| Number of user groups | To understand how complicated a permissions model most of our users have, and to make sure that we don't over-simplify our designs. |
|
||||
| Number of Dashboards | Whether we need to provide ways to organize dashboards. |
|
||||
| Number of Cards per Dashboards | Do we need to provide more structure to make long dashboards easier to parse? |
|
||||
| Number of Dashboards per Card | Are our users only creating a card to put it in a dashboard or are they used in many places? |
|
||||
| Types of Databases | Which database driver bugs to prioritize |
|
||||
| Number of pulses with attachments | Are people using attachments? |
|
||||
| Number of alerts | Are people using alerts? Do they typically have a few or does each user have them? |
|
||||
| Number of Collections | Do we need to add additional organization tools? |
|
||||
| Number of Databases | Are users using a single DB or many? How large should the icons for a database in the databrowser be? |
|
||||
| Number of Schema | Are users actively using namespaces on redshift? Do we actually need to design for 100s of schemas or is that just a small percentage of our users? |
|
||||
| Number of Tables | What kind of data models are people using? Do we need table search? |
|
||||
| Number of Fields | Can we pre-fetch all the fields in our metadata api to improve performance for most usres, or should we fetch them per table to scale more efficiently? |
|
||||
| Number of Segments | Are people using segments widely? If so, should we surface them up higher in the UI? |
|
||||
| Number of Metrics | Are metrics common? If not, should we remove the Metrics option in the New Question Flow |
|
||||
| Number of Queries Run | How many queries do our most active instances run per day? Do we need to improve caching? |
|
||||
| Number of Query Errors | Do we need to change how we display errors in the logs? Are they being spammed? |
|
||||
| Query Latencies | What percentage of our user base runs queries that allow for iterative querying (less than 1 second) |
|
||||
| Timezone | We have a bug in a certain timezone, how many users are in that timezone? |
|
||||
| Language | How many non-English speaking users do we have? How fast should we be pushing internationalization? |
|
||||
| OS and JVM Version | Can we deprecate Java 7 yet? |
|
||||
|
||||
Note this is meant to be representative. The actual code that is running to generate this can be audited at [https://github.com/metabase/metabase/blob/master/src/metabase/analytics/](https://github.com/metabase/metabase/blob/master/src/metabase/analytics).
|
||||
|
||||
## Data privacy
|
||||
## Further reading
|
||||
|
||||
Check out our page on [data privacy and security](https://www.metabase.com/security).
|
||||
|
||||
@@ -252,7 +252,6 @@ site-name
|
||||
application-font-files
|
||||
loading-message
|
||||
report-timezone
|
||||
show-lighthouse-illustration
|
||||
persisted-models-enabled
|
||||
enable-content-management?
|
||||
subscription-allowed-domains
|
||||
|
||||
@@ -42,6 +42,10 @@ Export Metabase metrics for viewing with Prometheus.
|
||||
|
||||
Rest in peace, Internet Explorer.
|
||||
|
||||
## [About the anonymous usage data we collect](./information-collection.md).
|
||||
|
||||
We collect anonymous Metabase usage data to improve the product. We don't collect your data.
|
||||
|
||||
## [Command line commands](./commands.md)
|
||||
|
||||
Metabase's CLI.
|
||||
|
||||
@@ -113,6 +113,32 @@ This action will delete any dashboard subscriptions or alerts the person has cre
|
||||
|
||||
This action doesn't affect email distribution lists that are managed outside of Metabase.
|
||||
|
||||
## Default user accounts
|
||||
|
||||
Metabase includes default user accounts to handle various tasks. We're documenting these accounts here so you know they're legitimate accounts and not someone trying to spy on your Metabase. Some things to know about them:
|
||||
|
||||
- Customers are not charged for these accounts.
|
||||
- No one can log in to these user accounts.
|
||||
- Metabase excludes these user accounts from the **Admin settings** > **People** tab.
|
||||
|
||||
### Anonymous user account
|
||||
|
||||
- ID: 0
|
||||
- First name: External
|
||||
- Last name: User
|
||||
- Email: null
|
||||
|
||||
Metabase uses this anonymous user account to identify anonymous views, for example views of a [public question or dashboard](../questions/sharing/public-links.md). This account is a virtual user: the account doesn't exist in the application database. You'll see this account show up in [usage analytics](../usage-and-performance-tools/usage-analytics.md).
|
||||
|
||||
### Metabase internal account
|
||||
|
||||
- ID: 13371338
|
||||
- First name: Internal
|
||||
- Last name: Metabase
|
||||
- Email: internal@metabase.com
|
||||
|
||||
Metabase uses this account to load content into Metabase (like the [Metabase analytics](../usage-and-performance-tools/usage-analytics.md) collection). You may see this `internal@metabase.com` account in the logs.
|
||||
|
||||
## Groups
|
||||
|
||||
To determine [who has access to what](../permissions/start.md), you’ll need to
|
||||
|
||||
@@ -31,13 +31,14 @@ When the "Set up Single Sign-On with SAML" page appears, you'll see an option fo
|
||||

|
||||
|
||||
Fill out the following fields as follows and click "Save":
|
||||
|
||||
- **Identifier (Entity ID)**: `Metabase`
|
||||
- **Reply URL (Assertion Consumer Service URL)**: go to your Metabase instance in Settings -> Admin-> Authentication -> SAML and insert the value that your Metabase instance reports in the "Configure your identity provider (IdP)" box.
|
||||
|
||||
In a new tab, visit the "App Federation Metadata URL". On the Metadata page, note the:
|
||||
|
||||
- "Login URL"
|
||||
- "Azure AD Identifier"
|
||||
- "Azure AD Identifier"
|
||||
|
||||
You'll need these URLs to complete the SSO setup in Metabase.
|
||||
|
||||
@@ -49,8 +50,8 @@ Log in to Metabase as an administrator and go to **Admin** -> **Settings** -> **
|
||||
|
||||
Under "Tell Metabase about your identity provider", enter the following:
|
||||
|
||||
- **SAML Identity Provider URL**: the "Login URL" you got on Step 4 on the Azure AD SAML SSO configuration
|
||||
- **SAML Identity Provider Certificate**: copy and paste the super long string under the "<X509Certificate>" tag in the "App Federation Metadata Url". Make sure you copy and paste the whole string; if you miss any character, the integration won't work.
|
||||
- **SAML Identity Provider URL**: the "Login URL" you got on Step 4 on the Azure AD SAML SSO configuration
|
||||
- **SAML Identity Provider Certificate**: copy and paste the super long string under the `<X509Certificate>` tag in the "App Federation Metadata Url". Make sure you copy and paste the whole string; if you miss any character, the integration won't work.
|
||||
- **SAML Application Name**: "Metabase"
|
||||
- **SAML Identity Provider Issuer**: the "Azure AD Identifier" URL you got from the Azure AD SAML SSO configuration.
|
||||
|
||||
|
||||
@@ -6,37 +6,45 @@ title: Notification permissions
|
||||
|
||||
Notifications in Metabase include [alerts](../questions/sharing/alerts.md) and [dashboard subscriptions](../dashboards/subscriptions.md#setting-up-a-dashboard-subscription).
|
||||
|
||||
## Who can edit dashboard subscriptions and alerts
|
||||
|
||||
What you can do with alerts and dashboard subscriptions depends on whether you're in the Administrators group or in a sandboxed group.
|
||||
|
||||
- [All Users group](#all-users-group-notification-permissions)
|
||||
- [Sandboxed group](#sandboxed-accounts-notification-permissions)
|
||||
- [Administrators group](#administrators-group-notification-permissions)
|
||||
|
||||
### All Users group notification permissions
|
||||
|
||||
Everyone's in the All Users group. Which means that everyone can:
|
||||
|
||||
- Create [alerts](../questions/sharing/alerts.md) and [dashboard subscriptions](../dashboards/subscriptions.md#setting-up-a-dashboard-subscription).
|
||||
- Add new recipients to dashboard subscriptions that they created. Non-admins can only add themselves to alerts.
|
||||
- Unsubscribe from any alert or subscription in their [Account settings](../people-and-groups/account-settings.md).
|
||||
|
||||
When a notification creator adds new recipients to an alert or subscription, Metabase will display data to the recipients using the **creator's** [data permissions](../permissions/data.md) and [collection permissions](../permissions/collections.md).
|
||||
|
||||
### Sandboxed accounts notification permissions
|
||||
|
||||
Same as everyone in the All Users group, but with a special case: **people in sandboxed groups will only see themselves in the list of recipients** when creating an alert or subscription.
|
||||
|
||||
### Administrators group notification permissions
|
||||
|
||||
People in the admin group can:
|
||||
|
||||
- View all subscriptions and alerts.
|
||||
- Add or remove recipients from an existing subscription or alert. Admins can safely add and remove recipients without changing the permissions of the alert or subscription. For example, if an admin adds Anya to a subscription created by Beau, Anya will receive emails with the same data that Beau can see (not what the admin can see).
|
||||
- Delete subscriptions or alerts.
|
||||
|
||||
Admins can also use Metabase's [auditing tools](../usage-and-performance-tools/audit.md#subscriptions-and-alerts) to manage notifications.
|
||||
|
||||
## What notification recipients can see
|
||||
|
||||
Notification **recipients** can see whatever the notification **creator** can see. For example, if:
|
||||
|
||||
- Beau creates a subscription to a dashboard saved in their [personal collection](../exploration-and-organization/collections.md#your-personal-collection).
|
||||
- Beau adds Anya to the dashboard subscription.
|
||||
- Anya will see the dashboard in her email, even though she doesn't have permissions to view that dashboard in Beau's personal collection.
|
||||
|
||||
## All accounts
|
||||
|
||||
From [Account settings](../people-and-groups/account-settings.md), all accounts can:
|
||||
|
||||
- Create [alerts](../questions/sharing/alerts.md) and [dashboard subscriptions](../dashboards/subscriptions.md#setting-up-a-dashboard-subscription).
|
||||
- Add new recipients to dashboard subscriptions that they own. Non-admins can only add themselves to alerts.
|
||||
- Unsubscribe from any alert or subscription.
|
||||
|
||||
When a notification creator adds new recipients to an alert or subscription, Metabase will display data to the recipients using the **creator's** [data permissions](../permissions/data.md) and [collection permissions](../permissions/collections.md).
|
||||
|
||||
## Sandboxed accounts
|
||||
|
||||
Same as [all accounts](#all-accounts), but **people using sandboxed accounts will only see themselves in the list of recipients** when creating an alert or subscription.
|
||||
|
||||
## Admins
|
||||
|
||||
{% include plans-blockquote.html feature="Auditing tools" %}
|
||||
|
||||
From Metabase's [auditing tools](../usage-and-performance-tools/audit.md#subscriptions-and-alerts), admins can:
|
||||
|
||||
- View all subscriptions and alerts
|
||||
- Add or remove recipients from an existing subscription or alert
|
||||
- Delete subscriptions or alerts
|
||||
|
||||
Admins can add recipients without changing the permissions of the alert or subscription. For example, if an admin adds Anya to a subscription created by Beau, Anya will receive emails with the same data that the Beau can see.
|
||||
- Anya will see the dashboard results in her email, even though she doesn't have permissions to view that dashboard in Beau's personal collection.
|
||||
|
||||
## More control over email options
|
||||
|
||||
|
||||
BIN
docs/questions/images/create-a-public-link.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
docs/questions/images/dashboard-sharing.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
docs/questions/images/download-card-results.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 57 KiB |
BIN
docs/questions/images/export-dashboard-as-pdf.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
docs/questions/images/exporting-the-results-of-a-question.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
docs/questions/images/public-embed.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 132 KiB |
@@ -6,7 +6,7 @@ redirect_from:
|
||||
|
||||
## Referencing models and saved questions
|
||||
|
||||
With SQL databases, we can use a [model][model] or an existing question as the basis for a new query, or as a common table expression [CTE][CTE].
|
||||
With SQL databases, we can use a [model][model] or an existing question as the basis for a new query, or as a common table expression [CTE][cte].
|
||||
|
||||
For example, let's say we have a lot of data spread across a number of tables, but people are most interested in a subset of that data. We can perform a complicated query once to return those results, and save that question as a model, which people can refer to in their queries just like they would with any other table.
|
||||
|
||||
@@ -37,14 +37,21 @@ FROM gizmo_orders
|
||||
When this query is run, the `{% raw %}{{#5-gizmo-orders-in-2019}}{% endraw %}` tag will be substituted with the SQL query of the referenced question, surrounded by parentheses. So it'll look like this under the hood:
|
||||
|
||||
```
|
||||
WITH gizmo_orders AS (SELECT *
|
||||
FROM orders AS o
|
||||
INNER JOIN products AS p
|
||||
ON o.product_id = p.id
|
||||
WHERE p.category = 'Gizmo'
|
||||
AND o.created_at BETWEEN '2019-01-01' AND '2019-12-31')
|
||||
SELECT count(*)
|
||||
FROM gizmo_orders
|
||||
WITH
|
||||
gizmo_orders AS (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
orders AS o
|
||||
INNER JOIN products AS p ON o.product_id = p.id
|
||||
WHERE
|
||||
p.category = 'Gizmo'
|
||||
AND o.created_at BETWEEN '2019-01-01' AND '2019-12-31'
|
||||
)
|
||||
SELECT
|
||||
count(*)
|
||||
FROM
|
||||
gizmo_orders
|
||||
```
|
||||
|
||||
## Limitations and tradeoffs
|
||||
@@ -61,6 +68,5 @@ FROM gizmo_orders
|
||||
- [SQL troubleshooting guide](../../troubleshooting-guide/sql.md).
|
||||
- [Segments and Metrics](../../data-modeling/segments-and-metrics.md)
|
||||
|
||||
|
||||
[cte]: https://www.metabase.com/learn/sql-questions/sql-cte
|
||||
[model]: ../../data-modeling/models.md
|
||||
|
||||
@@ -18,7 +18,7 @@ Field Filter, a special type of filter, have a [slightly different syntax](#fiel
|
||||
|
||||
This example defines a **Text** variable called `category`:
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
count(*)
|
||||
@@ -31,7 +31,7 @@ WHERE
|
||||
|
||||
Metabase will read the variable and attach a filter widget to the query, which people can use to change the value inserted into the `cat` variable with quotes. So if someone entered "Gizmo" into the filter widget, the query Metabase would run would be:
|
||||
|
||||
```sql
|
||||
```
|
||||
SELECT
|
||||
count(*)
|
||||
FROM
|
||||
@@ -118,7 +118,7 @@ Let's say you want to create a Field Filter that filters the `People` table by s
|
||||
|
||||
The syntax for Field Filters differs from a Text, Number, or Date variable.
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
*
|
||||
@@ -196,7 +196,7 @@ The reason is that field filters generate SQL based on the mapped field; Metabas
|
||||
|
||||
Your main query should be aware of all the tables that your Field Filter variable is pointing to, otherwise you'll get a SQL syntax error. For example, let's say that your main query includes a field filter like this:
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
*
|
||||
@@ -209,7 +209,7 @@ WHERE
|
||||
|
||||
Let's say the `{% raw %}{{ product_category }}{% endraw %}` variable refers to another question that uses the `Products` table. For the field filter to work, you'll need to include a join to `Products` in your main query.
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
*
|
||||
@@ -248,7 +248,7 @@ In the variables sidebar, you can set a default value for your variable. This va
|
||||
|
||||
You can also define default values directly in your query by enclosing comment syntax inside the end brackets of an optional parameter.
|
||||
|
||||
```sql
|
||||
```
|
||||
WHERE column = [[ {% raw %}{{ your_parameter }}{% endraw %} --]] your_default_value
|
||||
```
|
||||
|
||||
@@ -256,7 +256,7 @@ The comment will "activate" whenever you pass a value to `your_parameter`.
|
||||
|
||||
This is useful when defining complex default values (for example, if your default value is a function like `CURRENT_DATE`). Here's a PostgreSQL example that sets the default value of a Date filter to the current date using `CURRENT_DATE`:
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
*
|
||||
@@ -286,7 +286,7 @@ To make a clause optional in your native query, type `[[brackets around a {% raw
|
||||
|
||||
In this example, if no value is given to `cat`, then the query will just select all the rows from the `products` table. But if `cat` does have a value, like "Widget", then the query will only grab the products with a category type of Widget:
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
count(*)
|
||||
@@ -297,7 +297,7 @@ FROM
|
||||
|
||||
To use multiple optional clauses, you must include at least one regular `WHERE` clause followed by optional clauses, each starting with `AND`:
|
||||
|
||||
```sql
|
||||
```
|
||||
{% raw %}
|
||||
SELECT
|
||||
count(*)
|
||||
|
||||
@@ -57,7 +57,7 @@ where **Sighting Type** is a custom column with the expression:
|
||||
```
|
||||
case([Has Wings] = TRUE AND [Has Face] = TRUE, "Bird",
|
||||
[Has Wings] = TRUE AND [Has Face] = FALSE, "Plane",
|
||||
[Has Wings] = FALSE AND [Has Face] = TRUE, "Superman"), "Unknown")
|
||||
[Has Wings] = FALSE AND [Has Face] = TRUE, "Superman", "Unknown")
|
||||
```
|
||||
|
||||
You can use the columns holding your "labels" to:
|
||||
|
||||
74
docs/questions/sharing/exporting-results.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Exporting results
|
||||
---
|
||||
|
||||
# Exporting results
|
||||
|
||||
You can export the results of a question or dashboard.
|
||||
|
||||
## Exporting results of a question
|
||||
|
||||
To export the results of a questions, click on the **Download** button in the lower right of a question.
|
||||
|
||||

|
||||
You can export the results of a question as:
|
||||
|
||||
- .csv
|
||||
- .xlsx
|
||||
- .json
|
||||
|
||||
|
||||
If you don't see this option, you may not have [permissions to download results](../../permissions/data.md#download-results).
|
||||
|
||||
### Maximum number of rows you can download
|
||||
|
||||
You can download up to 1 million rows.
|
||||
|
||||
### Exporting unformatted results
|
||||
|
||||
You can export the unformatted results of a question by holding the `option` key on a Mac or the `Alt` key on PC when selecting the file format for your download.
|
||||
|
||||
Metabase will export the raw results of a question without applying any of the [formatting you applied](../../data-modeling/formatting.md) to the columns in the question. For example, if you formatted a floating point number to display only the first two decimal digits in the table results, exporting the unformatted results would include additional decimal digits (if any) found in the raw results.
|
||||
|
||||
## Exporting data via a public link
|
||||
|
||||
You can also create a [public link](../sharing/public-links.md#public-link-to-export-question-results-in-csv-xlsx-json) that people can use to download data in a specific format, as well as [raw, unformatted question results](public-links.md#exporting-raw-unformatted-question-results).
|
||||
|
||||
## Exporting question data via alerts
|
||||
|
||||
You can also export data by setting up an [alert](../sharing/alerts.md).
|
||||
|
||||
## Exporting results of a dashboard
|
||||
|
||||
You can export a dashboard as a PDF. Click on the three dot menu (**...**) and select **Export as PDF**.
|
||||
|
||||
.
|
||||
|
||||
The PDF will only include screenshots of the charts as they are visible on the dashboard.
|
||||
|
||||
### Exporting results of a dashboard card
|
||||
|
||||
To export the results of a particular card, hover over the dashboard card, click on the three dot menu (**...**), and select **Download results**.
|
||||
|
||||

|
||||
|
||||
From here you can select:
|
||||
|
||||
- .csv
|
||||
- .xlsx
|
||||
- .json
|
||||
- .png (if a chart)
|
||||
|
||||
To export the raw, unformatted results, hold down the `option` key for Macs, or `Alt` key for Windows, then click on the download file format.
|
||||
|
||||
If you don't see this option, you may not have [permissions to download results](../../permissions/data.md#download-results).
|
||||
|
||||
## Exporting results of a dashboard via dashboard subscriptions
|
||||
|
||||
You can use [dashboard subscriptions](../../dashboards/subscriptions.md) to regularly export data from all questions on a dashboard, and include those results as an attachment.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Alerts](../sharing/alerts.md)
|
||||
- [Dashboard subscriptions](../../dashboards/subscriptions.md)
|
||||
- [Tables](../sharing/visualizations/table.md)
|
||||
@@ -3,36 +3,25 @@ title: Public sharing
|
||||
redirect_from:
|
||||
- /docs/latest/administration-guide/12-public-links
|
||||
- /docs/latest/embedding/12-public-links
|
||||
- /docs/latest/questions/sharing/public-links
|
||||
---
|
||||
|
||||
# Public sharing
|
||||
|
||||
Sometimes you'll want to share a dashboard or question with someone who isn't a part of your organization or company. To share your work with people who don't need access to your full Metabase instance, you can create public links and public embeds.
|
||||
You can create and share public links (URLs) for questions and dashboards. You can view them as standalone destinations (URLs) or as embedded iframes in another page or app. In both cases, the public item will display static (view-only) results of your question or dashboard, so visitors won't be able to drill-down into the underlying data on their own.
|
||||
|
||||
## Enable public sharing in Metabase
|
||||
## Create a public link for a question
|
||||
|
||||

|
||||

|
||||
|
||||
In order to share Metabase items like questions and dashboards via a public link or an embedded iframe, an admin needs to enable public sharing for that Metabase instance by going to **Settings gear icon** > **Admin settings** > **Public sharing**.
|
||||
You can find the **Sharing** icon at the bottom right corner of a question. You can create a link to view the question as is, or to [export the results of a question](#public-link-to-export-question-results-in-csv-xlsx-json).
|
||||
|
||||
Once toggled on, the **Public sharing** section will display Metabase questions, dashboards, and actions with active public links. To deactivate a public link, click the **X** in the **Revoke link** column for that item.
|
||||
## Create a public link for a dashboard
|
||||
|
||||
## Enable sharing on your saved question or dashboard
|
||||
To share a dashboard via a public link, click on the **Sharing** button in the top right menu.
|
||||
|
||||
Once [public sharing](#enable-sharing-on-your-saved-question-or-dashboard) is enabled for your Metabase, you'll find the **Sharing and embedding icon** on saved questions and dashboards (it looks like a box with an arrow pointing to the upper right).
|
||||

|
||||
|
||||
You can find the **Sharing and embedding icon** icon at the bottom right corner of a question, or the top right corner of a dashboard.
|
||||
|
||||
To enable public sharing on a question or dashboard, click on the **Sharing and embedding icon** icon to bring up the sharing settings modal, then click on the toggle.
|
||||
|
||||

|
||||
|
||||
For more information about the option to **Embed this item in an application**, see the docs on [static embedding](../../embedding/static-embedding.md).
|
||||
|
||||
## Public links
|
||||
|
||||
Once you've [enabled sharing on your question or dashboard](#enable-sharing-on-your-saved-question-or-dashboard), you can copy and share the public link URL with whomever you please. The public link URL will display static (view-only) results of your question or dashboard, so visitors won't be able to drill-down into the underlying data on their own.
|
||||
To embed a dashboard, see [static embedding](../../embedding/static-embedding.md).
|
||||
|
||||
## Public link to export question results in CSV, XLSX, JSON
|
||||
|
||||
@@ -40,21 +29,34 @@ The export option is only available for questions, not dashboards.
|
||||
|
||||
To create a public link that people can use to download the results of a question:
|
||||
|
||||
1. Click on the **Sharing and embedding** icon for the question.
|
||||
2. Click the toggle to [enable sharing](#enable-sharing-on-your-saved-question-or-dashboard).
|
||||
3. Click on the file format you want (below the **Public link** URL).
|
||||
4. Open the public link in a new tab to test the download.
|
||||
1. Click on the **Sharing** icon for the question.
|
||||
2. Select **Public link**.
|
||||
3. Click on the file format you want (below the **Public link** URL): CSV, XLSX, or JSON.
|
||||
|
||||

|
||||
|
||||
Open the public link in a new tab to test the download.
|
||||
|
||||
### Exporting raw, unformatted question results
|
||||
|
||||
To export the raw, unformatted rows, you'll need to append `?format_rows=false` to the URL Metabase generates. For example, if you create a public link for a CSV download, the URL would look like:
|
||||
|
||||
```html
|
||||
https://www.example.com/public/question/cf347ce0-90bb-4669-b73b-56c73edd10cb.csv?format_rows=false
|
||||
```
|
||||
|
||||
By default, Metabase will export the results of a question that include any formatting you added (for example, if you formatted a column with floats to display as a percentage (0.42 -> 42%)).
|
||||
|
||||
See docs for the [export format endpoint](https://www.metabase.com/docs/latest/api/public#get-apipubliccarduuidqueryexport-format).
|
||||
|
||||
## Simulating drill-through with public links
|
||||
|
||||
Metabase's automatic [drill-through](https://www.metabase.com/learn/questions/drill-through) won't work on public dashboards because public links don't give people access to your raw data.
|
||||
|
||||
You can simulate drill-through on a public dashboard by setting up a [custom click behaviour](../../dashboards/interactive.md) that sends people from one public link to another public link.
|
||||
You can simulate drill-through on a public dashboard by setting up a [custom click behavior](../../dashboards/interactive.md) that sends people from one public link to another public link.
|
||||
|
||||
1. Create a second dashboard to act as the destination dashboard.
|
||||
2. [Enable sharing](#enable-sharing-on-your-saved-question-or-dashboard) on the destination dashboard.
|
||||
2. [Create a public link](#create-a-public-link-for-a-dashboard) for the destination dashboard.
|
||||
3. Copy the destination dashboard's public link.
|
||||
4. On your primary dashboard, create a [custom destination](../../dashboards/interactive.md#custom-destinations) with type "URL".
|
||||
5. Set the custom destination to the destination dashboard's public link.
|
||||
@@ -69,12 +71,15 @@ For example, if you have a primary public dashboard that displays **Invoices** d
|
||||
|
||||
## Public embeds
|
||||
|
||||
If you want to embed your question or dashboard in a simple web page or blog post:
|
||||

|
||||
|
||||
1. Click on the **Sharing and embedding** icon for your question or dashboard.
|
||||
2. Click the toggle to [enable sharing](#enable-sharing-on-your-saved-question-or-dashboard).
|
||||
3. Copy the **Public embed** iframe snippet.
|
||||
4. Paste the iframe snippet in your destination of choice.
|
||||
If you want to embed your question or dashboard as an iframe in a simple web page or app:
|
||||
|
||||
1. Click on the **Sharing** icon for your question or dashboard.
|
||||
2. Click **Embed**.
|
||||
3. Click **Public embed**.
|
||||
4. Copy the iframe snippet Metabase generates for you.
|
||||
5. Paste the iframe snippet in your destination of choice.
|
||||
|
||||
To customize the appearance of your question or dashboard, you can update the link in the `src` attribute with [public embed parameters](#public-embed-parameters).
|
||||
|
||||
@@ -157,6 +162,42 @@ Note that the name of the filter in the URL should be specified in lower case, a
|
||||
/dashboard/42?filter_for_user_zip_code=02116
|
||||
```
|
||||
|
||||
## Disable public sharing in Metabase
|
||||
|
||||
Public sharing is enabled by default.
|
||||
|
||||

|
||||
|
||||
To disable public sharing:
|
||||
|
||||
1. Click on the **Gear** icon in the upper right.
|
||||
2. Select **Admin settings**.
|
||||
3. In the **Settings** tab, select **Public sharing**.
|
||||
4. Toggle off **Public sharing**.
|
||||
|
||||
Once toggled on, the **Public sharing** section will display Metabase questions, dashboards, and actions with active public links.
|
||||
|
||||
If you disable public sharing, then re-enable public sharing, all your previously generated public links will still work (as long as you didn't deactivate them).
|
||||
|
||||
## Deactivating public links and embeds
|
||||
|
||||
### Individual question or dashboard links and embeds
|
||||
|
||||
1. Visit the question or dashboard.
|
||||
2. Click on **Sharing** icon.
|
||||
3. Select **Public link** or **Embed**.
|
||||
4. Click **Remove public link**.
|
||||
|
||||
## Deactivating multiple public links and embeds
|
||||
|
||||
Admins can view and deactivate all public links for a Metabase.
|
||||
|
||||
1. Click on the **gear** icon in the upper right.
|
||||
2. Select **Admin settings**.
|
||||
3. Go to the **Settings** tab.
|
||||
4. Go to the **Public sharing** tab in the left sidebar.
|
||||
5. For each item you want to deactivate, click on the **X** to revoke its public link.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Publishing data visualizations to the web](https://www.metabase.com/learn/embedding/embedding-charts-and-dashboards).
|
||||
|
||||
@@ -22,29 +22,126 @@ To format the display of any column in a table, click on the column heading and
|
||||
|
||||
The options you see will differ depending on the type of column you're viewing:
|
||||
|
||||
### Dates
|
||||
## Text formatting options
|
||||
|
||||
- **Date style** gives you a bunch of different choices for how to display the date.
|
||||
- **Abbreviate names of days and months**, when turned on, will turn things like `January` to `Jan`, and `Monday` to `Mon`.
|
||||
- **Show the time** lets you decide whether or not to display the time, and if so, how. You can include hours and minutes, and additionally seconds and milliseconds.
|
||||
### Title
|
||||
|
||||
### Numbers
|
||||
You can rename the column.
|
||||
|
||||
- **Show a mini bar chart** will display a small horizontal bar next to each number in this column to show its size relative to the other values in the column.
|
||||
- **Style** lets you choose to display the number as a plain number, a percent, in scientific notation, or as a currency.
|
||||
- **Separator style** gives you various options for how commas and periods are used to separate the number.
|
||||
- **Minimum number of decimal places** forces the number to be displayed with exactly this many decimal places.
|
||||
- **Multiply by a number** multiplies each number in this column by whatever you type here. Just don't type an emoji here; it almost always causes a temporal vortex to manifest.
|
||||
- **Add a prefix/suffix** lets you put a symbol, word, or whatever before or after each cell's value.
|
||||
### Display as
|
||||
|
||||
### Currency
|
||||
Currency columns have all the same options as numbers, plus the following:
|
||||
You can display the text as is, or you can display the text as a link. If you select link, you can also change the text displayed, and use parameters for both the link text and the link's URL.
|
||||
|
||||
- **Unit of Currency** lets you change the unit of currency from whatever the system default is.
|
||||
- **Currency label style** allows you to switch between displaying the currency label as a symbol, a code like (USD), or the full name of the currency.
|
||||
- **Where to display the unit of currency** lets you toggle between showing the currency label in the column heading or in every cell in the column.
|
||||
For example, you could create a dynamic URL using a parameter from another column in the results:
|
||||
|
||||
### Conditional table formatting
|
||||
```html
|
||||
{% raw %}
|
||||
https://www.example.com/{{category}}
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
In the above example, Metabase would take the value for the `category` column for that row (in this case `widget`), and insert it into the URL:
|
||||
|
||||
```
|
||||
https://www.example.com/widget
|
||||
```
|
||||
|
||||
## Date formatting options
|
||||
|
||||
Date formatting options include the same options as [Text formatting](#text-formatting-options), as well as:
|
||||
|
||||
### Date style
|
||||
|
||||
Styles include:
|
||||
|
||||
- April 1, 2024
|
||||
- 1 April 2024
|
||||
- Monday, April 1, 2024
|
||||
- 4/1/2024
|
||||
- 1/4/2024
|
||||
- 2024/4/1
|
||||
|
||||
### Abbreviate days and months
|
||||
|
||||
If you select a date style that includes words, this toggle will abbreviate them. For example:
|
||||
|
||||
- Unabbreviated: Monday, April 1, 2024
|
||||
- Abbreviated: Mon, Apr 1, 2024
|
||||
|
||||
### Show the time
|
||||
|
||||
This option determines whether Metabase includes the time along with the date.
|
||||
|
||||
- Off
|
||||
- HH:MM
|
||||
- HH:MM:SS
|
||||
- HH:MM:SS:MS
|
||||
|
||||
### Time style
|
||||
|
||||
You can choose either 12-hour clock style, or 24-hour clock style.
|
||||
|
||||
- 5:24 PM (12-hour clock)
|
||||
- 17:24 (24-hour clock)
|
||||
|
||||
## Number formatting options
|
||||
|
||||
### Show a mini bar chart
|
||||
|
||||
This option displays a small horizontal bar next to each number in this column to show its size relative to the other values in the column.
|
||||
|
||||
### Style
|
||||
|
||||
Number styles include:
|
||||
|
||||
- Number
|
||||
- Percent
|
||||
- Scientific
|
||||
- Currency
|
||||
|
||||
### Separator style
|
||||
|
||||
Gives you various options for how commas and periods are used to separate the number.
|
||||
|
||||
- 100,000.00
|
||||
- 100 000,00
|
||||
- 100.000,00
|
||||
- 100000.00
|
||||
- 100'000.00
|
||||
|
||||
### Minimum number of decimal places
|
||||
|
||||
Forces the number to be displayed with exactly this many decimal places.
|
||||
|
||||
### Multiply by a number
|
||||
|
||||
Multiplies each number in this column by whatever you type here. Just don't type an emoji here; there is a nontrivial chance that it will manifest a temporal vortex.
|
||||
|
||||
### Add a prefix/suffix
|
||||
|
||||
Lets you put a symbol, word, or whatever before or after each cell's value.
|
||||
|
||||
## Currency formatting options
|
||||
|
||||
Currency columns have all the same options as [numbers](#number-formatting-options), plus the following:
|
||||
|
||||
### Unit of Currency
|
||||
|
||||
Lets you change the unit of currency from whatever the system default is.
|
||||
|
||||
### Currency label style
|
||||
|
||||
Allows you to switch between displaying the currency label as:
|
||||
|
||||
- a symbol (like $)
|
||||
- a code (like USD)
|
||||
- the full name of the currency (like "US dollars")
|
||||
|
||||
### Where to display the unit of currency
|
||||
|
||||
Lets you toggle between showing the currency label in the column heading or in every cell in the column.
|
||||
|
||||
## Conditional table formatting
|
||||
|
||||
Sometimes it's helpful to highlight certain rows or columns in your tables when they meet a specific condition. You can set up conditional formatting rules by going to the visualization settings while looking at any table, then clicking on the **Conditional Formatting** tab.
|
||||
|
||||
|
||||
@@ -59,3 +59,7 @@ Get results via email or Slack, either on a schedule, or only when something int
|
||||
### [Public sharing](./sharing/public-links.md)
|
||||
|
||||
Create links or embeds for the good people of the internet.
|
||||
|
||||
### [Exporting](./sharing/exporting-results.md)
|
||||
|
||||
The different ways you can export the results of questions and dashboards.
|
||||
|
||||
@@ -19,8 +19,11 @@ To see what's new, check out all the [major release announcements](https://www.m
|
||||
|
||||
## Metabase Enterprise Edition releases
|
||||
|
||||
- [v1.49.3](https://github.com/metabase/metabase/releases/tag/v1.49.3)
|
||||
- [v1.49.2](https://github.com/metabase/metabase/releases/tag/v1.49.2)
|
||||
- [v1.49.1](https://github.com/metabase/metabase/releases/tag/v1.49.1)
|
||||
- [v1.49.0](https://github.com/metabase/metabase/releases/tag/v1.49.0)
|
||||
- [v1.48.10](https://github.com/metabase/metabase/releases/tag/v1.48.10)
|
||||
- [v1.48.9](https://github.com/metabase/metabase/releases/tag/v1.48.9)
|
||||
- [v1.48.8](https://github.com/metabase/metabase/releases/tag/v1.48.8)
|
||||
- [v1.48.7](https://github.com/metabase/metabase/releases/tag/v1.48.7)
|
||||
@@ -151,8 +154,11 @@ To see what's new, check out all the [major release announcements](https://www.m
|
||||
|
||||
## Metabase Open Source Edition releases
|
||||
|
||||
- [v0.49.3](https://github.com/metabase/metabase/releases/tag/v0.49.3)
|
||||
- [v0.49.2](https://github.com/metabase/metabase/releases/tag/v0.49.2)
|
||||
- [v0.49.1](https://github.com/metabase/metabase/releases/tag/v0.49.1)
|
||||
- [v0.49.0](https://github.com/metabase/metabase/releases/tag/v0.49.0)
|
||||
- [v0.48.10](https://github.com/metabase/metabase/releases/tag/v0.48.10)
|
||||
- [v0.48.9](https://github.com/metabase/metabase/releases/tag/v0.48.9)
|
||||
- [v0.48.8](https://github.com/metabase/metabase/releases/tag/v0.48.8)
|
||||
- [v0.48.7](https://github.com/metabase/metabase/releases/tag/v0.48.7)
|
||||
|
||||
60
docs/troubleshooting-guide/create-har-file.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Creating a HAR file for troubleshooting
|
||||
---
|
||||
|
||||
# Creating a HAR file for troubleshooting
|
||||
|
||||
HAR (short for _HTTP Archive_) files record network requests generated by your browser. HAR files can be helpful when troubleshooting issues with Metabase performance.
|
||||
|
||||
> HAR files can contain sensitive information that you submit during the recording, like passwords and API keys. Before sharing the HAR file with anyone, review the file contents in a text editor.
|
||||
|
||||
To create the HAR file, follow the instructions for your web browser:
|
||||
|
||||
- [Chrome](#create-a-har-file-in-chrome)
|
||||
- [Edge](#create-a-har-file-in-edge)
|
||||
- [Firefox](#create-a-har-file-in-firefox)
|
||||
- [Safari](#create-a-har-file-in-safari)
|
||||
|
||||
## Create a HAR file in Chrome
|
||||
|
||||
1. Open Chrome **Developer tools**.
|
||||
You can right-click anywhere on the page and select "Inspect".
|
||||
|
||||
2. In Developer tools, switch to the **Network** tab.
|
||||
The network log recording should start automatically.
|
||||
|
||||
3. With the Network tab open and recording in progress, repeat the steps to reproduce the issue.
|
||||
|
||||
4. Once you're finished reproducing the issue, right-click anywhere in the table of network calls and select **Save all as HAR with content**.
|
||||
|
||||
## Create a HAR file in Edge
|
||||
|
||||
1. Open Edge **Developer tools**. You can right-click anywhere on the page and select "Inspect".
|
||||
|
||||
2. In Developer tools, switch to the **Network** tab (look for the Wi-Fi icon). The network log recording should start automatically.
|
||||
|
||||
3. With the Network tab open and recording in progress, repeat the steps to reproduce the issue.
|
||||
|
||||
4. Once you're finished reproducing the issue, right-click anywhere in the table and select **Save all as HAR with content**
|
||||
|
||||
## Create a HAR file in Firefox
|
||||
|
||||
1. Open Firefox **Developer tools**. You can right-click anywhere on the page and select "Inspect".
|
||||
|
||||
2. In Developer tools, switch to the **Network** tab.
|
||||
|
||||
3. With the Network tab open and recording in progress, repeat the steps to reproduce the issue.
|
||||
|
||||
4. Once you're finished reproducing the issue, right-click anywhere in the table of network calls and select **Save All As HAR**.
|
||||
|
||||
## Create a HAR file in Safari
|
||||
|
||||
1. If you haven't yet, enable the **Develop** menu by going to **Safari > Settings > Advanced**, and select **Show features for web developers**.
|
||||
|
||||
2. Open the Safari developer tools by going to **Develop > Show Web Inspector**.
|
||||
|
||||
3. In Developer tools, switch to the **Network** tab. The network log recording should start automatically.
|
||||
|
||||
4. With the Network tab open and recording in progress, repeat the steps to reproduce the issue.
|
||||
|
||||
5. Once you're finished reproducing the issue, click on **Export** in the top right of the Network tab.
|
||||
@@ -76,6 +76,8 @@ Metabase will log errors, both on the server and in the browser console, dependi
|
||||
- [Safari][safari]
|
||||
- [Edge][edge]
|
||||
|
||||
**Creating a HAR file**: HAR files record network requests generated by your browser. HAR files can be helpful when troubleshooting issues with Metabase performance. Check out [Creating HAR files for troubleshooting](./create-har-file.md).
|
||||
|
||||
## Metabase tutorials
|
||||
|
||||
For tutorials that walk you through how to use Metabase features, check out [Learn Metabase][learn].
|
||||
@@ -86,7 +88,7 @@ To see if someone else has run into a similar issue, check out [our forum on Dis
|
||||
|
||||
## Upgrading Metabase
|
||||
|
||||
Metabase adds new features and squashes bugs with each release. [Upgrading to the latest and greatest][upgrade] may resolve your issue. If you're using [Metabase Cloud][cloud], we'll handle the upgrades for you. You can checkout the [release notes][releases] to see what's new.
|
||||
Metabase adds new features and squashes bugs with each release. [Upgrading to the latest and greatest][upgrade] may resolve your issue. If you're using [Metabase Cloud][cloud], we'll handle the upgrades for you. To see what's new, check out the [release notes][releases].
|
||||
|
||||
[appdb]: ./loading-from-h2.md
|
||||
[bugs]: ./bugs.md
|
||||
|
||||
@@ -7,7 +7,14 @@ import {
|
||||
SAMPLE_DB_TABLES,
|
||||
METABASE_SECRET_KEY,
|
||||
} from "e2e/support/cypress_data";
|
||||
import { snapshot, restore, withSampleDatabase } from "e2e/support/helpers";
|
||||
import {
|
||||
snapshot,
|
||||
restore,
|
||||
withSampleDatabase,
|
||||
setTokenFeatures,
|
||||
describeEE,
|
||||
deleteToken,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const {
|
||||
STATIC_ORDERS_ID,
|
||||
@@ -63,6 +70,29 @@ describe("snapshots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeEE("default-ee", () => {
|
||||
it("default-ee", () => {
|
||||
restore("blank");
|
||||
setup();
|
||||
updateSettings();
|
||||
setTokenFeatures("all");
|
||||
addUsersAndGroups(true);
|
||||
createCollections();
|
||||
withSampleDatabase(SAMPLE_DATABASE => {
|
||||
ensureTableIdsAreCorrect(SAMPLE_DATABASE);
|
||||
hideNewSampleTables(SAMPLE_DATABASE);
|
||||
createQuestionsAndDashboards(SAMPLE_DATABASE);
|
||||
createModels(SAMPLE_DATABASE);
|
||||
cy.writeFile(
|
||||
"e2e/support/cypress_sample_database.json",
|
||||
SAMPLE_DATABASE,
|
||||
);
|
||||
});
|
||||
deleteToken();
|
||||
snapshot("default-ee");
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
cy.request("GET", "/api/session/properties").then(
|
||||
({ body: properties }) => {
|
||||
@@ -86,10 +116,13 @@ describe("snapshots", () => {
|
||||
|
||||
function updateSettings() {
|
||||
cy.request("PUT", "/api/setting/enable-public-sharing", { value: true });
|
||||
cy.request("PUT", "/api/setting/enable-embedding", { value: true });
|
||||
cy.request("PUT", "/api/setting/embedding-secret-key", {
|
||||
value: METABASE_SECRET_KEY,
|
||||
});
|
||||
cy.request("PUT", "/api/setting/enable-embedding", { value: true }).then(
|
||||
() => {
|
||||
cy.request("PUT", "/api/setting/embedding-secret-key", {
|
||||
value: METABASE_SECRET_KEY,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// update the Sample db connection string so it is valid in both CI and locally
|
||||
cy.request("GET", `/api/database/${SAMPLE_DB_ID}`).then(response => {
|
||||
@@ -99,26 +132,28 @@ describe("snapshots", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function addUsersAndGroups() {
|
||||
function addUsersAndGroups(isEE = false) {
|
||||
const lowest_read_data_permission = isEE ? "blocked" : "unrestricted";
|
||||
|
||||
// groups
|
||||
cy.request("POST", "/api/permissions/group", { name: "collection" }).then(
|
||||
({ body }) => {
|
||||
expect(body.id).to.eq(COLLECTION_GROUP); // 4
|
||||
expect(body.id).to.eq(COLLECTION_GROUP); // 3
|
||||
},
|
||||
);
|
||||
cy.request("POST", "/api/permissions/group", { name: "data" }).then(
|
||||
({ body }) => {
|
||||
expect(body.id).to.eq(DATA_GROUP); // 5
|
||||
expect(body.id).to.eq(DATA_GROUP); // 4
|
||||
},
|
||||
);
|
||||
cy.request("POST", "/api/permissions/group", { name: "readonly" }).then(
|
||||
({ body }) => {
|
||||
expect(body.id).to.eq(READONLY_GROUP); // 6
|
||||
expect(body.id).to.eq(READONLY_GROUP); // 5
|
||||
},
|
||||
);
|
||||
cy.request("POST", "/api/permissions/group", { name: "nosql" }).then(
|
||||
({ body }) => {
|
||||
expect(body.id).to.eq(NOSQL_GROUP); // 7
|
||||
expect(body.id).to.eq(NOSQL_GROUP); // 6
|
||||
},
|
||||
);
|
||||
|
||||
@@ -132,19 +167,35 @@ describe("snapshots", () => {
|
||||
|
||||
cy.updatePermissionsGraph({
|
||||
[ALL_USERS_GROUP]: {
|
||||
[SAMPLE_DB_ID]: { data: { schemas: "none", native: "none" } },
|
||||
[SAMPLE_DB_ID]: {
|
||||
// set the data permission so the UI doesn't warn us that "all users has higher permissions than X"
|
||||
"view-data": lowest_read_data_permission,
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
[DATA_GROUP]: {
|
||||
[SAMPLE_DB_ID]: { data: { schemas: "all", native: "write" } },
|
||||
[SAMPLE_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder-and-native",
|
||||
},
|
||||
},
|
||||
[NOSQL_GROUP]: {
|
||||
[SAMPLE_DB_ID]: { data: { schemas: "all", native: "none" } },
|
||||
[SAMPLE_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder",
|
||||
},
|
||||
},
|
||||
[COLLECTION_GROUP]: {
|
||||
[SAMPLE_DB_ID]: { data: { schemas: "none", native: "none" } },
|
||||
[SAMPLE_DB_ID]: {
|
||||
"view-data": lowest_read_data_permission,
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
[READONLY_GROUP]: {
|
||||
[SAMPLE_DB_ID]: { data: { schemas: "none", native: "none" } },
|
||||
[SAMPLE_DB_ID]: {
|
||||
"view-data": lowest_read_data_permission,
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
203
e2e/support/assets/pokedex.tsv
Normal file
@@ -0,0 +1,203 @@
|
||||
Id No Name Type1 Type2
|
||||
1 1 Bulbasaur Grass Poison
|
||||
2 2 Ivysaur Grass Poison
|
||||
3 3 Venusaur Grass Poison
|
||||
4 3 Mega Venusaur Grass Poison
|
||||
5 4 Charmander Fire
|
||||
6 5 Charmeleon Fire
|
||||
7 6 Charizard Fire Flying
|
||||
8 6 Mega Charizard X Fire Dragon
|
||||
9 6 Mega Charizard Y Fire Flying
|
||||
10 7 Squirtle Water
|
||||
11 8 Wartortle Water
|
||||
12 9 Blastoise Water
|
||||
13 9 Mega Blastoise Water
|
||||
14 10 Caterpie Bug
|
||||
15 11 Metapod Bug
|
||||
16 12 Butterfree Bug Flying
|
||||
17 13 Weedle Bug Poison
|
||||
18 14 Kakuna Bug Poison
|
||||
19 15 Beedrill Bug Poison
|
||||
20 15 Mega Beedrill Bug Poison
|
||||
21 16 Pidgey Normal Flying
|
||||
22 17 Pidgeotto Normal Flying
|
||||
23 18 Pidgeot Normal Flying
|
||||
24 18 Mega Pidgeot Normal Flying
|
||||
25 19 Rattata Normal
|
||||
26 19 Alolan Rattata Dark Normal
|
||||
27 20 Raticate Normal
|
||||
28 20 Alolan Raticate Dark Normal
|
||||
29 21 Spearow Normal Flying
|
||||
30 22 Fearow Normal Flying
|
||||
31 23 Ekans Poison
|
||||
32 24 Arbok Poison
|
||||
33 25 Pikachu Electric
|
||||
34 26 Raichu Electric
|
||||
35 26 Alolan Raichu Electric Psychic
|
||||
36 27 Sandshrew Ground
|
||||
37 27 Alolan Sandshrew Ice Steel
|
||||
38 28 Sandslash Ground
|
||||
39 28 Alolan Sandslash Ice Steel
|
||||
40 29 Nidoran F Poison
|
||||
41 30 Nidorina Poison
|
||||
42 31 Nidoqueen Poison Ground
|
||||
43 32 Nidoran M Poison
|
||||
44 33 Nidorino Poison
|
||||
45 34 Nidoking Poison Ground
|
||||
46 35 Clefairy Fairy
|
||||
47 36 Clefable Fairy
|
||||
48 37 Vulpix Fire
|
||||
49 37 Alolan Vulpix Ice
|
||||
50 38 Ninetales Fire
|
||||
51 38 Alolan Ninetales Ice Fairy
|
||||
52 39 Jigglypuff Normal Fairy
|
||||
53 40 Wigglytuff Normal Fairy
|
||||
54 41 Zubat Poison Flying
|
||||
55 42 Golbat Poison Flying
|
||||
56 43 Oddish Grass Poison
|
||||
57 44 Gloom Grass Poison
|
||||
58 45 Vileplume Grass Poison
|
||||
59 46 Paras Bug Grass
|
||||
60 47 Parasect Bug Grass
|
||||
61 48 Venonat Bug Poison
|
||||
62 49 Venomoth Bug Poison
|
||||
63 50 Diglett Ground
|
||||
64 50 Alolan Diglett Ground Steel
|
||||
65 51 Dugtrio Ground
|
||||
66 51 Alolan Dugtrio Ground Steel
|
||||
67 52 Meowth Normal
|
||||
68 52 Alolan Meowth Dark
|
||||
69 52 Galarian Meowth Steel
|
||||
70 53 Persian Normal
|
||||
71 53 Alolan Persian Dark
|
||||
72 54 Psyduck Water
|
||||
73 55 Golduck Water
|
||||
74 56 Mankey Fighting
|
||||
75 57 Primeape Fighting
|
||||
76 58 Growlithe Fire
|
||||
77 58 Hisuian Growlithe Fire Rock
|
||||
78 59 Arcanine Fire
|
||||
79 59 Hisuian Arcanine Fire Rock
|
||||
80 60 Poliwag Water
|
||||
81 61 Poliwhirl Water
|
||||
82 62 Poliwrath Water Fighting
|
||||
83 63 Abra Psychic
|
||||
84 64 Kadabra Psychic
|
||||
85 65 Alakazam Psychic
|
||||
86 65 Mega Alakazam Psychic
|
||||
87 66 Machop Fighting
|
||||
88 67 Machoke Fighting
|
||||
89 68 Machamp Fighting
|
||||
90 69 Bellsprout Grass Poison
|
||||
91 70 Weepinbell Grass Poison
|
||||
92 71 Victreebel Grass Poison
|
||||
93 72 Tentacool Water Poison
|
||||
94 73 Tentacruel Water Poison
|
||||
95 74 Geodude Rock Ground
|
||||
96 74 Alolan Geodude Rock Electric
|
||||
97 75 Graveler Rock Ground
|
||||
98 75 Alolan Graveler Rock Electric
|
||||
99 76 Golem Rock Ground
|
||||
100 76 Alolan Golem Rock Electric
|
||||
101 77 Ponyta Fire
|
||||
102 77 Galarian Ponyta Psychic
|
||||
103 78 Rapidash Fire
|
||||
104 78 Galarian Rapidash Psychic Fairy
|
||||
105 79 Slowpoke Water Psychic
|
||||
106 79 Galarian Slowpoke Psychic
|
||||
107 80 Slowbro Water Psychic
|
||||
108 80 Galarian Slowbro Poison Psychic
|
||||
109 80 Mega Slowbro Water Psychic
|
||||
110 81 Magnemite Electric Steel
|
||||
111 82 Magneton Electric Steel
|
||||
112 83 Farfetch'd Normal Flying
|
||||
113 83 Galarian Farfetch'd Fighting
|
||||
114 84 Doduo Normal Flying
|
||||
115 85 Dodrio Normal Flying
|
||||
116 86 Seel Water
|
||||
117 87 Dewgong Water Ice
|
||||
118 88 Grimer Poison
|
||||
119 88 Alolan Grimer Poison Dark
|
||||
120 89 Muk Poison
|
||||
121 89 Alolan Muk Poison Dark
|
||||
122 90 Shellder Water
|
||||
123 91 Cloyster Water Ice
|
||||
124 92 Gastly Ghost Poison
|
||||
125 93 Haunter Ghost Poison
|
||||
126 94 Gengar Ghost Poison
|
||||
127 94 Mega Gengar Ghost Poison
|
||||
128 95 Onix Rock Ground
|
||||
129 96 Drowzee Psychic
|
||||
130 97 Hypno Psychic
|
||||
131 98 Krabby Water
|
||||
132 99 Kingler Water
|
||||
133 100 Voltorb Electric
|
||||
134 100 Hisuian Voltorb Electric Grass
|
||||
135 101 Electrode Electric
|
||||
136 101 Hisuian Electrode Electric Grass
|
||||
137 102 Exeggcute Grass Psychic
|
||||
138 103 Exeggutor Grass Psychic
|
||||
139 103 Alolan Exeggutor Grass Dragon
|
||||
140 104 Cubone Ground
|
||||
141 105 Marowak Ground
|
||||
142 105 Alolan Marowak Fire Ghost
|
||||
143 106 Hitmonlee Fighting
|
||||
144 107 Hitmonchan Fighting
|
||||
145 108 Lickitung Normal
|
||||
146 109 Koffing Poison
|
||||
147 110 Weezing Poison
|
||||
148 110 Galarian Weezing Poison Fairy
|
||||
149 111 Rhyhorn Ground Rock
|
||||
150 112 Rhydon Ground Rock
|
||||
151 113 Chansey Normal
|
||||
152 114 Tangela Grass
|
||||
153 115 Kangaskhan Normal
|
||||
154 115 Mega Kangaskhan Normal
|
||||
155 116 Horsea Water
|
||||
156 117 Seadra Water
|
||||
157 118 Goldeen Water
|
||||
158 119 Seaking Water
|
||||
159 120 Staryu Water
|
||||
160 121 Starmie Water Psychic
|
||||
161 122 Mr. Mime Psychic Fairy
|
||||
162 122 Galarian Mr. Mime Ice Psychic
|
||||
163 123 Scyther Bug Flying
|
||||
164 124 Jynx Ice Psychic
|
||||
165 125 Electabuzz Electric
|
||||
166 126 Magmar Fire
|
||||
167 127 Pinsir Bug
|
||||
168 127 Mega Pinsir Bug Flying
|
||||
169 128 Tauros Fighting
|
||||
170 128 Paldean Tauros Fighting Fire
|
||||
171 128 Paldean Tauros Fighting Water
|
||||
172 128 Tauros Normal
|
||||
173 129 Magikarp Water
|
||||
174 130 Gyarados Water Flying
|
||||
175 130 Mega Gyarados Water Dark
|
||||
176 131 Lapras Water Ice
|
||||
177 132 Ditto Normal
|
||||
178 133 Eevee Normal
|
||||
179 134 Vaporeon Water
|
||||
180 135 Jolteon Electric
|
||||
181 136 Flareon Fire
|
||||
182 137 Porygon Normal
|
||||
183 138 Omanyte Rock Water
|
||||
184 139 Omastar Rock Water
|
||||
185 140 Kabuto Rock Water
|
||||
186 141 Kabutops Rock Water
|
||||
187 142 Aerodactyl Rock Flying
|
||||
188 142 Mega Aerodactyl Rock Flying
|
||||
189 143 Snorlax Normal
|
||||
190 144 Articuno Ice Flying
|
||||
191 144 Galarian Articuno Psychic Flying
|
||||
192 145 Zapdos Electric Flying
|
||||
193 145 Galarian Zapdos Fighting Flying
|
||||
194 146 Moltres Fire Flying
|
||||
195 146 Galarian Moltres Dark Flying
|
||||
196 147 Dratini Dragon
|
||||
197 148 Dragonair Dragon
|
||||
198 149 Dragonite Dragon Flying
|
||||
199 150 Mewtwo Psychic
|
||||
200 150 Mega Mewtwo X Psychic Fighting
|
||||
201 150 Mega Mewtwo Y Psychic
|
||||
202 151 Mew Psychic
|
||||
|
@@ -2,15 +2,18 @@ import { cypressWaitAll } from "e2e/support/helpers";
|
||||
|
||||
Cypress.Commands.add(
|
||||
"createDashboardWithQuestions",
|
||||
({ dashboardName, questions }) => {
|
||||
({ dashboardName, dashboardDetails, questions }) => {
|
||||
return cy
|
||||
.createDashboard({ name: dashboardName })
|
||||
.createDashboard({ name: dashboardName, ...dashboardDetails })
|
||||
.then(({ body: dashboard }) => {
|
||||
return cypressWaitAll(
|
||||
questions.map(query =>
|
||||
cy.createQuestionAndAddToDashboard(query, dashboard.id),
|
||||
),
|
||||
).then(questions => {
|
||||
).then(dashcardResponses => {
|
||||
const questions = dashcardResponses.map(
|
||||
dashcardResponse => dashcardResponse.body.card,
|
||||
);
|
||||
return {
|
||||
questions,
|
||||
dashboard,
|
||||
|
||||
@@ -26,7 +26,7 @@ Cypress.Commands.add(
|
||||
})
|
||||
.then(response => ({
|
||||
...response,
|
||||
body: response.body.dashcards[0],
|
||||
body: response.body.dashcards.at(-1),
|
||||
})),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,14 +20,17 @@ Cypress.Commands.add(
|
||||
const attr = Object.keys(attribute_remappings).join(", "); // Account for the possiblity of passing multiple user attributes
|
||||
|
||||
cy.log(`Sandbox "${name}" table on "${attr}"`);
|
||||
cy.updatePermissionsSchemas({
|
||||
schemas: {
|
||||
[schema]: {
|
||||
[table_id]: { query: "segmented", read: "all" },
|
||||
cy.updatePermissionsGraph({
|
||||
[group_id]: {
|
||||
[db_id]: {
|
||||
"view-data": {
|
||||
[schema]: {
|
||||
[table_id]: "sandboxed",
|
||||
},
|
||||
},
|
||||
"create-queries": "query-builder",
|
||||
},
|
||||
},
|
||||
user_group: group_id,
|
||||
database_id: db_id,
|
||||
});
|
||||
cy.request("POST", "/api/mt/gtap", {
|
||||
attribute_remappings,
|
||||
|
||||
@@ -66,7 +66,7 @@ export function startNewNativeQuestion(alias = "editor") {
|
||||
*/
|
||||
export function visitQuestionAdhoc(
|
||||
question,
|
||||
{ callback, mode, autorun = true } = {},
|
||||
{ callback, mode, autorun = true, skipWaiting = false } = {},
|
||||
) {
|
||||
const questionMode = mode === "notebook" ? "/notebook" : "";
|
||||
|
||||
@@ -78,7 +78,7 @@ export function visitQuestionAdhoc(
|
||||
|
||||
runQueryIfNeeded(question, autorun);
|
||||
|
||||
if (mode !== "notebook") {
|
||||
if (mode !== "notebook" && !skipWaiting) {
|
||||
cy.wait("@" + alias).then(xhr => callback && callback(xhr));
|
||||
}
|
||||
}
|
||||
@@ -110,18 +110,35 @@ export function openTable({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @typedef {{mode?: "notebook", limit?: number, callback?: function }} OpenTablesProps
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {OpenTablesProps} props
|
||||
*/
|
||||
export function openProductsTable({ mode, limit, callback } = {}) {
|
||||
return openTable({ table: STATIC_PRODUCTS_ID, mode, limit, callback });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OpenTablesProps} props
|
||||
*/
|
||||
export function openOrdersTable({ mode, limit, callback } = {}) {
|
||||
return openTable({ table: STATIC_ORDERS_ID, mode, limit, callback });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OpenTablesProps} props
|
||||
*/
|
||||
export function openPeopleTable({ mode, limit, callback } = {}) {
|
||||
return openTable({ table: STATIC_PEOPLE_ID, mode, limit, callback });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OpenTablesProps} props
|
||||
*/
|
||||
export function openReviewsTable({ mode, limit, callback } = {}) {
|
||||
return openTable({ table: STATIC_REVIEWS_ID, mode, limit, callback });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getFullName, modal, popover } from "e2e/support/helpers";
|
||||
import { entityPickerModal, getFullName, popover } from "e2e/support/helpers";
|
||||
|
||||
/**
|
||||
* Clicks the "+" icon on the collection page and selects one of the menu options
|
||||
@@ -75,13 +75,12 @@ export const moveOpenedCollectionTo = newParent => {
|
||||
openCollectionMenu();
|
||||
popover().within(() => cy.findByText("Move").click());
|
||||
|
||||
cy.findAllByTestId("item-picker-item").contains(newParent).click();
|
||||
|
||||
modal().within(() => {
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText(newParent).click();
|
||||
cy.button("Move").click();
|
||||
});
|
||||
// Make sure modal closed
|
||||
modal().should("not.exist");
|
||||
|
||||
entityPickerModal().should("not.exist");
|
||||
};
|
||||
|
||||
export function pickEntity({ path, select }) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const commandPalette = () => cy.findByTestId("command-palette");
|
||||
export const openCommandPalette = () => cy.get("body").type("{ctrl+k}");
|
||||
export const openCommandPalette = () => cy.get("body").type("{ctrl+k}{cmd+k}");
|
||||
export const closeCommandPalette = () => cy.get("body").type("{esc}");
|
||||
export const pressPageUp = () => cy.get("body").type("{pageup}");
|
||||
export const pressPageDown = () => cy.get("body").type("{pagedown}");
|
||||
export const pressHome = () => cy.get("body").type("{home}");
|
||||
export const pressEnd = () => cy.get("body").type("{end}");
|
||||
export const commandPaletteSearch = () =>
|
||||
cy.findByPlaceholderText("Jump to...");
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export function expressionEditorWidget() {
|
||||
return cy.findByTestId("expression-editor");
|
||||
}
|
||||
|
||||
export function enterCustomColumnDetails({ formula, name } = {}) {
|
||||
cy.get(".ace_text-input")
|
||||
.first()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { visitDashboard } from "./e2e-misc-helpers";
|
||||
import { menu, popover } from "./e2e-ui-elements-helpers";
|
||||
import { menu, popover, sidebar } from "./e2e-ui-elements-helpers";
|
||||
|
||||
// Metabase utility functions for commonly-used patterns
|
||||
export function selectDashboardFilter(selection, filterName) {
|
||||
@@ -124,18 +124,21 @@ export function checkFilterLabelAndValue(label, value) {
|
||||
cy.get("fieldset").contains(value);
|
||||
}
|
||||
|
||||
export function setFilter(type, subType) {
|
||||
export function setFilter(type, subType, name) {
|
||||
cy.icon("filter").click();
|
||||
|
||||
cy.findByText("What do you want to filter?");
|
||||
|
||||
popover().within(() => {
|
||||
cy.findByText(type).click();
|
||||
popover().findByText(type).click();
|
||||
|
||||
if (subType) {
|
||||
cy.findByText(subType).click();
|
||||
}
|
||||
});
|
||||
if (subType) {
|
||||
sidebar().findByText("Filter operator").next().click();
|
||||
popover().findByText(subType).click();
|
||||
}
|
||||
|
||||
if (name) {
|
||||
sidebar().findByLabelText("Label").clear().type(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequiredToggle() {
|
||||
|
||||
@@ -29,6 +29,7 @@ export function downloadAndAssert(
|
||||
downloadUrl,
|
||||
isEmbed,
|
||||
isDashboard,
|
||||
enableFormatting = true,
|
||||
} = {},
|
||||
callback,
|
||||
) {
|
||||
@@ -76,6 +77,9 @@ export function downloadAndAssert(
|
||||
cy.findByTestId("download-button").click();
|
||||
}
|
||||
// Initiate the file download
|
||||
if (!enableFormatting) {
|
||||
cy.window().trigger("keydown", { key: "Alt" });
|
||||
}
|
||||
popover().findByText(`.${fileType}`).click();
|
||||
|
||||
cy.wait("@fileDownload")
|
||||
@@ -110,15 +114,15 @@ export function assertSheetRowsCount(expectedCount) {
|
||||
|
||||
function getEndpoint(fileType, questionId, publicUid, dashcardId, dashboardId) {
|
||||
if (dashcardId != null && dashboardId != null) {
|
||||
return `api/dashboard/${dashboardId}/dashcard/${dashcardId}/card/${questionId}/query/${fileType}`;
|
||||
return `api/dashboard/${dashboardId}/dashcard/${dashcardId}/card/${questionId}/query/${fileType}**`;
|
||||
}
|
||||
|
||||
if (publicUid) {
|
||||
return `/public/question/${publicUid}.${fileType}**`;
|
||||
}
|
||||
|
||||
const questionEndpoint = `/api/card/${questionId}/query/${fileType}`;
|
||||
const queryEndpoint = `/api/dataset/${fileType}`;
|
||||
const questionEndpoint = `/api/card/${questionId}/query/${fileType}**`;
|
||||
const queryEndpoint = `/api/dataset/${fileType}**`;
|
||||
|
||||
return questionId ? questionEndpoint : queryEndpoint;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ export function createPublicDashboardLink(dashboardId) {
|
||||
return cy.request("POST", `/api/dashboard/${dashboardId}/public_link`, {});
|
||||
}
|
||||
|
||||
export const visitFullAppEmbeddingUrl = ({ url, qs }) => {
|
||||
export const visitFullAppEmbeddingUrl = ({ url, qs, onBeforeLoad }) => {
|
||||
cy.visit({
|
||||
url,
|
||||
qs,
|
||||
@@ -270,6 +270,7 @@ export const visitFullAppEmbeddingUrl = ({ url, qs }) => {
|
||||
// cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests
|
||||
// by removing the property the app would work in embedding mode
|
||||
window.Cypress = undefined;
|
||||
onBeforeLoad?.(window);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -65,3 +65,19 @@ export const setTokenFeatures = featuresScope => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteToken = () => {
|
||||
if (!isEE) {
|
||||
throw new Error(
|
||||
"You must run Metabase® Enterprise Edition™ for token to make sense.\nMake sure you have `MB_EDITION=ee` in your environment variables.",
|
||||
);
|
||||
}
|
||||
return cy.request({
|
||||
method: "PUT",
|
||||
url: "/api/setting/premium-embedding-token",
|
||||
failOnStatusCode: false,
|
||||
body: {
|
||||
value: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { modal, popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
|
||||
import {
|
||||
entityPickerModal,
|
||||
modal,
|
||||
popover,
|
||||
} from "e2e/support/helpers/e2e-ui-elements-helpers";
|
||||
|
||||
export function setDropdownFilterType() {
|
||||
cy.findByText("Dropdown list").click();
|
||||
@@ -16,10 +20,8 @@ export function setFilterQuestionSource({ question, field }) {
|
||||
cy.findByText("Pick a model or question…").click();
|
||||
});
|
||||
|
||||
modal().within(() => {
|
||||
cy.findByPlaceholderText(/Search for a question/).type(question);
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText(question).click();
|
||||
cy.button("Done").click();
|
||||
});
|
||||
|
||||
modal().within(() => {
|
||||
|
||||
@@ -99,28 +99,29 @@ export function interceptPromise(method, path) {
|
||||
* cy.visit(`/dashboard/1`);
|
||||
* });
|
||||
*/
|
||||
const cypressWaitAllRecursive = (results, currentCommand, commands) => {
|
||||
return currentCommand.then(result => {
|
||||
const cypressWaitAllRecursive = (results, commands) => {
|
||||
const [nextCommand, ...restCommands] = commands;
|
||||
if (!nextCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
return nextCommand.then(result => {
|
||||
results.push(result);
|
||||
|
||||
const [nextCommand, ...rest] = Array.from(commands);
|
||||
|
||||
if (nextCommand == null) {
|
||||
return results;
|
||||
return;
|
||||
}
|
||||
|
||||
return cypressWaitAllRecursive(results, nextCommand, rest);
|
||||
return cypressWaitAllRecursive(results, restCommands);
|
||||
});
|
||||
};
|
||||
|
||||
export const cypressWaitAll = function (commands) {
|
||||
const results = [];
|
||||
|
||||
return cypressWaitAllRecursive(
|
||||
results,
|
||||
cy.wrap(null, { log: false }),
|
||||
commands,
|
||||
);
|
||||
return cy.wrap(results).then(() => {
|
||||
cypressWaitAllRecursive(results, commands);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
// various Metabase-specific "scoping" functions like inside popover/modal/navbar/main/sidebar content area
|
||||
export const POPOVER_ELEMENT =
|
||||
".popover[data-state~='visible'],[data-position]:not(.emotion-HoverCard-dropdown)";
|
||||
".popover[data-state~='visible'],[data-element-id=mantine-popover]";
|
||||
|
||||
export function popover() {
|
||||
cy.get(POPOVER_ELEMENT).should("be.visible");
|
||||
return cy.get(POPOVER_ELEMENT);
|
||||
}
|
||||
|
||||
export function mantinePopover() {
|
||||
const MANTINE_POPOVER = "[data-popover=mantine-popover]";
|
||||
return cy.get(MANTINE_POPOVER).should("be.visible");
|
||||
}
|
||||
|
||||
const HOVERCARD_ELEMENT = ".emotion-HoverCard-dropdown[role='dialog']:visible";
|
||||
|
||||
export function hovercard() {
|
||||
@@ -28,8 +23,8 @@ export function menu() {
|
||||
}
|
||||
|
||||
export function modal() {
|
||||
const LEGACY_MODAL_SELECTOR = ".Modal";
|
||||
const MODAL_SELECTOR = ".emotion-Modal-content[role='dialog']";
|
||||
const LEGACY_MODAL_SELECTOR = "[data-testid=modal]";
|
||||
return cy.get([MODAL_SELECTOR, LEGACY_MODAL_SELECTOR].join(","));
|
||||
}
|
||||
|
||||
@@ -179,7 +174,9 @@ export const moveDnDKitElement = (
|
||||
button: 0,
|
||||
})
|
||||
.wait(200)
|
||||
.trigger("pointermove", 5, 5, {
|
||||
// This initial move needs to be greater than the activation constraint
|
||||
// of the pointer sensor
|
||||
.trigger("pointermove", 20, 20, {
|
||||
force: true,
|
||||
isPrimary: true,
|
||||
button: 0,
|
||||
@@ -216,5 +213,5 @@ export const undoToastList = () => {
|
||||
};
|
||||
|
||||
export function dashboardCards() {
|
||||
return cy.get("#Dashboard-Cards-Container");
|
||||
return cy.get("[data-element-id=dashboard-cards-container]");
|
||||
}
|
||||
|
||||
@@ -102,6 +102,24 @@ export const many_data_types = async dbClient => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const uuid_pk_table = async dbClient => {
|
||||
const tableName = "uuid_pk_table";
|
||||
|
||||
await dbClient.schema.dropTableIfExists(tableName);
|
||||
|
||||
await dbClient.schema.createTable(tableName, table => {
|
||||
table.uuid("id").primary();
|
||||
table.string("name");
|
||||
});
|
||||
|
||||
await dbClient(tableName).insert([
|
||||
{ id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", name: "Duck" },
|
||||
{ id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", name: "Rabbit" },
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const composite_pk_table = async dbClient => {
|
||||
const tableName = "composite_pk_table";
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createModelFromTableName,
|
||||
queryWritableDB,
|
||||
setTokenFeatures,
|
||||
entityPickerModal,
|
||||
} from "e2e/support/helpers";
|
||||
import { getCreatePostgresRoleIfNotExistSql } from "e2e/support/test_roles";
|
||||
import { createMockActionParameter } from "metabase-types/api/mocks";
|
||||
@@ -219,7 +220,10 @@ describe(
|
||||
});
|
||||
|
||||
modal().eq(1).findByText("Select a model").click();
|
||||
popover().findByText("Order").click();
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText("Order").click();
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Create" }).click();
|
||||
|
||||
cy.get("@modelId").then(modelId => {
|
||||
@@ -239,12 +243,19 @@ describe(
|
||||
// to test database picker behavior in the action editor
|
||||
setActionsEnabledForDB(SAMPLE_DB_ID);
|
||||
|
||||
setTokenFeatures("all");
|
||||
cy.updatePermissionsGraph({
|
||||
[USER_GROUPS.ALL_USERS_GROUP]: {
|
||||
[WRITABLE_DB_ID]: { data: { schemas: "none", native: "none" } },
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "blocked",
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
[USER_GROUPS.DATA_GROUP]: {
|
||||
[WRITABLE_DB_ID]: { data: { schemas: "all", native: "write" } },
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder-and-native",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -805,7 +816,16 @@ describe(
|
||||
cy.updatePermissionsGraph(
|
||||
{
|
||||
[USER_GROUPS.ALL_USERS_GROUP]: {
|
||||
[WRITABLE_DB_ID]: { data: { schemas: "all", native: "write" } },
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "impersonated",
|
||||
"create-queries": "query-builder-and-native",
|
||||
},
|
||||
},
|
||||
// By default, all groups get `unrestricted` access that will override the impersonation.
|
||||
[USER_GROUPS.COLLECTION_GROUP]: {
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "blocked",
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
|
||||
@@ -37,7 +37,7 @@ const normalUserName = getFullName(normal);
|
||||
describe("scenarios > admin > people", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("GET", "/api/permissions/group").as("getGroups");
|
||||
cy.intercept("GET", "/api/api-key").as("getApiKeys");
|
||||
cy.intercept("GET", "/api/api-key").as("listApiKeys");
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
@@ -335,7 +335,7 @@ describe("scenarios > admin > people", () => {
|
||||
cy.intercept("DELETE", "/api/permissions/group/*").as("deleteGroup");
|
||||
|
||||
cy.visit("/admin/people/groups");
|
||||
cy.wait(["@getGroups", "@getApiKeys"]);
|
||||
cy.wait(["@getGroups", "@listApiKeys"]);
|
||||
|
||||
cy.findByTestId("admin-panel").within(() => {
|
||||
cy.button("Create a group").click();
|
||||
@@ -358,7 +358,7 @@ describe("scenarios > admin > people", () => {
|
||||
it("should display api keys included in a group and display a warning when deleting the group", () => {
|
||||
createApiKey("MyApiKey", COLLECTION_GROUP_ID);
|
||||
cy.visit("/admin/people/groups");
|
||||
cy.wait(["@getGroups", "@getApiKeys"]);
|
||||
cy.wait(["@getGroups", "@listApiKeys"]);
|
||||
|
||||
cy.findByTestId("admin-panel")
|
||||
.findByText("collection")
|
||||
|
||||
@@ -52,7 +52,7 @@ describeEE("scenarios > admin > settings > SSO > JWT", () => {
|
||||
setupJwt();
|
||||
cy.visit("/admin/settings/authentication/jwt");
|
||||
|
||||
getUserProvisioningInput().click();
|
||||
getUserProvisioningInput().label.click();
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateSettings");
|
||||
|
||||
|
||||
@@ -65,15 +65,13 @@ describe(
|
||||
getLdapCard().findByText("Active").should("exist");
|
||||
});
|
||||
|
||||
it("should allow the user to enable/disable user provisioning", () => {
|
||||
it("should not show the user provision UI to OSS users", () => {
|
||||
setupLdap();
|
||||
cy.visit("/admin/settings/authentication/ldap");
|
||||
|
||||
getUserProvisioningInput().click();
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateLdapSettings");
|
||||
|
||||
getSuccessUi().should("exist");
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("User Provisioning")
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
it("should allow to reset ldap settings", () => {
|
||||
@@ -172,6 +170,20 @@ describeEE(
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
setTokenFeatures("all");
|
||||
cy.intercept("PUT", "/api/ldap/settings").as("updateLdapSettings");
|
||||
});
|
||||
|
||||
it("should allow the user to enable/disable user provisioning", () => {
|
||||
setupLdap();
|
||||
cy.visit("/admin/settings/authentication/ldap");
|
||||
|
||||
const { label, input } = getUserProvisioningInput();
|
||||
input.should("be.checked");
|
||||
label.click();
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateLdapSettings");
|
||||
|
||||
getSuccessUi().should("exist");
|
||||
});
|
||||
|
||||
it("should show the login form when ldap is enabled but password login isn't (metabase#25661)", () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ describeEE("scenarios > admin > settings > SSO > SAML", () => {
|
||||
setupSaml();
|
||||
cy.visit("/admin/settings/authentication/saml");
|
||||
|
||||
getUserProvisioningInput().click();
|
||||
getUserProvisioningInput().label.click();
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateSamlSettings");
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ const addGroupsToMapping = (mappingName, groups) => {
|
||||
cy.findByText(group).click();
|
||||
|
||||
cy.findByText(group)
|
||||
.closest(".List-section")
|
||||
.closest("[data-element-id=list-section]")
|
||||
.within(() => {
|
||||
cy.icon("check");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export function getUserProvisioningInput() {
|
||||
return cy
|
||||
.findByTestId("admin-layout-content")
|
||||
.findByText("User Provisioning");
|
||||
return {
|
||||
label: cy
|
||||
.findByTestId("admin-layout-content")
|
||||
.findByText("User Provisioning"),
|
||||
input: cy
|
||||
.findByTestId("admin-layout-content")
|
||||
.findByLabelText("User Provisioning"),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSuccessUi() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
setTokenFeatures,
|
||||
appBar,
|
||||
main,
|
||||
modal,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const TOOLS_ERRORS_URL = "/admin/tools/errors";
|
||||
@@ -157,7 +158,7 @@ function fixQuestion(name) {
|
||||
|
||||
cy.findByText("Save").click();
|
||||
|
||||
cy.get(".Modal").within(() => {
|
||||
modal().within(() => {
|
||||
cy.button("Save").click();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import {
|
||||
appBar,
|
||||
describeEE,
|
||||
entityPickerModal,
|
||||
main,
|
||||
modal,
|
||||
popover,
|
||||
restore,
|
||||
setTokenFeatures,
|
||||
undoToast,
|
||||
visitDashboard,
|
||||
visitQuestion,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
function checkFavicon(url) {
|
||||
@@ -17,10 +21,12 @@ function checkFavicon(url) {
|
||||
|
||||
function checkLogo() {
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(logo_data => {
|
||||
cy.get(`img[src="data:image/jpeg;base64,${logo_data}"]`);
|
||||
cy.get(`img[src="data:image/jpeg;base64,${logo_data}"]`).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
describeEE("formatting > whitelabel", () => {
|
||||
beforeEach(() => {
|
||||
restore();
|
||||
@@ -60,73 +66,424 @@ describeEE("formatting > whitelabel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("company logo", () => {
|
||||
beforeEach(() => {
|
||||
cy.log("Add a logo");
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(logo_data => {
|
||||
cy.request("PUT", "/api/setting/application-logo-url", {
|
||||
value: `data:image/jpeg;base64,${logo_data}`,
|
||||
describe("image uploads", () => {
|
||||
describe("company logo", () => {
|
||||
beforeEach(() => {
|
||||
cy.log("Add a logo");
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
logo_data => {
|
||||
cy.request("PUT", "/api/setting/application-logo-url", {
|
||||
value: `data:image/jpeg;base64,${logo_data}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("changes should reflect on admin's dashboard", () => {
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
|
||||
it("changes should reflect while signed out", () => {
|
||||
cy.signOut();
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
|
||||
it("changes should reflect on user's dashboard", () => {
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
});
|
||||
|
||||
describe("favicon", () => {
|
||||
it("should work for people that set favicon URL before we change the input to file input", () => {
|
||||
const faviconUrl =
|
||||
"https://cdn.ecosia.org/assets/images/ico/favicon.ico";
|
||||
cy.request("PUT", "/api/setting/application-favicon-url", {
|
||||
value: faviconUrl,
|
||||
});
|
||||
checkFavicon(faviconUrl);
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
cy.get('head link[rel="icon"]')
|
||||
.get('[href="https://cdn.ecosia.org/assets/images/ico/favicon.ico"]')
|
||||
.should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should show up in user's HTML", () => {
|
||||
cy.visit("/admin/settings/whitelabel");
|
||||
cy.log("Add favicon");
|
||||
|
||||
cy.findByLabelText("Favicon").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/favicon.ico",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
cy.readFile("e2e/support/assets/favicon.ico", "base64").then(
|
||||
base64Url => {
|
||||
const faviconUrl = `data:image/jpeg;base64,${base64Url}`;
|
||||
cy.wrap(faviconUrl).as("faviconUrl");
|
||||
checkFavicon(faviconUrl);
|
||||
},
|
||||
);
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
cy.get("@faviconUrl").then(faviconUrl => {
|
||||
cy.get('head link[rel="icon"]')
|
||||
.get(`[href="${faviconUrl}"]`)
|
||||
.should("have.length", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("changes should reflect on admin's dashboard", () => {
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
describe("custom illustrations", () => {
|
||||
describe("login page illustration", () => {
|
||||
it("should only allow uploading a valid image files (PNG, JPG, SVG) and display on login page", () => {
|
||||
/**
|
||||
* Unfortunately, we couldn't test the browser file selector with Cypress yet.
|
||||
* With `input.selectFile`, we still can select any files unlike the browser file selector
|
||||
* which would respect the `accept` attribute (which specifies the accepted MIME types).
|
||||
*/
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
it("changes should reflect while signed out", () => {
|
||||
cy.signOut();
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
cy.log("test error message for file size > 2MB");
|
||||
cy.findByRole("searchbox", {
|
||||
name: "Login and unsubscribe pages",
|
||||
}).click();
|
||||
popover().findByText("Custom").click();
|
||||
/**
|
||||
* Clicking "Choose File" doesn't actually open the file browser on Cypress,
|
||||
* so I need to use `selectFile` with the file input instead.
|
||||
*/
|
||||
cy.findByTestId("login-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from("a".repeat(2 * MB + 1)),
|
||||
fileName: "big-file.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText(
|
||||
"The image you chose is larger than 2MB. Please choose another one.",
|
||||
).should("be.visible");
|
||||
cy.findByText("big-file.jpg").should("not.exist");
|
||||
});
|
||||
cy.findByRole("searchbox", {
|
||||
name: "Login and unsubscribe pages",
|
||||
}).click();
|
||||
popover().findByText("Custom").click();
|
||||
cy.log("test uploading a corrupted file");
|
||||
cy.findByTestId("login-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from("a".repeat(2 * MB)),
|
||||
fileName: "corrupted-file.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText(
|
||||
"The image you chose is corrupted. Please choose another one.",
|
||||
)
|
||||
.scrollIntoView()
|
||||
.should("be.visible");
|
||||
cy.findByText("corrupted-file.jpg").should("not.exist");
|
||||
});
|
||||
|
||||
it("changes should reflect on user's dashboard", () => {
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
checkLogo();
|
||||
});
|
||||
});
|
||||
cy.log('test replacing the "corrupted" file with a valid one');
|
||||
cy.findByTestId("login-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
|
||||
describe("favicon", () => {
|
||||
it("should work for people that set favicon URL before we change the input to file input", () => {
|
||||
const faviconUrl = "https://cdn.ecosia.org/assets/images/ico/favicon.ico";
|
||||
cy.request("PUT", "/api/setting/application-favicon-url", {
|
||||
value: faviconUrl,
|
||||
cy.findByText(
|
||||
"The image you chose is corrupted. Please choose another one.",
|
||||
).should("not.exist");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
undoToast().icon("close").click();
|
||||
|
||||
cy.log("test removing the custom illustration");
|
||||
cy.findByTestId("login-page-illustration-setting").within(() => {
|
||||
cy.button("Remove custom illustration").click();
|
||||
cy.log(
|
||||
"the default option should be selected once removing the custom illustration",
|
||||
);
|
||||
cy.findByDisplayValue("Lighthouse").should("be.visible");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
undoToast().icon("close").click();
|
||||
|
||||
cy.log("test uploading a valid image file");
|
||||
cy.findByTestId("login-page-illustration-setting")
|
||||
.findByRole("searchbox", { name: "Login and unsubscribe pages" })
|
||||
.click();
|
||||
popover().findByText("Custom").click();
|
||||
cy.findByTestId("login-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
logo_data => {
|
||||
const backgroundImage = `url("data:image/jpeg;base64,${logo_data}")`;
|
||||
cy.signOut();
|
||||
cy.visit("/");
|
||||
cy.findByTestId("login-page-illustration").should(
|
||||
"have.css",
|
||||
"background-image",
|
||||
backgroundImage,
|
||||
);
|
||||
|
||||
cy.visit("/unsubscribe?hash=hash&email=email&pulse-id=pulse-id");
|
||||
cy.findByTestId("unsubscribe-page-illustration").should(
|
||||
"have.css",
|
||||
"background-image",
|
||||
backgroundImage,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cy.log("test no illustration");
|
||||
cy.signInAsAdmin();
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByRole("searchbox", {
|
||||
name: "Login and unsubscribe pages",
|
||||
}).click();
|
||||
popover().findByText("No illustration").click();
|
||||
|
||||
cy.signOut();
|
||||
cy.visit("/");
|
||||
cy.findByTestId("login-page-illustration").should("not.exist");
|
||||
|
||||
cy.visit("/unsubscribe?hash=hash&email=email&pulse-id=pulse-id");
|
||||
cy.findByTestId("unsubscribe-page-illustration").should("not.exist");
|
||||
});
|
||||
});
|
||||
checkFavicon(faviconUrl);
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
cy.get('head link[rel="icon"]')
|
||||
.get('[href="https://cdn.ecosia.org/assets/images/ico/favicon.ico"]')
|
||||
.should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should show up in user's HTML", () => {
|
||||
cy.visit("/admin/settings/whitelabel");
|
||||
cy.log("Add favicon");
|
||||
describe("landing page illustration", () => {
|
||||
it("should allow display the selected illustration on the landing page", () => {
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByLabelText("Favicon").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/favicon.ico",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
cy.readFile("e2e/support/assets/favicon.ico", "base64").then(
|
||||
base64Url => {
|
||||
const faviconUrl = `data:image/jpeg;base64,${base64Url}`;
|
||||
cy.wrap(faviconUrl).as("faviconUrl");
|
||||
checkFavicon(faviconUrl);
|
||||
},
|
||||
);
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
cy.get("@faviconUrl").then(faviconUrl => {
|
||||
cy.get('head link[rel="icon"]')
|
||||
.get(`[href="${faviconUrl}"]`)
|
||||
.should("have.length", 1);
|
||||
cy.findByRole("searchbox", { name: "Landing page" }).should(
|
||||
"have.value",
|
||||
"Lighthouse",
|
||||
);
|
||||
|
||||
cy.findByRole("searchbox", { name: "Landing page" }).click();
|
||||
popover().findByText("Custom").click();
|
||||
|
||||
cy.findByTestId("landing-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
logo_data => {
|
||||
const backgroundImage = `url("data:image/jpeg;base64,${logo_data}")`;
|
||||
cy.visit("/");
|
||||
cy.findByTestId("landing-page-illustration").should(
|
||||
"have.css",
|
||||
"background-image",
|
||||
backgroundImage,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cy.log("test no illustration");
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByLabelText("Landing page").click();
|
||||
popover().findByText("No illustration").click();
|
||||
|
||||
cy.visit("/");
|
||||
cy.findByTestId("landing-page-illustration").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("no data illustration", () => {
|
||||
it("should allow display the selected illustration at relevant places", () => {
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When calculations return no results",
|
||||
}).should("have.value", "Sailboat");
|
||||
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When calculations return no results",
|
||||
}).click();
|
||||
popover().findByText("Custom").click();
|
||||
|
||||
cy.findByTestId("no-data-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
|
||||
cy.createDashboardWithQuestions({
|
||||
dashboardName: "No results dashboard",
|
||||
questions: [
|
||||
{
|
||||
name: "No results question",
|
||||
native: {
|
||||
query: "select * from products where id = 999999999",
|
||||
},
|
||||
},
|
||||
],
|
||||
}).then(({ dashboard, questions }) => {
|
||||
cy.wrap(dashboard.id).as("dashboardId");
|
||||
cy.wrap(questions[0].id).as("questionId");
|
||||
});
|
||||
|
||||
cy.log("test custom illustration");
|
||||
|
||||
visitDashboard("@dashboardId");
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
logo_data => {
|
||||
const imageDataUrl = `data:image/jpeg;base64,${logo_data}`;
|
||||
cy.wrap(imageDataUrl).as("imageDataUrl");
|
||||
cy.findByAltText("No results").should(
|
||||
"have.attr",
|
||||
"src",
|
||||
imageDataUrl,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
visitQuestion("@questionId");
|
||||
cy.get("@imageDataUrl").then(imageDataUrl => {
|
||||
cy.findByAltText("No results").should(
|
||||
"have.attr",
|
||||
"src",
|
||||
imageDataUrl,
|
||||
);
|
||||
});
|
||||
|
||||
cy.log("test no illustration");
|
||||
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When calculations return no results",
|
||||
}).click();
|
||||
popover().findByText("No illustration").click();
|
||||
|
||||
visitDashboard("@dashboardId");
|
||||
cy.findByAltText("No results").should("not.exist");
|
||||
|
||||
visitQuestion("@questionId");
|
||||
cy.findByAltText("No results").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("no object illustration", () => {
|
||||
it("should allow display the selected illustration at relevant places", () => {
|
||||
const emptyCollectionName = "Empty Collection";
|
||||
cy.request("POST", "/api/collection", { name: emptyCollectionName });
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When no objects can be found",
|
||||
}).should("have.value", "Sailboat");
|
||||
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When no objects can be found",
|
||||
}).click();
|
||||
popover().findByText("Custom").click();
|
||||
|
||||
cy.findByTestId("no-object-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
});
|
||||
undoToast().findByText("Changes saved").should("be.visible");
|
||||
|
||||
cy.log("test custom illustration");
|
||||
|
||||
cy.findByRole("navigation").findByText("Exit admin").click();
|
||||
appBar().findByText("New").click();
|
||||
popover().findByText("Dashboard").click();
|
||||
modal().findByTestId("collection-picker-button").click();
|
||||
entityPickerModal().within(() => {
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
logo_data => {
|
||||
const imageDataUrl = `data:image/jpeg;base64,${logo_data}`;
|
||||
cy.wrap(imageDataUrl).as("imageDataUrl");
|
||||
},
|
||||
);
|
||||
|
||||
cy.log("test search not found illustration");
|
||||
cy.findByPlaceholderText("Search…").type(
|
||||
"This aren't the objects you're looking for",
|
||||
);
|
||||
cy.get("@imageDataUrl").then(imageDataUrl => {
|
||||
cy.findByAltText("No results").should(
|
||||
"have.attr",
|
||||
"src",
|
||||
imageDataUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cy.log("test no illustration");
|
||||
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
cy.findByRole("searchbox", {
|
||||
name: "When no objects can be found",
|
||||
}).click();
|
||||
popover().findByText("No illustration").click();
|
||||
|
||||
cy.findByRole("navigation").findByText("Exit admin").click();
|
||||
appBar().findByText("New").click();
|
||||
popover().findByText("Dashboard").click();
|
||||
modal().findByTestId("collection-picker-button").click();
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText(emptyCollectionName).click();
|
||||
cy.findByAltText("No results").should("not.exist");
|
||||
|
||||
cy.log("test search not found illustration");
|
||||
cy.findByPlaceholderText("Search…").type(
|
||||
"This aren't the objects you're looking for",
|
||||
);
|
||||
cy.findByAltText("No results").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import {
|
||||
appBar,
|
||||
describeWithSnowplow,
|
||||
enableTracking,
|
||||
expectGoodSnowplowEvents,
|
||||
expectNoBadSnowplowEvents,
|
||||
resetSnowplow,
|
||||
restore,
|
||||
rightSidebar,
|
||||
visitDashboard,
|
||||
describeEE,
|
||||
setTokenFeatures,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
describeEE("database prompt banner", () => {
|
||||
beforeEach(() => {
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
setTokenFeatures("all");
|
||||
});
|
||||
|
||||
it("should show a database prompt banner when logged in as an admin, an instance is on a paid plan, only have a single sample dataset, and is not white labelled", () => {
|
||||
cy.visit("/");
|
||||
cy.findByRole("main").findByText("Loading...").should("not.exist");
|
||||
|
||||
cy.findAllByRole("banner")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText(
|
||||
"Connect to your database to get the most from Metabase.",
|
||||
).should("exist");
|
||||
|
||||
cy.findByRole("link", { name: "Get help connecting" })
|
||||
.should("have.attr", "href")
|
||||
.and(
|
||||
"eq",
|
||||
"https://metabase.com/help/connect?email=admin%40metabase.test&site_url=http%3A%2F%2Flocalhost%3A4000",
|
||||
);
|
||||
|
||||
cy.findByRole("link", { name: "Connect your database" }).click();
|
||||
cy.url().should("include", "/admin/databases/create");
|
||||
});
|
||||
|
||||
// Assert that database form is rendered
|
||||
cy.findByRole("main").within(() => {
|
||||
cy.findByText("Add Database").should("exist");
|
||||
cy.findByLabelText("Database type").should("exist");
|
||||
cy.findByLabelText("Database name").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show a database prompt banner when logged in as an admin, an instance is on a paid plan, and only have a single sample dataset, but is white labelled", () => {
|
||||
cy.request("PUT", "/api/setting/application-name", { value: "Acme Corp." });
|
||||
cy.visit("/");
|
||||
cy.findByRole("main").findByText("Loading...").should("not.exist");
|
||||
|
||||
cy.findAllByRole("banner")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText(
|
||||
"Connect to your database to get the most from Metabase.",
|
||||
).should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
// Until we enable multi-browser support, this repro will be skipped by Cypress in CI
|
||||
// Issue was specific to Firefox only - it is still possible to test it locally
|
||||
it(
|
||||
"should show info sidebar correctly on Firefox",
|
||||
{ browser: "firefox" },
|
||||
function () {
|
||||
visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
cy.findByRole("main").findByText("Loading...").should("not.exist");
|
||||
cy.findByRole("main").icon("info").click();
|
||||
|
||||
rightSidebar().within(() => {
|
||||
cy.findByRole("heading", { name: "About" }).should("be.visible");
|
||||
cy.findByRole("heading", { name: "History" }).should("be.visible");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("embeddings", () => {
|
||||
// Public and signed embeds are tested in `PublicQuestion.unit.spec.tsx`
|
||||
|
||||
describe("full-app embeddings", () => {
|
||||
it("should render database prompt banner when logged in as an admin, an instance is on a paid plan, only have a single sample dataset, and is not white labeling", () => {
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: "/",
|
||||
qs: { side_nav: false, logo: false },
|
||||
});
|
||||
|
||||
cy.findByRole("link", { name: "Metabase tips" }).should("exist");
|
||||
|
||||
// Test that we're in full-app embedding since parameters are working.
|
||||
appBar().should("not.exist");
|
||||
|
||||
cy.findAllByRole("banner")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText(
|
||||
"Connect to your database to get the most from Metabase.",
|
||||
).should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render for any other condition", () => {
|
||||
// Adding a second database should prevent the database prompt
|
||||
cy.addSQLiteDatabase();
|
||||
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: "/",
|
||||
qs: { side_nav: false, logo: false },
|
||||
});
|
||||
|
||||
cy.findByRole("link", { name: "Metabase tips" }).should("exist");
|
||||
|
||||
// Test that we're in full-app embedding since parameters are working.
|
||||
appBar().should("not.exist");
|
||||
|
||||
// Since there wouldn't be a database prompt banner, we can assert that there is no banner at all
|
||||
cy.findAllByRole("banner").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describeWithSnowplow("database prompt banner", () => {
|
||||
const PAGE_VIEW_EVENT = 1;
|
||||
|
||||
beforeEach(() => {
|
||||
restore();
|
||||
resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
setTokenFeatures("all");
|
||||
enableTracking();
|
||||
cy.visit("/");
|
||||
cy.findByRole("main").findByText("Loading...").should("not.exist");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("should send snowplow events when clicking on links in the database prompt banner", () => {
|
||||
expectNoBadSnowplowEvents();
|
||||
expectGoodSnowplowEvents(PAGE_VIEW_EVENT);
|
||||
cy.findAllByRole("banner")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole("link", { name: "Get help connecting" }).click();
|
||||
expectGoodSnowplowEvents(PAGE_VIEW_EVENT + 1);
|
||||
|
||||
cy.findByRole("link", { name: "Connect your database" }).click();
|
||||
// clicking this link also brings us to the admin page causing a new page_view event
|
||||
expectGoodSnowplowEvents(2 * PAGE_VIEW_EVENT + 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const visitFullAppEmbeddingUrl = ({ url, qs }) => {
|
||||
cy.visit({
|
||||
url,
|
||||
qs,
|
||||
onBeforeLoad(window) {
|
||||
// cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests
|
||||
// by removing the property the app would work in embedding mode
|
||||
window.Cypress = undefined;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -297,6 +297,7 @@ describe("scenarios > admin > databases > sample database", () => {
|
||||
cy.findByText("Browse data").click();
|
||||
});
|
||||
|
||||
cy.findByRole("tab", { name: "Databases" }).click();
|
||||
cy.findByTestId("database-browser").within(() => {
|
||||
cy.findByText("Sample Database").should("exist");
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
startNewQuestion,
|
||||
setTokenFeatures,
|
||||
openTable,
|
||||
moveDnDKitElement,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const { ORDERS, ORDERS_ID, PRODUCTS_ID, REVIEWS, REVIEWS_ID, PEOPLE_ID } =
|
||||
@@ -283,7 +284,10 @@ describe("scenarios > admin > datamodel > editor", () => {
|
||||
|
||||
it("should allow sorting fields in the custom order", () => {
|
||||
visitTableMetadata({ tableId: PRODUCTS_ID });
|
||||
moveField(0, 200);
|
||||
//moveField(0, 200);
|
||||
moveDnDKitElement(cy.findAllByTestId("grabber").first(), {
|
||||
vertical: 200,
|
||||
});
|
||||
cy.wait("@updateFieldOrder");
|
||||
openProductsTable();
|
||||
assertTableHeader([
|
||||
@@ -658,28 +662,6 @@ const getFieldSection = fieldName => {
|
||||
return cy.findByLabelText(fieldName);
|
||||
};
|
||||
|
||||
const moveField = (fieldIndex, deltaY) => {
|
||||
cy.findAllByTestId("grabber")
|
||||
.eq(fieldIndex)
|
||||
.trigger("pointerdown", 0, 0, { force: true, button: 0, isPrimary: true })
|
||||
.wait(200)
|
||||
.trigger("pointermove", 5, 5, { force: true, button: 0, isPrimary: true })
|
||||
.wait(200)
|
||||
//cy.get("#ColumnsList")
|
||||
.trigger("pointermove", 10, deltaY, {
|
||||
force: true,
|
||||
button: 0,
|
||||
isPrimary: true,
|
||||
})
|
||||
.wait(200)
|
||||
.trigger("pointerup", 10, deltaY, {
|
||||
force: true,
|
||||
button: 0,
|
||||
isPrimary: true,
|
||||
})
|
||||
.wait(200);
|
||||
};
|
||||
|
||||
const setTableOrder = order => {
|
||||
cy.findByLabelText("Sort").click();
|
||||
popover().findByText(order).click();
|
||||
|
||||
@@ -166,7 +166,10 @@ describe("scenarios > admin > datamodel > field", () => {
|
||||
|
||||
cy.log("Make sure custom mapping appears in QB");
|
||||
openTable({ database: dbId, table: NUMBER_WITH_NULLS_ID });
|
||||
cy.get(".cellData").should("contain", remappedNullValue);
|
||||
cy.get("[data-testid=cell-data]").should(
|
||||
"contain",
|
||||
remappedNullValue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -127,8 +127,10 @@ describe("scenarios > admin > datamodel > metadata", () => {
|
||||
cy.findByText("Count of rows").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Pick a column to group by").click();
|
||||
cy.get(".List-section-header").contains("Created At").click();
|
||||
cy.get(".List-section--expanded .List-item-title")
|
||||
cy.get("[data-element-id=list-section-header]")
|
||||
.contains("Created At")
|
||||
.click();
|
||||
cy.get("[data-element-id=list-section] [data-element-id=list-item-title]")
|
||||
.contains("Created At")
|
||||
.should("have.length", 1);
|
||||
});
|
||||
@@ -149,10 +151,16 @@ describe("scenarios > admin > datamodel > metadata", () => {
|
||||
cy.findAllByTestId("dimension-list-item")
|
||||
.eq(3)
|
||||
.should("have.text", "Rating");
|
||||
cy.get(".List-section-header").should("have.length", 3);
|
||||
cy.get(".List-section-header").eq(0).should("have.text", "Review");
|
||||
cy.get(".List-section-header").eq(1).should("have.text", "Product");
|
||||
cy.get(".List-section-header").eq(2).should("have.text", "Rating");
|
||||
cy.get("[data-element-id=list-section-header]").should("have.length", 3);
|
||||
cy.get("[data-element-id=list-section-header]")
|
||||
.eq(0)
|
||||
.should("have.text", "Review");
|
||||
cy.get("[data-element-id=list-section-header]")
|
||||
.eq(1)
|
||||
.should("have.text", "Product");
|
||||
cy.get("[data-element-id=list-section-header]")
|
||||
.eq(2)
|
||||
.should("have.text", "Rating");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -64,14 +64,20 @@ describe("scenarios > admin > datamodel > metrics", () => {
|
||||
.first()
|
||||
.as("tableHeader")
|
||||
.within(() => {
|
||||
cy.get(".cellData").eq(1).invoke("text").should("eq", "Revenue");
|
||||
cy.get("[data-testid=cell-data]")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should("eq", "Revenue");
|
||||
});
|
||||
|
||||
cy.get("@table")
|
||||
.last()
|
||||
.as("tableBody")
|
||||
.within(() => {
|
||||
cy.get(".cellData").eq(1).invoke("text").should("eq", "50,072.98");
|
||||
cy.get("[data-testid=cell-data]")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should("eq", "50,072.98");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +270,7 @@ describe("scenarios > admin > datamodel > metrics", () => {
|
||||
it("should save the metric using custom expressions (metabase#13022)", () => {
|
||||
createMetric({
|
||||
name: "13022_Metric",
|
||||
desription: "desc",
|
||||
description: "desc",
|
||||
table_id: ORDERS_ID,
|
||||
definition: {
|
||||
"source-table": ORDERS_ID,
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("issue 17768", () => {
|
||||
|
||||
popover().within(() => {
|
||||
cy.findByText("ID")
|
||||
.closest(".List-section")
|
||||
.closest("[data-element-id=list-section]")
|
||||
.realHover()
|
||||
.contains("Auto bin")
|
||||
.should("not.exist");
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { restore, openNativeEditor, runNativeQuery } from "e2e/support/helpers";
|
||||
import {
|
||||
restore,
|
||||
openNativeEditor,
|
||||
runNativeQuery,
|
||||
modal,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const nativeQuery = "select (random() * random() * random()), pg_sleep(2)";
|
||||
|
||||
@@ -85,7 +90,7 @@ function saveQuestion(name) {
|
||||
|
||||
cy.findByLabelText("Name").type(name);
|
||||
|
||||
cy.get(".Modal").button("Save").click();
|
||||
modal().button("Save").click();
|
||||
|
||||
cy.findByText("Not now").click();
|
||||
|
||||
@@ -93,7 +98,7 @@ function saveQuestion(name) {
|
||||
}
|
||||
|
||||
function getCellText() {
|
||||
return cy.get(".cellData").eq(-1).invoke("text");
|
||||
return cy.get("[data-testid=cell-data]").eq(-1).invoke("text");
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
|
||||