Compare commits

..

40 Commits

Author SHA1 Message Date
Nathan Voxland
2881136730 Only update search index if instance was actually updated 2025-05-01 10:05:57 -05:00
Nathan Voxland
10cfe891bb Only update search index if instance was actually updated 2025-04-30 16:58:01 -05:00
Nathan Voxland
8c80c8107f Only update search index if instance was actually updated 2025-04-30 16:39:07 -05:00
bryan
90b9fea551 fix docs bump workflow (#57454) 2025-04-30 20:47:26 +00:00
bryan
8574e882e9 Remove non-doc branches before sending workflow trigger (#57446) 2025-04-30 15:50:42 -04:00
lbrdnk
609807dc33 Fix sorting by aggregation expression with cumulative function works (#57292) 2025-04-30 20:24:30 +02:00
Alex Yarosh
698012db51 Update countif.md (#57373) 2025-04-30 14:12:24 -04:00
Edward Paget
8006d49537 fix: move internal stats for module organization (#57430) 2025-04-30 17:50:33 +00:00
Cam Saul
0c154e49de [Backend modularization] cache module (#57410)
* [Backend modularization] `cache` module

* Update module config

* Update Setting references
2025-04-30 09:43:46 -06:00
Edward Paget
eb3cc6d1cc feat: add prometheus metric for quartz task states (#56994)
* feat: add prometheus metric for quartz task states

Adds a new quartz_task_state metric that will report to prometheus the
states of all quartz jobs running on a metabase instance using a guage.

Each task state is reported as a label and we also report a state for
tasks that are executing.

All hosts on a metabase cluster _should_ report the same value for these
metrics if they are scraped at the same time, and we need to consider if
the additional DB load is okay since this does query the database during
a prometheus scrape to report this metric.

* fixup! add job-name tag and histogram of job time
2025-04-30 13:31:08 +00:00
Nicolò Pretto
af3156b9dd add locale selector to sdk storybook (#57393)
* add locale selector to sdk storybook

* clarify the default value and change to have objects as items
2025-04-30 15:12:40 +02:00
Alexander Polyankin
0f158889c6 Handle submit in dashboard filter widgets (#57388)
* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit

* Handle submit
2025-04-30 08:11:58 -04:00
Alexander Polyankin
85411a4abc Apply filter split button (#57215)
* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Add filter add button

* Fix E2E

* Fix E2E

* Fix E2E

* Fix back button

* Fix back button

* Fix back button

* Fix back button

* Fix back button

* Fix back button

* Fix back button

* Remove extra source abstraction
2025-04-30 06:21:07 -04:00
Timofey Kachalov
1586c15e04 Add Access-Control-Max-Age header to cache CORS preflight requests made by SDK (#57383) 2025-04-30 11:55:52 +04:00
Timofey Kachalov
827c5d8b2e Update Shoppy Sample App Tests to work with App DB Dump (#57250) 2025-04-30 11:17:49 +04:00
Cam Saul
6d588fef7a [Backend modularization] embedding module (#57409)
* Embedding module

* Embedding module

* Fix lint errors

* Fix circular references

* Remove reference to renamed namespace
2025-04-29 16:58:58 -07:00
Cam Saul
c1d7e63ac0 [Backend modularization] Move premium-features API into module and other misc cleanup (#57404)
* [Backend modularization] Move `premium-features` API into module and other misc cleanup

* Fix Search Settings

* Fix

* Fix removed namespace

* Test fix 🔧
2025-04-29 15:30:17 -07:00
Cam Saul
486a5904a4 [Backend modularization] task-history module (#57369)
* [Backend modularization]  module

* Add init namespaces

* Test fix 🔧

* Test fixes 🔧

* [Backend Modularization] `task-history` module

* cljfmt
2025-04-29 14:57:15 -07:00
Noah Moss
b0c1eddc38 Quarantine cypress flakes in download format test + small correctness fix (#57392)
* try to fix cypress download format flakes

* handle unset last_table_download_format

* more test tweaks

* make tests shorter, refactor, and add wait periods

* rearrange more code

* remove the waits, quarantine tests as flaky

* move cy.intercept to right before reload

* skip tests again

* unravel .then calls
2025-04-29 21:38:16 +00:00
Nathan Voxland
ff6f759dbd Delete indexed rows from the search index which correspond to deleted rows (#57086) 2025-04-29 16:29:53 -05:00
Ryan Laurie
06d74f2209 Map settings array on fetch (#57367)
* map array on fetch

* fix types
2025-04-29 15:28:51 -06:00
Case Nelson
b11300d091 feat: Implement multi-catalog for databricks (#57148)
* feat: Implement multi-catalog for databricks

* Fix schema for nil

* Add tests for joins on multi-catalog

* Fix multicatalog driver name

* Remove require

* Alphabetize drivers

* Add changelog

* Fix tests

* Fix tests

* Do not sync system catalogs

* Address review comments

* Update connection properties

* Update example to make clear its a schema
2025-04-29 14:57:36 -06:00
Cam Saul
01b2c8c140 [Backend modularization] settings module (#57366)
* [Backend modularization]  module

* Add init namespaces

* Test fix 🔧

* Test fixes 🔧
2025-04-29 13:32:18 -07:00
Nemanja Glumac
2ab279f7bb Add unit tests coverage for branded export utils (#57358)
* Add basic unit test coverage for export branding utils

* Add tests for `getChartFileName` util
2025-04-29 21:17:07 +02:00
Nathan Voxland
cfd1cd8c3b Handle deleted connections in harbormaster (#57356) 2025-04-29 13:14:35 -05:00
Nathan Voxland
a7ff114fed mb/mb should be able to get an mb_dev_* token (#57363) 2025-04-29 09:10:49 -05:00
Alan Molina
6b5b200b71 Fix typo on collections.md (#52657)
Just removing an additional square bracket in the docs.
2025-04-29 09:09:11 -04:00
metamben
e8acf5e4b1 [SEM-219] Humanize error message even more (#57205)
* [SEM-219] Humanize error message even more

To provide the customized error message, the type check built into the defendpoint framework has to be disabled, which
results in less specific API documentation.

(Malli has an experimental feature we currently don't use for looking up humanized errors from parent schemas, that
would let us configure the error message AND keep the input type information.)
2025-04-29 15:51:34 +03:00
Ryan Laurie
a180c257e1 Refactor Build + Docker Uberjar Job (#57298)
* rewrite uberjar workflow

* test

* fix

* fix

* fix

* fix

* fix

* fixes

* dont make docker sad

* remove test code

* remove metabot shenanigans

* address review comments

* different secrets handling

* remove test code
2025-04-29 06:22:03 -06:00
github-automation-metabase
f990334130 Bump com.gfredericks/test.chuck from 0.2.14 to 0.2.15 (#56651)
Inspect dependency changes here: https://github.com/gfredericks/test.chuck/blob/test.chuck-0.2.15/changes.md

Co-authored-by: Metabase bot <metabase-bot@metabase.com>
2025-04-29 10:30:07 +00:00
Nemanja Glumac
ebd7ea407f Use custom redux import to remove type casting (#57362)
Resolves PRG-13
2025-04-29 09:46:47 +00:00
lbrdnk
79daf9d2bd Use case statment to filter metrics (#55094)
* Test comparing  case vs filtered ags

* Add test and update case definition

* Add logging to check CI, mark as driver ns

* Remove metadata to see which driver tests run

* add failing op for some drivers

* Use case to filter metric ag

* Filter in case

* Existing tests update

* WIP Respect expression deduplication

* Move transformation right after metric fetch

* Update test

* Aggregation maps to be used in tests

* Simplify metric transform

* New tests

* Cleanup

* Cleanup

* Cleanup

* Fix equality

* Cleanup

* Cleanup equality

* Simplify test

* Tolerate float deviation

* Feature dependent aggregations

* Update test

* Fix value formatting

* Add var to std dev ags

* Update the filtering logic

* Speculate

* Adjust tests

* Update test

* Kondo

* Adjust test for starburst

* Remove debug def

* Apply suggestions from code review

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>

* Adjust naming

* Comment

* fresh-uuids into definition

* Add shortcut for empty condition

* Adjust meta and case default

* Remove now redundant condition

* Docstring

* Indentation

* Remove dev leftover

* Remove dev leftover

* Update tests and distinct-where

* Nullary aggregations map

* Distinct-where feature

* Fix formatting

* Adjust e2e test

---------

Co-authored-by: metamben <103100869+metamben@users.noreply.github.com>
2025-04-29 09:48:55 +02:00
Sloan Sparger
d8cd6914d6 [Admin] Migrate groups pages + components to TS (#57232)
* convert UnsubscribeUserModal and UnsubscribeUserForm components

* convert UserActivationModal to TS

* convert UserPasswordResetModal to TS

* convert UserSuccessModal to TS

* convert PeopleList and PeopleListingApp components to TS

* convert PeopleListRow to TS

* convert use-people-query to TS

* fix deprecated usages of google_auth and ldap_auth with sso_source field

* fix lint

* convert GroupsListing, GroupsListingApp, and AddRow components to TS

* convert lib/groups, AdminContentTable, and AddMemberRow to TS

* convert GroupDetailApp and GroupDetail to TS

* fix import

* convert GroupSummary to TS, make adjustments to other converted files to make types happy

* remove people/components/SearchInput and convert usage to mantine component to avoid TS migration
2025-04-28 19:09:20 -05:00
Eric Normand
85194c8080 Extend integer() cast function to accept floats (#56817)
* Add float cases to integer cast testing

* Update types of integer mbql expression

* Better messages for tests

* Add rounding test and specialize for redshift

* Don't use extreme values for redshift

* Add e2e tests for float -> integer casts

* Update helper text for integer

* Change test message.

Co-authored-by: appleby <86076+appleby@users.noreply.github.com>

* Helper function to convert to integer

* Appease the kondo

* More accurate comment

* Update arithmetic.cljc

* Refactor to use better ->integer implementation for SQL dbs

* kick ci

* Missed an argument

* Round properly for inline constants

* Add String->Integer and Float->Integer coercion strategies

* Add MongoDB string->integer and float->integer coercions

* Fix types in coercion test

* Also cast in mongodb coercion

* Use NUMBER(p) type in Oracle

* Spark SQL needs explicit round()

* SQLite needs explicit round()

* SQL Server needs explicit round()

* Clickhouse cast to float first

* SQLite needs to cast to float first so it can round

* SQL Server needs to cast to float so it can round

* Mongo needs to cast to float before rounding

* Spark SQL needs to cast to float so it can round

* Revert converting multimethod ->integer to defn

* Remove cast-integer multi-method and restore ->integer

* Update `integer-dbtype` in changelog

* Fix oracle integer type

* Fix spark sql casting

* Databricks driver needs to round

* kick ci

* Use ->integer in coercion

* Helper function for cast to integer with round

* kick ci

* Remove unnecessary ->float implementation

* Fix cljfmt precommmit hook

* Fix clickhouse returning bigdecimal

* Handle raw values in untyped-inline-value

* This came in with the merge

* Allow integer(float) and integer(string)

* Remove stray println

* Work for known types only

* Test for converting literals at compile time

* Update mysql

* Fix timezone test failing on null week-of-year

* Update fe test for float coercion

* Doh! Typo.

---------

Co-authored-by: Romeo Van Snick <romeo@romeovansnick.be>
Co-authored-by: appleby <86076+appleby@users.noreply.github.com>
2025-04-28 18:34:36 -05:00
Cam Saul
a32c124933 [Backend Modularization] query-analysis module cleanup (#57360)
* Move QueryAnalysis model into `query-analysis` module

* Add `metabase.query-analysis.settings`

* Move `query-analysis-enabled` Setting

* Update modules config
2025-04-28 22:53:20 +00:00
Aleksandr Lesnenko
b0065a0c8e fix invalid tooltip on interpolated values (#57305)
* fix invalid tooltip on interpolated values

* specs, timezone issues for interpolated datums

* fix waterfall, scatter

* update e2e spec after fixing timezone issue

* fix e2e failures

* specs
2025-04-28 18:07:48 -04:00
Aleksandr Lesnenko
a43bdf804d pivot table: show subtotals for a single item (#56988)
* add viz setting fe

* ClJS changes

* Fix up some BE tests

* fix last test

* spec, hint

* specs

* review

---------

Co-authored-by: Noah Moss <noahbmoss@gmail.com>
2025-04-28 21:27:16 +00:00
Edward Paget
3584a5c83b fix: avoid deadlocks by not holding connection (#56524)
Avoid the possiblity of deadlocks during cached query processing by no
taking the connection using with-connection and instead just selecting
the cached result using toucan.

I _think_ this connection holding was introduced under the assumption
that we could 'stream' a binary column from postgresql using the
.getBinaryStreaming jdbc method. However postgres requires binary
columns to be full read and written and so we are unable to stream
results from that column, so this operation ends up being the same as
just querying the column normally.

Because we are not streaming the result in it seems safe to just yeild
the connection back to pool so it can shared with other requests.
2025-04-28 15:52:02 -05:00
Noah Moss
8ccc4df47d Remember last download format in user kv store (#57155)
* initial implementation

* e2e test

* tweaks

* fix e2e test + refactor

* reduce unneeded comments

* clean ns

* fix unit tests

* fix e2e test

* disable userKV when no user is logged in

* fix up a number of unit tests

* refactor out BaseQuestionDownloadPopoverProps with two wrapper components to avoid using the user kv hook in public/embedded contexts

* fix up types

* fix up e2e test

* use ?. syntax
2025-04-28 15:57:13 -04:00
Noah Moss
1a4744e18c Hide email domains for non-admin users without settings access (#57300)
* hide email domains for users without settings access

* fix test

* fix up e2e test

* update BE setting visibility

* Revert "update BE setting visibility"

This reverts commit b3728ad21d.

* remove commented-out line
2025-04-28 19:45:19 +00:00
538 changed files with 6869 additions and 3226 deletions

View File

@@ -417,13 +417,11 @@
metabase.model-persistence.core model-persistence
metabase.models.dependency dependency
metabase.models.interface mi
metabase.models.setting setting
metabase.notification.core notification
metabase.notification.payload.core notification.payload
metabase.notification.test-util notification.tu
metabase.permissions.core perms
metabase.plugins.initialize plugins.init
metabase.public-settings public-settings
metabase.public-sharing.core public-sharing
metabase.premium-features.core premium-features
metabase.pulse.core pulse
@@ -466,6 +464,7 @@
metabase.server.core server
metabase.server.protocols server.protocols
metabase.sso.core sso
metabase.settings.core setting
metabase.sync.analyze.fingerprint sync.fingerprint
metabase.sync.field-values sync.field-values
metabase.sync.interface i
@@ -550,14 +549,10 @@
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info clojure.core/fn
metabase.models.collection-test/with-collection-in-location clojure.core/let
metabase.models.json-migration/def-json-migration clj-kondo.lint-as/def-catch-all
metabase.models.setting/defsetting clj-kondo.lint-as/def-catch-all
metabase.models.setting.multi-setting/define-multi-setting clojure.core/def
metabase.models.util.spec-update/define-spec clojure.core/def
metabase.notification.test-util/with-temp-notification clojure.core/let
metabase.notification.test-util/with-card-notification clojure.core/let
metabase.notification.test-util/with-system-event-notification! clojure.core/let
metabase.public-settings.premium-features/defenterprise-schema clj-kondo.lint-as/def-catch-all
metabase.public-settings.premium-features/define-premium-feature clojure.core/def
metabase.premium-features.core/defenterprise-schema clj-kondo.lint-as/def-catch-all
metabase.premium-features.core/define-premium-feature clojure.core/def
metabase.premium-features.defenterprise/defenterprise-schema clj-kondo.lint-as/def-catch-all
@@ -569,6 +564,10 @@
metabase.query-processor.middleware.process-userland-query-test/with-query-execution! clojure.core/let
metabase.query-processor.setup/with-qp-setup clojure.core/let
metabase.search.api-test/do-test-users clojure.core/let
metabase.settings.core/defsetting clj-kondo.lint-as/def-catch-all
metabase.settings.core/define-multi-setting clojure.core/def
metabase.settings.models.setting/defsetting clj-kondo.lint-as/def-catch-all
metabase.settings.models.setting.multi-setting/define-multi-setting clojure.core/def
metabase.sso.ldap/with-ldap-connection clojure.core/fn
metabase.sync.util/sum-for clojure.core/for
metabase.sync.util/with-emoji-progress-bar clojure.core/let
@@ -635,10 +634,6 @@
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
metabase.api.dashboard-test/with-chain-filter-fixtures hooks.common/let-one-with-optional-value
metabase.api.dashboard-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
metabase.api.embed-test/do-response-formats hooks.common/with-two-bindings
metabase.api.embed-test/with-chain-filter-fixtures! hooks.common/let-one-with-optional-value
metabase.api.embed-test/with-temp-card hooks.common/let-one-with-optional-value
metabase.api.embed-test/with-temp-dashcard hooks.common/let-one-with-optional-value
metabase.api.macros/defendpoint hooks.metabase.api.macros/defendpoint
metabase.api.user-test/with-temp-user-email! hooks.common/with-one-binding
metabase.channel.template.handlebars-helper/defhelper hooks.metabase.channel.template.handlebars-helper/defhelper
@@ -648,6 +643,10 @@
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
metabase.driver.oracle-test/with-temp-user hooks.common/let-one-with-optional-value
metabase.driver/register! hooks.metabase.driver/register!
metabase.embedding.api.embed-test/do-response-formats hooks.common/with-two-bindings
metabase.embedding.api.embed-test/with-chain-filter-fixtures! hooks.common/let-one-with-optional-value
metabase.embedding.api.embed-test/with-temp-card hooks.common/let-one-with-optional-value
metabase.embedding.api.embed-test/with-temp-dashcard hooks.common/let-one-with-optional-value
metabase.legacy-mbql.schema.macros/defclause hooks.metabase.legacy-mbql.schemas.macros/defclause
metabase.lib.schema.mbql-clause/define-catn-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
metabase.lib.schema.mbql-clause/define-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
@@ -664,9 +663,6 @@
metabase.models.dashboard-test/with-dash-in-collection! hooks.common/with-three-bindings
metabase.models.interface/define-batched-hydration-method hooks.metabase.models.interface/define-hydration-method
metabase.models.interface/define-simple-hydration-method hooks.metabase.models.interface/define-hydration-method
metabase.models.setting.multi-setting/define-multi-setting hooks.metabase.models.setting/define-multi-setting
metabase.models.setting.multi-setting/define-multi-setting-impl hooks.metabase.models.setting/define-multi-setting-impl
metabase.models.setting/defsetting hooks.metabase.models.setting/defsetting
metabase.permissions.core/allow-changing-all-users-group-members hooks.common/do*
metabase.permissions.core/disable-perms-cache hooks.common/do*
metabase.plugins.classloader/require hooks.clojure.core.require/lint-require
@@ -690,6 +686,12 @@
metabase.search.api-test/with-search-items-in-collection hooks.metabase.search.api-test/with-search-items-in-collection
metabase.search.api-test/with-search-items-in-root-collection hooks.common/do*
metabase.server.streaming-response-test/with-start-execution-chan hooks.common/with-one-binding
metabase.settings.core/define-multi-setting hooks.metabase.settings.models.setting/define-multi-setting
metabase.settings.core/define-multi-setting-impl hooks.metabase.settings.models.setting/define-multi-setting-impl
metabase.settings.core/defsetting hooks.metabase.settings.models.setting/defsetting
metabase.settings.models.setting.multi-setting/define-multi-setting hooks.metabase.settings.models.setting/define-multi-setting
metabase.settings.models.setting.multi-setting/define-multi-setting-impl hooks.metabase.settings.models.setting/define-multi-setting-impl
metabase.settings.models.setting/defsetting hooks.metabase.settings.models.setting/defsetting
metabase.stale-test/with-stale-items hooks.toucan2.tools.with-temp/with-temp
metabase.test.data.datasets/test-driver hooks.metabase.test.data.datasets/test-driver
metabase.test.data.datasets/test-drivers hooks.metabase.test.data.datasets/test-drivers
@@ -749,43 +751,43 @@
metabase.xrays.api.automagic-dashboards-test/with-indexed-model! hooks.metabase.xrays.api.automagic-dashboards-test/with-indexed-model!}
:macroexpand
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.test/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase.api.card-test/with-ordered-items macros.metabase.api.card-test/with-ordered-items
metabase.api.collection-test/with-collection-hierarchy! macros.metabase.api.collection-test/with-collection-hierarchy!
metabase.api.collection-test/with-some-children-of-collection! macros.metabase.api.collection-test/with-some-children-of-collection!
metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing! macros.metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing!
metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing! macros.metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing!
metabase.lib.common/defop macros.metabase.lib.common/defop
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
metabase.models.params.chain-filter-test/chain-filter macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.params.chain-filter-test/chain-filter-search macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.query-analysis-test/with-test-setup macros.metabase.models.query-analysis-test/with-test-setup
metabase.models.user-test/with-groups macros.metabase.models.user-test/with-groups
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing!
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
metabase.query-analysis.task.test-setup/with-test-setup! macros.metabase.query-analysis.task.test-setup/with-test-setup!
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
metabase.server.statistics-handler-test/with-server macros.metabase.server.statistics-handler-test/with-server
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
metabase.xrays.domain-entities.malli/define-getters-and-setters macros.metabase.xrays.domain-entities.malli/define-getters-and-setters
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.test/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase.api.card-test/with-ordered-items macros.metabase.api.card-test/with-ordered-items
metabase.api.collection-test/with-collection-hierarchy! macros.metabase.api.collection-test/with-collection-hierarchy!
metabase.api.collection-test/with-some-children-of-collection! macros.metabase.api.collection-test/with-some-children-of-collection!
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing! macros.metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing!
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing! macros.metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing!
metabase.lib.common/defop macros.metabase.lib.common/defop
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
metabase.models.params.chain-filter-test/chain-filter macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.params.chain-filter-test/chain-filter-search macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.user-test/with-groups macros.metabase.models.user-test/with-groups
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing!
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
metabase.query-analysis.models.query-analysis-test/with-test-setup macros.metabase.query-analysis.models.query-analysis-test/with-test-setup
metabase.query-analysis.task.test-setup/with-test-setup! macros.metabase.query-analysis.task.test-setup/with-test-setup!
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
metabase.server.statistics-handler-test/with-server macros.metabase.server.statistics-handler-test/with-server
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
metabase.xrays.domain-entities.malli/define-getters-and-setters macros.metabase.xrays.domain-entities.malli/define-getters-and-setters
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
:config-in-comment
{:linters {:unresolved-symbol {:level :off}}}

View File

@@ -30,11 +30,12 @@
;; ## Tips
;;
;; PRO TIP: Check out the [[dev.deps-graph]] namespace for helpful tools to see where a module is used externally, and
;; for tools for calculating this config.
;; for tools for calculating this config -- you can use [[dev.deps-graph/print-kondo-config-diff]] to see what updates
;; you need to make
{:metabase/modules
{actions
{:api #{metabase.actions.api metabase.actions.core}
:uses #{analytics api driver legacy-mbql lib model-persistence models query-processor search util}}
:uses #{analytics api driver legacy-mbql lib model-persistence models query-processor search settings util}}
activity-feed
{:api #{metabase.activity-feed.api metabase.activity-feed.init}
@@ -53,8 +54,7 @@
internal-stats
models
premium-features
public-settings
server
settings
util}}
analyze
{:api #{metabase.analyze.core metabase.analyze.query-results}
@@ -62,7 +62,6 @@
api
{:api #{metabase.api.auth
metabase.api.cache
metabase.api.card
metabase.api.collection
metabase.api.common
@@ -81,8 +80,9 @@
metabase.api.util.handlers}
:uses :any}
audit {:api #{metabase.audit}
:uses #{db models}}
audit
{:api #{metabase.audit}
:uses #{db settings}}
auth-provider
{:api #{metabase.auth-provider}
@@ -92,6 +92,12 @@
{:api #{metabase.bookmarks.api}
:uses #{api db models util}}
cache
{:api #{metabase.cache.api
metabase.cache.core
metabase.cache.init}
:uses #{api config db events models premium-features settings util}}
channel
{:api #{metabase.channel.api
metabase.channel.core
@@ -114,7 +120,7 @@
notification
permissions
premium-features
public-settings
settings
query-processor
timeline
types
@@ -124,13 +130,13 @@
{:api #{metabase.cloud-migration.api
metabase.cloud-migration.core
metabase.cloud-migration.init}
:uses #{api cmd config db models premium-features task util}}
:uses #{api cmd config db models premium-features settings task util}}
cmd
{:api #{metabase.cmd
metabase.cmd.copy
metabase.cmd.dump-to-h2}
:uses #{api config db legacy-mbql models plugins query-processor search util}}
:uses #{api config db legacy-mbql models plugins query-processor search settings util}}
;; technically this 'uses' `enterprise/core` and `test` since it tries to load them to see if they exists so we know
;; if EE/test code is available; however we can ignore them since they're not real usages.
@@ -180,6 +186,7 @@
metabase.driver.sync
metabase.driver.util}
:uses #{actions
api
auth-provider
config
connection-pool
@@ -192,30 +199,44 @@
notification
plugins
premium-features
public-settings
settings
pulse
query-processor
sync
task
upload
util
enterprise/database-routing}}
util}}
eid-translation
{:api #{metabase.eid-translation.core}
:uses #{}}
:uses #{settings util}}
embed
{:api #{metabase.embed.settings metabase.embed.app-origins-sdk}
:uses #{analytics models premium-features tiles util}}
embedding
{:api #{metabase.embedding.api
metabase.embedding.app-origins-sdk
metabase.embedding.init
metabase.embedding.settings}
:uses #{analytics
api
config
driver
eid-translation
events
models
notification
premium-features
public-sharing
query-processor
settings
tiles
util}}
events
{:api #{metabase.events metabase.events.init metabase.events.notification}
:uses #{api channel driver models notification premium-features util}}
:uses #{api channel driver models notification premium-features task-history util}}
formatter
{:api #{metabase.formatter}
:uses #{models public-settings query-processor types util}}
:uses #{models settings query-processor types util}}
indexed-entities
{:api #{metabase.indexed-entities.api metabase.indexed-entities.init}
@@ -223,10 +244,10 @@
integrations
{:api #{metabase.integrations.slack}
:uses #{events models util}}
:uses #{events settings util}}
internal-stats
{:api #{metabase.internal-stats}
{:api #{metabase.internal-stats.core}
:uses #{db models}}
legacy-mbql
@@ -274,11 +295,11 @@
metabase.lib.util
metabase.lib.util.match
metabase.lib.walk}
:uses #{config driver legacy-mbql models types util}}
:uses #{config legacy-mbql models settings types util}}
lib-be
{:api #{metabase.lib-be.init}
:uses #{models task util}}
:uses #{models settings task util}}
logger
{:api #{metabase.logger metabase.logger.init}
@@ -288,19 +309,18 @@
{:api #{metabase.login-history.api
metabase.login-history.core
metabase.login-history.init}
:uses #{analytics api channel models request util}}
:uses #{analytics api channel request settings util}}
model-persistence
{:api #{metabase.model-persistence.api
metabase.model-persistence.core
metabase.model-persistence.init}
:uses #{api db driver events lib models premium-features public-settings query-processor request task util}}
:uses #{api db driver events lib models premium-features settings query-processor request task task-history util}}
models
{:api #{metabase.models.api-key
metabase.models.application-permissions-revision
metabase.models.audit-log
metabase.models.cache-config
metabase.models.card
metabase.models.card.metadata
metabase.models.collection
@@ -322,23 +342,17 @@
metabase.models.moderation-review
metabase.models.native-query-snippet
metabase.models.native-query-snippet.permissions
metabase.notification.models
metabase.models.params
metabase.models.params.chain-filter
metabase.models.params.custom-values
metabase.models.params.field-values
metabase.models.params.shared
metabase.models.query
metabase.models.query-analysis
metabase.models.query.permissions
metabase.models.resolution
metabase.models.secret
metabase.models.serialization
metabase.models.setting
metabase.models.setting.cache
metabase.models.setting.multi-setting
metabase.models.table
metabase.models.task-history
metabase.models.user
metabase.models.user-parameter-value
metabase.models.util.spec-update
@@ -348,6 +362,7 @@
analyze
api
audit
cache
channel
config
db
@@ -360,20 +375,17 @@
permissions
plugins
premium-features
public-settings
settings
public-sharing
pulse
query-analysis
query-processor
request
search
server
setup
sync
task
types
util
enterprise/advanced-permissions}}
util}}
moderation
{:api #{metabase.moderation}
@@ -384,7 +396,9 @@
metabase.notification.core
metabase.notification.init
metabase.notification.models
metabase.notification.payload.core}
metabase.notification.api.notification
metabase.notification.payload.core
metabase.notification.send}
:uses #{analytics
api
channel
@@ -394,10 +408,11 @@
models
permissions
premium-features
public-settings
settings
query-processor
request
task
task-history
util}}
permissions
@@ -407,7 +422,7 @@
metabase.permissions.models.permissions
metabase.permissions.models.permissions-group
metabase.permissions.util}
:uses #{api audit config db models plugins premium-features request server util}}
:uses #{api audit config db models plugins premium-features request server settings util}}
pivot
{:api #{metabase.pivot.core}
@@ -418,19 +433,17 @@
:uses #{config driver util}}
premium-features
{:api #{metabase.premium-features.core
{:api #{metabase.premium-features.api
metabase.premium-features.core
metabase.premium-features.init
metabase.premium-features.token-check}
:uses #{config db internal-stats models plugins util}}
public-settings
{:api #{metabase.public-settings}
:uses #{api config models premium-features util}}
:uses #{api config db internal-stats plugins settings util}}
public-sharing
{:api #{metabase.public-sharing.api
metabase.public-sharing.core
metabase.public-sharing.init}
:uses #{actions analytics api db events lib models query-processor request tiles util}}
:uses #{actions analytics api db events lib models query-processor request settings tiles util}}
pulse
{:api #{metabase.pulse.api
@@ -438,12 +451,12 @@
metabase.pulse.init
metabase.pulse.task.send-pulses}
:uses #{api channel config db driver events integrations models notification permissions plugins premium-features
query-processor request task util
query-processor request task task-history util
enterprise/advanced-config enterprise/advanced-permissions enterprise/sandbox}}
query-analysis
{:api #{metabase.query-analysis.core metabase.query-analysis.init}
:uses #{config driver legacy-mbql lib public-settings query-processor task util}}
:uses #{config driver legacy-mbql lib query-processor settings task util}}
query-processor
{:api #{metabase.query-processor
@@ -484,8 +497,8 @@
analyze
api
audit
cache
config
db
driver
events
formatter
@@ -496,7 +509,7 @@
permissions
pivot
premium-features
public-settings
settings
request
server
types
@@ -504,7 +517,7 @@
request
{:api #{metabase.request.core}
:uses #{api config models permissions public-settings util}}
:uses #{api config models permissions settings util}}
revisions
{:api #{metabase.revisions.api metabase.revisions.core metabase.revisions.init}
@@ -523,7 +536,7 @@
metabase.search.ingestion
metabase.search.init
metabase.search.spec}
:uses #{analytics api audit config db driver models permissions premium-features public-settings request task util}}
:uses #{analytics api audit config db driver models permissions premium-features settings request task util}}
segments
{:api #{metabase.segments.api}
@@ -534,14 +547,21 @@
metabase.server.middleware.json
metabase.server.middleware.session
metabase.server.streaming-response}
:uses #{analytics api config core db driver embed models premium-features public-settings session request util enterprise/sso}}
:uses #{analytics api config core db driver embedding models premium-features settings session request util enterprise/sso}}
session
{:api #{metabase.session.core
metabase.session.api
metabase.session.init
metabase.session.models.session}
:uses #{api channel config db driver events login-history models public-settings request sso task util}}
:uses #{api channel config db driver events login-history models settings request sso task util}}
settings
{:api #{metabase.settings.api
metabase.settings.core
metabase.settings.deprecated-grab-bag
metabase.settings.init}
:uses #{api config db events models plugins premium-features server util enterprise/advanced-permissions}}
setup
{:api #{metabase.setup.api metabase.setup.core}
@@ -550,13 +570,10 @@
channel
config
db
embed
events
integrations
models
permissions
premium-features
public-settings
settings
request
session
util}}
@@ -565,7 +582,7 @@
{:api #{metabase.sso.api
metabase.sso.core
metabase.sso.init}
:uses #{api models permissions premium-features util}}
:uses #{api models permissions premium-features settings util}}
sync
{:api #{metabase.sync.api
@@ -574,32 +591,41 @@
metabase.sync.schedules
metabase.sync.task.sync-databases
metabase.sync.util}
:uses #{analyze api audit config db driver events lib models plugins query-processor task util}}
:uses #{analyze api audit config db driver events lib models query-processor settings task task-history util}}
task
{:api #{metabase.task
metabase.task.bootstrap
metabase.task.init
metabase.task.notification}
metabase.task.init}
:uses #{analytics
channel
config
db
driver
integrations
models
notification
plugins
premium-features
public-settings
query-processor
settings
task-history
util
enterprise/task}}
task-history
{:api #{metabase.task-history.api
metabase.task-history.core
metabase.task-history.init}
:uses #{api
models
permissions
premium-features
request
task
util}}
tiles
{:api #{metabase.tiles.api
metabase.tiles.init}
:uses #{api legacy-mbql models query-processor util}}
:uses #{api legacy-mbql query-processor settings util}}
timeline
{:api #{metabase.timeline.api metabase.timeline.core}
@@ -611,7 +637,7 @@
upload
{:api #{metabase.upload}
:uses #{analytics api driver events legacy-mbql lib model-persistence models permissions public-settings sync util}}
:uses #{analytics api driver events legacy-mbql lib model-persistence models permissions settings sync util}}
user-key-value
{:api #{metabase.user-key-value.api metabase.user-key-value.init}
@@ -619,24 +645,25 @@
util
{:api :any
:uses #{config db driver legacy-mbql lib models plugins premium-features public-settings}}
:uses #{config db driver legacy-mbql lib models plugins settings}}
xrays
{:api #{metabase.xrays.api metabase.xrays.core}
:uses #{analyze api db driver legacy-mbql lib models public-settings query-processor util}}
{:api #{metabase.xrays.api
metabase.xrays.core
metabase.xrays.init}
:uses #{analyze api driver legacy-mbql lib models settings query-processor util}}
enterprise/advanced-config
{:api #{metabase-enterprise.advanced-config.api.logs
metabase-enterprise.advanced-config.models.pulse-channel
metabase-enterprise.advanced-config.file
metabase-enterprise.advanced-config.crufty}
:uses #{api db driver models permissions premium-features setup sync util}}
metabase-enterprise.advanced-config.file}
:uses #{api db driver models permissions premium-features setup settings sync util}}
enterprise/advanced-permissions
{:api #{metabase-enterprise.advanced-permissions.api.routes
metabase-enterprise.advanced-permissions.common
metabase-enterprise.advanced-permissions.models.permissions.group-manager}
:uses #{api audit driver enterprise/impersonation lib models permissions premium-features query-processor util enterprise/sandbox}}
:uses #{api enterprise/impersonation lib models permissions premium-features query-processor util}}
enterprise/airgap
{:api #{}
@@ -659,6 +686,7 @@
plugins
premium-features
query-processor
settings
sync
util
enterprise/serialization}}
@@ -672,8 +700,8 @@
:uses #{api premium-features util}}
enterprise/cache
{:api #{metabase-enterprise.cache.strategies}
:uses #{api models premium-features query-processor util}}
{:api #{metabase-enterprise.cache.init}
:uses #{cache premium-features query-processor task util}}
enterprise/content-verification
{:api #{metabase-enterprise.content-verification.api.routes}
@@ -681,44 +709,47 @@
enterprise/core
{:api #{metabase-enterprise.core.init}
:uses #{enterprise/enhancements}}
:uses :any}
enterprise/dashboard-subscription-filters
{:api #{}
:uses #{premium-features}}
enterprise/data-editing
{:api #{metabase-enterprise.data-editing.api}
:uses #{api actions util}}
enterprise/database-routing
{:api #{metabase-enterprise.database-routing.core metabase-enterprise.database-routing.api}
:uses #{api config database-routing events lib models premium-features query-processor util}}
{:api #{metabase-enterprise.database-routing.api}
:uses #{api config database-routing events lib models premium-features query-processor settings util}}
enterprise/enhancements
{:api #{metabase-enterprise.enhancements.init}
:uses #{models premium-features sso util enterprise/sso}}
:uses #{models premium-features settings sso util enterprise/sso}}
enterprise/gsheets
{:api #{metabase-enterprise.gsheets.api}
:uses #{api analytics enterprise/harbormaster models premium-features server util}}
:uses #{api analytics premium-features settings util enterprise/harbormaster}}
enterprise/harbormaster
{:api #{metabase-enterprise.harbormaster.client}
:uses #{api config cloud-migration models util}}
:uses #{api cloud-migration util}}
enterprise/impersonation
{:api #{metabase-enterprise.impersonation.core
metabase-enterprise.impersonation.api}
:uses #{api audit driver enterprise/sandbox lib models permissions premium-features query-processor util}}
{:api #{metabase-enterprise.impersonation.api}
:uses #{api audit driver lib models premium-features query-processor util enterprise/sandbox}}
enterprise/internal-stats
{:api #{metabase-enterprise.internal-stats}
:uses #{models premium-features}}
{:api #{}
:uses #{premium-features settings}}
enterprise/llm
{:api #{metabase-enterprise.llm.api}
:uses #{analytics analyze api models query-processor util}}
:uses #{analytics analyze api query-processor settings util}}
enterprise/query-reference-validation
{:api #{metabase-enterprise.query-reference-validation.api}
:uses #{api models public-settings request util}}
:uses #{api models query-analysis request util}}
enterprise/sandbox
{:api #{metabase-enterprise.sandbox.api.routes
@@ -735,12 +766,11 @@
query-processor
request
util
enterprise/impersonation
enterprise/api}}
enterprise/scim
{:api #{metabase-enterprise.scim.api metabase-enterprise.scim.routes}
:uses #{analytics api models permissions public-settings server util enterprise/serialization}}
:uses #{analytics api models permissions settings server util enterprise/serialization}}
enterprise/search
{:api #{}
@@ -750,7 +780,7 @@
{:api #{metabase-enterprise.serialization.api
metabase-enterprise.serialization.cmd
metabase-enterprise.serialization.v2.backfill-ids}
:uses #{analytics api config db legacy-mbql lib logger models plugins premium-features public-settings setup util}}
:uses #{analytics api config db legacy-mbql lib logger models plugins premium-features settings setup util}}
enterprise/snippet-collections
{:api #{}
@@ -760,23 +790,19 @@
{:api #{metabase-enterprise.sso.api.routes
metabase-enterprise.sso.integrations.sso-settings
metabase-enterprise.sso.integrations.sso-utils}
:uses #{api channel embed events models premium-features public-settings request session sso util enterprise/scim}}
:uses #{api channel embedding events premium-features request session settings sso util enterprise/scim}}
enterprise/stale
{:api #{metabase-enterprise.stale.routes}
:uses #{analytics api embed models premium-features public-settings request util}}
:uses #{analytics api embedding models premium-features request settings util}}
enterprise/stats
{:api #{}
:uses #{driver premium-features enterprise/advanced-config enterprise/scim enterprise/sso}}
enterprise/data-editing
{:api #{}
:uses #{api actions util}}
enterprise/task
{:api #{metabase-enterprise.task.truncate-audit-tables}
:uses #{premium-features query-processor task util enterprise/cache}}
:uses #{premium-features}}
enterprise/upload-management
{:api #{metabase-enterprise.upload-management.api}

View File

@@ -74,11 +74,6 @@
metabase.models.field-values/create-or-update-full-field-values!
metabase.models.moderation-review/create-review!
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
metabase.models.setting-test/test-user-local-allowed-setting!
metabase.models.setting-test/test-user-local-only-setting!
metabase.models.setting.cache/restore-cache!
metabase.models.setting/set!
metabase.models.setting/validate-settings-formatting!
metabase.permissions.models.permissions/grant-application-permissions!
metabase.permissions.models.permissions/grant-collection-read-permissions!
metabase.permissions.models.permissions/grant-collection-readwrite-permissions!
@@ -97,6 +92,11 @@
metabase.query-processor.streaming.interface/finish!
metabase.query-processor.streaming.interface/write-row!
metabase.sample-data/try-to-extract-sample-database!
metabase.settings.models.setting-test/test-user-local-allowed-setting!
metabase.settings.models.setting-test/test-user-local-only-setting!
metabase.settings.models.setting.cache/restore-cache!
metabase.settings.models.setting/set!
metabase.settings.models.setting/validate-settings-formatting!
metabase.setup.core/create-token!
metabase.sso.ldap-test-util/start-ldap-server!
metabase.sync.core/sync-database!

View File

@@ -1,4 +1,4 @@
(ns macros.metabase.api.embed-test)
(ns macros.metabase.embedding.api.embed-test)
(defmacro with-embedding-enabled-and-temp-card-referencing!
[table-kw field-kw [card-binding] & body]

View File

@@ -1,4 +1,4 @@
(ns macros.metabase.models.query-analysis-test)
(ns macros.metabase.query-analysis.models.query-analysis-test)
(defmacro with-test-setup [& body]
`(let [~'card-id 1

View File

@@ -1,4 +1,4 @@
(ns hooks.metabase.models.setting
(ns hooks.metabase.settings.models.setting
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common :as common]))
@@ -207,7 +207,7 @@
anon-binding (hooks/map-node options-list)])
(hooks/reg-keyword! (-> (hooks/keyword-node (keyword (hooks/sexpr setting-name)))
(with-meta (meta setting-name)))
'metabase.models.setting/defsetting)
'metabase.settings.models.setting/defsetting)
getter-node
setter-node))
(with-meta (meta node)))}))

View File

@@ -81,269 +81,271 @@
throttle.core/with-throttling [[:block 1]]
;; the rest of the stuff in this list is generated automatically based on `:style/indent` specs
;; by running [[dev.generate-cljfmt-config/specs]]... use that code to update this list
metabase-enterprise.impersonation.util-test/with-impersonations! [[:inner 0]]
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
metabase-enterprise.test/with-gtaps! [[:inner 0]]
metabase-enterprise.test/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.test/with-user-attributes [[:block 2]]
metabase.actions.actions-test/with-actions-test-data-and-actions-permissively-enabled [[:block 0]]
metabase.actions.test-util/with-actions [[:block 1]]
metabase.actions.test-util/with-actions-disabled [[:block 0]]
metabase.actions.test-util/with-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
metabase.actions.test-util/with-empty-db [[:inner 0]]
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
metabase.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.api.card-test/with-ordered-items [[:inner 0]]
metabase.api.card-test/with-temp-native-card! [[:block 1]]
metabase.api.card-test/with-temp-native-card-with-params! [[:block 1]]
metabase.api.collection-test/with-collection-hierarchy [[:block 1]]
metabase.api.collection-test/with-french-user-and-personal-collection [[:block 2]]
metabase.api.collection-test/with-some-children-of-collection [[:block 1]]
metabase.api.common/check [[:default]]
metabase.api.common/define-routes [[:block 0]]
metabase.api.common/let-404 [[:block 1]]
metabase.api.embed-test/do-response-formats [[:block 1]]
metabase.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.api.embed-test/with-new-secret-key [[:block 0]]
metabase.api.embed-test/with-temp-card [[:block 1]]
metabase.api.embed-test/with-temp-dashcard [[:block 1]]
metabase.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.api.preview-embed-test/with-new-secret-key [[:block 0]]
metabase.cmd.copy/step [[:block 1]]
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
metabase.config/build-type-case [[:block 0]]
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
metabase.db.liquibase/with-liquibase [[:block 1]]
metabase.db.liquibase/with-scope-locked [[:block 1]]
metabase.db.schema-migrations-test.impl/test-migrations [[:block 2]]
metabase.db/with-application-db [[:inner 0]]
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
metabase.driver.druid.client/DELETE [[:default]]
metabase.driver.druid.client/GET [[:default]]
metabase.driver.druid.client/POST [[:default]]
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info [[:block 1]]
metabase.driver.sql.query-processor/apply-top-level-clause [[:default]]
metabase.driver/with-driver [[:block 1]]
metabase.lib.binning/with-binning [[:default]]
metabase.lib.core/with-binning [[:default]]
metabase.lib.core/with-join-alias [[:default]]
metabase.lib.core/with-join-conditions [[:default]]
metabase.lib.join/with-join-alias [[:default]]
metabase.lib.join/with-join-conditions [[:default]]
metabase.lib.test-util.macros/$ids [[:inner 0]]
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.collection-test/with-collection-hierarchy [[:block 1]]
metabase.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
metabase.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
metabase.models.dashboard-test/with-dash-in-collection [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
metabase.models.interface/define-simple-hydration-method [[:inner 0]]
metabase.models.params.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
metabase.models.params.chain-filter-test/with-fk-field-to-field-remapping [[:block 0]]
metabase.models.params.chain-filter-test/with-human-readable-values-remapping [[:block 0]]
metabase.models.setting.multi-setting/define-multi-setting [[:inner 0]]
metabase.models.setting/defsetting [[:block 1]]
metabase.models.task-history/with-task-history [[:block 1]]
metabase.permissions.core/allow-changing-all-users-group-members [[:block 0]]
metabase.permissions.core/disable-perms-cache [[:block 0]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.public-sharing.api-test/with-temp-public-card [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard-and-card [[:block 1]]
metabase.pulse.api.alert-test/with-alert-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.debug/debug> [[:block 0]]
metabase.query-processor.error-type/deferror [[:block 1]]
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
metabase.query-processor.store/cached [[:block 1]]
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
metabase.query-processor.streaming/streaming-response [[:block 1]]
metabase.query-processor.test-util/format-rows-by [[:default]]
metabase.request.core/as-admin [[:block 0]]
metabase.request.core/with-current-request [[:inner 0]]
metabase.request.core/with-current-user [[:inner 0]]
metabase.request.core/with-limit-and-offset [[:inner 0]]
metabase.request.session/as-admin [[:block 0]]
metabase.request.session/with-current-user [[:inner 0]]
metabase.sample-data-test/with-temp-sample-database-db [[:block 1]]
metabase.search.api-test/do-test-users [[:block 1]]
metabase.server.middleware.session/as-admin [[:block 0]]
metabase.server.middleware.session/with-current-user [[:inner 0]]
metabase.server.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
metabase.server.streaming-response-test/with-test-driver-db! [[:block 0]]
metabase.server.streaming-response/streaming-response [[:block 2]]
metabase.session.api/http-401-on-error [[:block 0]]
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
metabase.sync.util/sum-for [[:block 1]]
metabase.sync.util/sync-operation [[:block 3]]
metabase.sync.util/with-db-logging-disabled [[:default]]
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
metabase.sync.util/with-error-handling [[:block 1]]
metabase.sync.util/with-start-and-finish-logging [[:default]]
metabase.sync.util/with-start-and-finish-logging* [[:default]]
metabase.sync.util/with-sync-events [[:default]]
metabase.task/rerun-on-error [[:block 1]]
metabase.test/$ids [[:inner 0]]
metabase.test/as-admin [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/discard-setting-changes [[:block 1]]
metabase.test/format-rows-by [[:default]]
metabase.test/let-url [[:block 1]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/native-query [[:block 0]]
metabase.test/query [[:block 1]]
metabase.test/run-mbql-query [[:inner 0]]
metabase.test/test-driver [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/test-helpers-set-global-values! [[:block 0]]
metabase.test/throw-if-called! [[:block 1]]
metabase.test/with-actions [[:block 1]]
metabase.test/with-actions-disabled [[:block 0]]
metabase.test/with-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data [[:inner 0]]
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data-tables [[:block 1]]
metabase.test/with-additional-premium-features [[:block 1]]
metabase.test/with-current-user [[:inner 0]]
metabase.test/with-driver [[:block 1]]
metabase.test/with-empty-db [[:inner 0]]
metabase.test/with-empty-h2-app-db [[:block 0]]
metabase.test/with-metadata-provider [[:inner 0]]
metabase.test/with-premium-features [[:block 1]]
metabase.test/with-temp [[:inner 0]]
metabase.test/with-temp-copy-of-db [[:block 0]]
metabase.test/with-temp-scheduler [[:block 0]]
metabase.test/with-temp-test-data [[:inner 0]]
metabase.test/with-temp-vals-in-db [[:block 3]]
metabase.test/with-test-user [[:inner 0]]
metabase.test/with-user-in-groups [[:inner 0]]
metabase.test.data/$ids [[:inner 0]]
metabase.test.data/dataset [[:inner 0]]
metabase.test.data/mbql-query [[:inner 0]]
metabase.test.data/native-query [[:block 0]]
metabase.test.data/query [[:block 1]]
metabase.test.data/run-mbql-query [[:inner 0]]
metabase.test.data/with-empty-h2-app-db [[:block 0]]
metabase.test.data/with-temp-copy-of-db [[:block 0]]
metabase.test.data.datasets/test-driver [[:inner 0]]
metabase.test.data.datasets/test-drivers [[:inner 0]]
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
metabase.test.data.users/with-test-user [[:inner 0]]
metabase.test.util/discard-setting-changes [[:block 1]]
metabase.test.util/let-url [[:block 1]]
metabase.test.util/throw-if-called! [[:block 1]]
metabase.test.util/with-temp-scheduler [[:block 0]]
metabase.test.util/with-temp-vals-in-db [[:block 3]]
metabase.test.util/with-user-in-groups [[:inner 0]]
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
metabase.test.util.public-settings/with-premium-features [[:block 1]]
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
metabase.upload-test/with-upload-table! [[:inner 0]]
metabase.upload.types/with-currency [[:default]]
metabase.upload.types/with-parens [[:default]]
metabase.util/auto-retry [[:block 1]]
metabase.util/case-enum [[:block 1]]
metabase.util/ignore-exceptions [[:block 0]]
metabase.util/or-with [[:block 1]]
metabase.util/profile [[:block 1]]
metabase.util/prog1 [[:inner 0]]
metabase.util/varargs [[:inner 0]]
metabase.util/with-us-locale [[:block 0]]
metabase.util.async/cancelable-thread [[:block 0]]
metabase.util.date-2/with-time-zone-same-instant [[:default]]
metabase.util.encryption-test/with-secret-key [[:block 1]]
metabase.util.honey-sql-2/with-database-type-info [[:default]]
metabase.util.i18n/deferred-trs [[:default]]
metabase.util.i18n/deferred-tru [[:default]]
metabase.util.i18n/trs [[:default]]
metabase.util.i18n/trsn [[:default]]
metabase.util.i18n/tru [[:default]]
metabase.util.i18n/trun [[:default]]
metabase.util.jvm/auto-retry [[:block 1]]
metabase.util.jvm/varargs [[:block 1]]
metabase.util.jvm/with-us-locale [[:block 0]]
metabase.util.malli/disable-enforcement [[:block 0]]
metabase.util.malli/with [[:default]]
metabase.util.malli/with-api-error-message [[:default]]
metabase.util.namespaces/import-fns [[:default]]
metabase.util.queue-test/await-test-while [[:block 1]]
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.core/step [[:block 1]]
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.steps/step [[:block 1]]
methodical.core/defmethod [[:inner 0]]
methodical.core/defmulti [[:inner 0]]
methodical.core/with-dispatcher [[:default]]
methodical.core/with-method-table [[:default]]
methodical.core/with-prefers [[:default]]
methodical.interface/with-dispatcher [[:default]]
methodical.interface/with-method-table [[:default]]
methodical.interface/with-prefers [[:default]]
methodical.macros/defmethod [[:inner 0]]
methodical.macros/defmulti [[:inner 0]]
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
toucan2.connection/with-transaction [[:block 1]]
toucan2.core/build [[:block 0]]
toucan2.core/compile [[:block 0]]
toucan2.core/debug [[:block 0]]
toucan2.core/define-after-insert [[:inner 0]]
toucan2.core/define-after-select [[:inner 0]]
toucan2.core/define-after-update [[:inner 0]]
toucan2.core/define-before-delete [[:inner 0]]
toucan2.core/define-before-insert [[:inner 0]]
toucan2.core/define-before-select [[:inner 0]]
toucan2.core/define-default-fields [[:inner 0]]
toucan2.core/define-named-query [[:block 1]]
toucan2.core/deftransforms [[:block 1]]
toucan2.core/with-transaction [[:block 1]]
toucan2.tools.after/define-after-impl [[:default]]
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
toucan2.tools.after-select/define-after-select [[:inner 0]]
toucan2.tools.after-update/define-after-update [[:inner 0]]
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
toucan2.tools.before-select/define-before-select [[:inner 0]]
toucan2.tools.compile/build [[:block 0]]
toucan2.tools.compile/compile [[:block 0]]
toucan2.tools.compile/resolved [[:block 0]]
toucan2.tools.debug/debug [[:block 0]]
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
toucan2.tools.named-query/define-named-query [[:block 1]]
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
toucan2.tools.transformed/deftransforms [[:block 1]]
toucan2.tools.with-temp/with-temp [[:inner 0]]
toucan2.util/try-with-error-context [[:inner 0]]}}
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
metabase-enterprise.impersonation.util-test/with-impersonations! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
metabase-enterprise.test/with-gtaps! [[:inner 0]]
metabase-enterprise.test/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.test/with-user-attributes [[:block 2]]
metabase.actions.actions-test/with-actions-test-data-and-actions-permissively-enabled [[:block 0]]
metabase.actions.test-util/with-actions [[:block 1]]
metabase.actions.test-util/with-actions-disabled [[:block 0]]
metabase.actions.test-util/with-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
metabase.actions.test-util/with-empty-db [[:inner 0]]
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
metabase.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.api.card-test/with-ordered-items [[:inner 0]]
metabase.api.card-test/with-temp-native-card! [[:block 1]]
metabase.api.card-test/with-temp-native-card-with-params! [[:block 1]]
metabase.api.collection-test/with-collection-hierarchy [[:block 1]]
metabase.api.collection-test/with-french-user-and-personal-collection [[:block 2]]
metabase.api.collection-test/with-some-children-of-collection [[:block 1]]
metabase.api.common/check [[:default]]
metabase.api.common/define-routes [[:block 0]]
metabase.api.common/let-404 [[:block 1]]
metabase.cmd.copy/step [[:block 1]]
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
metabase.config/build-type-case [[:block 0]]
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
metabase.db.liquibase/with-liquibase [[:block 1]]
metabase.db.liquibase/with-scope-locked [[:block 1]]
metabase.db.schema-migrations-test.impl/test-migrations [[:block 2]]
metabase.db/with-application-db [[:inner 0]]
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
metabase.driver.druid.client/DELETE [[:default]]
metabase.driver.druid.client/GET [[:default]]
metabase.driver.druid.client/POST [[:default]]
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info [[:block 1]]
metabase.driver.sql.query-processor/apply-top-level-clause [[:default]]
metabase.driver/with-driver [[:block 1]]
metabase.embedding.api.embed-test/do-response-formats [[:block 1]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
metabase.embedding.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.embedding.api.embed-test/with-new-secret-key [[:block 0]]
metabase.embedding.api.embed-test/with-temp-card [[:block 1]]
metabase.embedding.api.embed-test/with-temp-dashcard [[:block 1]]
metabase.embedding.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.embedding.api.preview-embed-test/with-new-secret-key [[:block 0]]
metabase.lib.binning/with-binning [[:default]]
metabase.lib.core/with-binning [[:default]]
metabase.lib.core/with-join-alias [[:default]]
metabase.lib.core/with-join-conditions [[:default]]
metabase.lib.join/with-join-alias [[:default]]
metabase.lib.join/with-join-conditions [[:default]]
metabase.lib.test-util.macros/$ids [[:inner 0]]
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.collection-test/with-collection-hierarchy [[:block 1]]
metabase.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
metabase.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
metabase.models.dashboard-test/with-dash-in-collection [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
metabase.models.interface/define-simple-hydration-method [[:inner 0]]
metabase.models.params.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
metabase.models.params.chain-filter-test/with-fk-field-to-field-remapping [[:block 0]]
metabase.models.params.chain-filter-test/with-human-readable-values-remapping [[:block 0]]
metabase.permissions.core/allow-changing-all-users-group-members [[:block 0]]
metabase.permissions.core/disable-perms-cache [[:block 0]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.public-sharing.api-test/with-temp-public-card [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard [[:block 1]]
metabase.public-sharing.api-test/with-temp-public-dashboard-and-card [[:block 1]]
metabase.pulse.api.alert-test/with-alert-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
metabase.pulse.models.pulse-test/with-pulse-in-collection [[:inner 0]]
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.debug/debug> [[:block 0]]
metabase.query-processor.error-type/deferror [[:block 1]]
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
metabase.query-processor.store/cached [[:block 1]]
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
metabase.query-processor.streaming/streaming-response [[:block 1]]
metabase.query-processor.test-util/format-rows-by [[:default]]
metabase.request.core/as-admin [[:block 0]]
metabase.request.core/with-current-request [[:inner 0]]
metabase.request.core/with-current-user [[:inner 0]]
metabase.request.core/with-limit-and-offset [[:inner 0]]
metabase.request.session/as-admin [[:block 0]]
metabase.request.session/with-current-user [[:inner 0]]
metabase.sample-data-test/with-temp-sample-database-db [[:block 1]]
metabase.search.api-test/do-test-users [[:block 1]]
metabase.server.middleware.session/as-admin [[:block 0]]
metabase.server.middleware.session/with-current-user [[:inner 0]]
metabase.server.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
metabase.server.streaming-response-test/with-test-driver-db! [[:block 0]]
metabase.server.streaming-response/streaming-response [[:block 2]]
metabase.session.api/http-401-on-error [[:block 0]]
metabase.settings.core/define-multi-setting [[:inner 0]]
metabase.settings.core/defsetting [[:block 1]]
metabase.settings.models.setting.multi-setting/define-multi-setting [[:inner 0]]
metabase.settings.models.setting/defsetting [[:block 1]]
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
metabase.sync.util/sum-for [[:block 1]]
metabase.sync.util/sync-operation [[:block 3]]
metabase.sync.util/with-db-logging-disabled [[:default]]
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
metabase.sync.util/with-error-handling [[:block 1]]
metabase.sync.util/with-start-and-finish-logging [[:default]]
metabase.sync.util/with-start-and-finish-logging* [[:default]]
metabase.sync.util/with-sync-events [[:default]]
metabase.task-history.models.task-history/with-task-history [[:block 1]]
metabase.task/rerun-on-error [[:block 1]]
metabase.test.data.datasets/test-driver [[:inner 0]]
metabase.test.data.datasets/test-drivers [[:inner 0]]
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
metabase.test.data.users/with-test-user [[:inner 0]]
metabase.test.data/$ids [[:inner 0]]
metabase.test.data/dataset [[:inner 0]]
metabase.test.data/mbql-query [[:inner 0]]
metabase.test.data/native-query [[:block 0]]
metabase.test.data/query [[:block 1]]
metabase.test.data/run-mbql-query [[:inner 0]]
metabase.test.data/with-empty-h2-app-db [[:block 0]]
metabase.test.data/with-temp-copy-of-db [[:block 0]]
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
metabase.test.util.public-settings/with-premium-features [[:block 1]]
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
metabase.test.util/discard-setting-changes [[:block 1]]
metabase.test.util/let-url [[:block 1]]
metabase.test.util/throw-if-called! [[:block 1]]
metabase.test.util/with-temp-scheduler [[:block 0]]
metabase.test.util/with-temp-vals-in-db [[:block 3]]
metabase.test.util/with-user-in-groups [[:inner 0]]
metabase.test/$ids [[:inner 0]]
metabase.test/as-admin [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/discard-setting-changes [[:block 1]]
metabase.test/format-rows-by [[:default]]
metabase.test/let-url [[:block 1]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/native-query [[:block 0]]
metabase.test/query [[:block 1]]
metabase.test/run-mbql-query [[:inner 0]]
metabase.test/test-driver [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/test-helpers-set-global-values! [[:block 0]]
metabase.test/throw-if-called! [[:block 1]]
metabase.test/with-actions [[:block 1]]
metabase.test/with-actions-disabled [[:block 0]]
metabase.test/with-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data [[:inner 0]]
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data-tables [[:block 1]]
metabase.test/with-additional-premium-features [[:block 1]]
metabase.test/with-current-user [[:inner 0]]
metabase.test/with-driver [[:block 1]]
metabase.test/with-empty-db [[:inner 0]]
metabase.test/with-empty-h2-app-db [[:block 0]]
metabase.test/with-metadata-provider [[:inner 0]]
metabase.test/with-premium-features [[:block 1]]
metabase.test/with-temp [[:inner 0]]
metabase.test/with-temp-copy-of-db [[:block 0]]
metabase.test/with-temp-scheduler [[:block 0]]
metabase.test/with-temp-test-data [[:inner 0]]
metabase.test/with-temp-vals-in-db [[:block 3]]
metabase.test/with-test-user [[:inner 0]]
metabase.test/with-user-in-groups [[:inner 0]]
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
metabase.upload-test/with-upload-table! [[:inner 0]]
metabase.upload.types/with-currency [[:default]]
metabase.upload.types/with-parens [[:default]]
metabase.util.async/cancelable-thread [[:block 0]]
metabase.util.date-2/with-time-zone-same-instant [[:default]]
metabase.util.encryption-test/with-secret-key [[:block 1]]
metabase.util.honey-sql-2/with-database-type-info [[:default]]
metabase.util.i18n/deferred-trs [[:default]]
metabase.util.i18n/deferred-tru [[:default]]
metabase.util.i18n/trs [[:default]]
metabase.util.i18n/trsn [[:default]]
metabase.util.i18n/tru [[:default]]
metabase.util.i18n/trun [[:default]]
metabase.util.jvm/auto-retry [[:block 1]]
metabase.util.jvm/varargs [[:block 1]]
metabase.util.jvm/with-us-locale [[:block 0]]
metabase.util.malli/disable-enforcement [[:block 0]]
metabase.util.malli/with [[:default]]
metabase.util.malli/with-api-error-message [[:default]]
metabase.util.namespaces/import-fns [[:default]]
metabase.util.queue-test/await-test-while [[:block 1]]
metabase.util/auto-retry [[:block 1]]
metabase.util/case-enum [[:block 1]]
metabase.util/ignore-exceptions [[:block 0]]
metabase.util/or-with [[:block 1]]
metabase.util/profile [[:block 1]]
metabase.util/prog1 [[:inner 0]]
metabase.util/varargs [[:inner 0]]
metabase.util/with-us-locale [[:block 0]]
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.core/step [[:block 1]]
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.steps/step [[:block 1]]
methodical.core/defmethod [[:inner 0]]
methodical.core/defmulti [[:inner 0]]
methodical.core/with-dispatcher [[:default]]
methodical.core/with-method-table [[:default]]
methodical.core/with-prefers [[:default]]
methodical.interface/with-dispatcher [[:default]]
methodical.interface/with-method-table [[:default]]
methodical.interface/with-prefers [[:default]]
methodical.macros/defmethod [[:inner 0]]
methodical.macros/defmulti [[:inner 0]]
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
toucan2.connection/with-transaction [[:block 1]]
toucan2.core/build [[:block 0]]
toucan2.core/compile [[:block 0]]
toucan2.core/debug [[:block 0]]
toucan2.core/define-after-insert [[:inner 0]]
toucan2.core/define-after-select [[:inner 0]]
toucan2.core/define-after-update [[:inner 0]]
toucan2.core/define-before-delete [[:inner 0]]
toucan2.core/define-before-insert [[:inner 0]]
toucan2.core/define-before-select [[:inner 0]]
toucan2.core/define-default-fields [[:inner 0]]
toucan2.core/define-named-query [[:block 1]]
toucan2.core/deftransforms [[:block 1]]
toucan2.core/with-transaction [[:block 1]]
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
toucan2.tools.after-select/define-after-select [[:inner 0]]
toucan2.tools.after-update/define-after-update [[:inner 0]]
toucan2.tools.after/define-after-impl [[:default]]
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
toucan2.tools.before-select/define-before-select [[:inner 0]]
toucan2.tools.compile/build [[:block 0]]
toucan2.tools.compile/compile [[:block 0]]
toucan2.tools.compile/resolved [[:block 0]]
toucan2.tools.debug/debug [[:block 0]]
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
toucan2.tools.named-query/define-named-query [[:block 1]]
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
toucan2.tools.transformed/deftransforms [[:block 1]]
toucan2.tools.with-temp/with-temp [[:inner 0]]
toucan2.util/try-with-error-context [[:inner 0]]}}

View File

@@ -13,6 +13,9 @@ on:
type: string
commit:
type: string
release:
type: string
default: 'true'
workflow_dispatch:
inputs:
artifact-name:
@@ -56,7 +59,7 @@ jobs:
const version = "${{ inputs.tag }}".split('-')[0];
if (!isValidVersionString(version)) {
return "linux/amd64";
return "linux/amd64,linux/arm64";
}
// versions before 53 need to be handled differently
const buildInfo = getBuildRequirements(version);
@@ -66,8 +69,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
if: contains(steps.platforms.outputs.result, 'arm')
uses: docker/setup-qemu-action@v3
@@ -103,8 +106,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Pull the container image
run: docker pull ${{ inputs.repo }}:${{ inputs.tag }}
- name: Launch container
@@ -123,8 +126,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Pull the container image
run: docker pull ${{ inputs.repo }}:${{ inputs.tag }}
- name: Launch container

View File

@@ -20,11 +20,15 @@ jobs:
with:
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
script: | #js
github.rest.repos.createDispatchEvent({
owner: "${{ github.repository_owner }}",
repo: "docs.metabase.github.io",
event_type: "docs_update",
client_payload: {
branch_name: process.env.BRANCH_NAME // This needs to match the workflow file
}
});
if (process.env.BRANCH_NAME === "master" ||
/^release-x\.\d+\.x$/.test(process.env.BRANCH_NAME)) {
github.rest.repos.createDispatchEvent({
owner: "${{ github.repository_owner }}",
repo: "docs.metabase.github.io",
event_type: "docs_update",
client_payload: { branch_name: process.env.BRANCH_NAME
}
});
} else {
console.log("Branch name is not to be published: ", process.env.BRANCH_NAME);
}

View File

@@ -12,33 +12,6 @@ concurrency:
cancel-in-progress: true
jobs:
be-tests-h2:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
name: H2
steps:
- uses: actions/checkout@v4
- name: Test H2 driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-h2'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-athena-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
@@ -108,6 +81,53 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-clickhouse-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: clickhouse
MB_CLICKHOUSE_TEST_HOST: localhost
MB_CLICKHOUSE_TEST_PORT: 8123
MB_CLICKHOUSE_TEST_OLD_PORT: 8124
MB_CLICKHOUSE_TEST_NGINX_PORT: 8127
name: Clickhouse
steps:
- uses: actions/checkout@v4
- name: Add ClickHouse TLS instance to /etc/hosts
run: |
sudo echo "127.0.0.1 server.clickhouseconnect.test" | sudo tee -a /etc/hosts
- name: Start ClickHouse in Docker
uses: hoverkraft-tech/compose-action@v2.0.0
with:
compose-file: "modules/drivers/clickhouse/docker-compose.yml"
down-flags: "--volumes"
services: |
clickhouse
clickhouse_tls
clickhouse_older_version
clickhouse_cluster_node1
clickhouse_cluster_node2
nginx
- name: Test Clickhouse driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-clickhouse-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-druid-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
@@ -143,6 +163,41 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-druid-jdbc-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: druid-jdbc
services:
druid:
image: metabase/druid:29.0.1-json-no-ingestion
ports:
- "8888:8888"
env:
CLUSTER_SIZE: nano-quickstart
name: Druid (JDBC)
steps:
- uses: actions/checkout@v4
- name: Test Druid JDBC driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-jdbc-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-databricks-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
@@ -177,27 +232,54 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-druid-jdbc-ee:
be-tests-databricks-multi-catalog-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: druid-jdbc
services:
druid:
image: metabase/druid:29.0.1-json-no-ingestion
ports:
- "8888:8888"
env:
CLUSTER_SIZE: nano-quickstart
name: Druid (JDBC)
DRIVERS: databricks
MB_DATABRICKS_TEST_MULTI_LEVEL_SCHEMA: true
MB_DATABRICKS_TEST_HOST: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HOST }}
MB_DATABRICKS_TEST_HTTP_PATH: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HTTP_PATH }}
MB_DATABRICKS_TEST_TOKEN: ${{ secrets.MB_DATABRICKS_JDBC_TEST_TOKEN }}
MB_DATABRICKS_TEST_CATALOG: 'metabase_ci'
MB_DATABRICKS_TEST_CLIENT_ID: ${{ secrets.MB_DATABRICKS_JDBC_TEST_CLIENT_ID }}
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
name: Databricks (Multi-Catalog)
steps:
- uses: actions/checkout@v4
- name: Test Druid JDBC driver
- name: Test Databricks driver with multi-catalog
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-jdbc-ee'
junit-name: 'be-tests-databricks-multi-catalog-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-h2:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
name: H2
steps:
- uses: actions/checkout@v4
- name: Test H2 driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-h2'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
@@ -1004,53 +1086,6 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-clickhouse-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: clickhouse
MB_CLICKHOUSE_TEST_HOST: localhost
MB_CLICKHOUSE_TEST_PORT: 8123
MB_CLICKHOUSE_TEST_OLD_PORT: 8124
MB_CLICKHOUSE_TEST_NGINX_PORT: 8127
name: Clickhouse
steps:
- uses: actions/checkout@v4
- name: Add ClickHouse TLS instance to /etc/hosts
run: |
sudo echo "127.0.0.1 server.clickhouseconnect.test" | sudo tee -a /etc/hosts
- name: Start ClickHouse in Docker
uses: hoverkraft-tech/compose-action@v2.0.0
with:
compose-file: "modules/drivers/clickhouse/docker-compose.yml"
down-flags: "--volumes"
services: |
clickhouse
clickhouse_tls
clickhouse_older_version
clickhouse_cluster_node1
clickhouse_cluster_node2
nginx
- name: Test Clickhouse driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-clickhouse-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
drivers-tests-result:
needs:
# if you add a driver job, you must add it to this list for it to be a required check

View File

@@ -17,10 +17,13 @@ env:
TEST_SUITES: |
[
"metabase-nodejs-react-sdk-embedding-sample-e2e",
"metabase-nextjs-sdk-embedding-sample-e2e"
"metabase-nextjs-sdk-embedding-sample-e2e",
"shoppy-e2e"
]
TEST_SUITES_TO_RUN_IN_MASTER_ONLY: |
[]
[
"shoppy-e2e"
]
SAMPLE_APP_MAIN_BRANCH_NAME: "main"
SAMPLE_APP_STABLE_RELEASE_BRANCH_NAME_SUFFIX: "stable"
@@ -215,8 +218,9 @@ jobs:
TZ: US/Pacific # to make node match the instance tz
# Sample Apps testing variables
CYPRESS_CI: true
TEST_SUITE: ${{ matrix.test_suite }}
SAMPLE_APP_BRANCH_NAME: ${{ needs.get-sample-apps-data.outputs.branch_name }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
@@ -242,6 +246,7 @@ jobs:
- name: Check if sample app branch exists
id: check-sample-app-branch
run: |
TEST_SUITE="${{ matrix.test_suite }}"
REPO_NAME="${TEST_SUITE%-e2e}"
REPO_URL="https://github.com/metabase/${REPO_NAME}.git"
echo "Checking if branch '${SAMPLE_APP_BRANCH_NAME}' exists in ${REPO_URL}..."
@@ -251,9 +256,29 @@ jobs:
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Prepare and launch Sample Apps
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
run: npx tsx ./e2e/runner/start-sample-app-containers/start-ci.ts
- name: Configure AWS credentials
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.SHOPPY_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Shoppy's App DB PostgreSQL dump from S3
if: ${{ matrix.test_suite == 'shoppy-e2e' }}
run: |
aws s3 cp s3://${{ secrets.SHOPPY_METABASE_APP_DB_S3_BUCKET }}/${{ secrets.SHOPPY_METABASE_APP_DB_S3_KEY }} ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
- name: Prepare and launch Sample Apps for shoppy-e2e test suite
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite == 'shoppy-e2e' }}
env:
METABASE_APP_DB_USER: ${{ secrets.SHOPPY_METABASE_APP_DB_USER }}
METABASE_APP_DB: ${{ secrets.SHOPPY_METABASE_APP_DB_DATABASE }}
run: npx tsx ./e2e/runner/start-sample-app-containers/start-ci.ts shoppy-e2e
- name: Prepare and launch Sample Apps for other test suites
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' && matrix.test_suite != 'shoppy-e2e' }}
run: npx tsx ./e2e/runner/start-sample-app-containers/start-ci.ts ${{ matrix.test_suite }}
- name: Run e2e tests for Sample App
id: run-e2e-tests

View File

@@ -1,12 +1,14 @@
name: Build + Docker Uberjar
# This workflow builds an uberjar and pushes it to dockerhub for any branch, either on-demand,
# or automatically for the listed branches
on:
push:
branches:
- "master"
- metabot-v3-second-try
- master
- feature-visualizer
- "internal-tools"
- internal-tools
paths-ignore:
# config files
- ".**"
@@ -23,16 +25,63 @@ on:
description: "Optional full-length commit SHA-1 hash"
jobs:
get-container-info:
runs-on: ubuntu-22.04
timeout-minutes: 5
name: Get container info
outputs:
editions: ${{ toJson(fromJson(steps.info.outputs.result).editions) }}
ee: ${{ toJson(fromJson(steps.info.outputs.result).ee) }}
oss: ${{ toJson(fromJson(steps.info.outputs.result).oss) }}
steps:
- name: Get docker repo and tag
id: info
uses: actions/github-script@v7
with:
script: | # js
const branch = '${{ github.head_ref || github.ref_name }}';
const owner = '${{ github.repository_owner }}';
console.log(`Branch: ${branch}`);
if (branch === "master") {
return {
editions: ["ee", "oss"],
ee: {
repo: `${owner}/metabase-enterprise-head`,
tag: "latest"
},
oss: {
repo: `${owner}/metabase-head`,
tag: "latest"
}
};
}
return {
editions: ["ee"],
ee: {
repo: `${owner}/metabase-dev`,
tag: branch.replaceAll("/", "-"), // slashes make docker sad
},
};
- name: Print output
run: |
echo "${{ steps.info.outputs.result }}"
build:
needs: get-container-info
name: Build MB ${{ matrix.edition }}
runs-on: ubuntu-22.04
timeout-minutes: 15
strategy:
matrix:
edition: [ee, oss]
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
env:
MB_EDITION: ${{ matrix.edition }}
INTERACTIVE: false
SKIP_LICENSES: true # faster builds for dev
steps:
- name: Check out the code
uses: actions/checkout@v4
@@ -50,16 +99,16 @@ jobs:
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
with:
name: metabase-${{ matrix.edition }}-${{ github.sha }}-uberjar
name: metabase-${{ matrix.edition }}-${{ github.event.inputs.commit || github.sha }}-uberjar
check_jar_health:
check-jar-health:
runs-on: ubuntu-22.04
name: Is ${{ matrix.edition }} (java ${{ matrix.java-version }}) healthy?
needs: build
needs: [build, get-container-info]
timeout-minutes: 10
strategy:
matrix:
edition: [ee, oss]
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
java-version: [21]
steps:
- name: Prepare JRE (Java Run-time Environment)
@@ -72,261 +121,51 @@ jobs:
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-${{ matrix.edition }}-${{ github.sha }}-uberjar
name: metabase-${{ matrix.edition }}-${{ github.event.inputs.commit || github.sha }}-uberjar
- name: Launch uberjar
run: >-
java --add-opens java.base/java.nio=ALL-UNNAMED -jar ./target/uberjar/metabase.jar &
- name: Wait for Metabase to start
run: while ! curl 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
containerize_test_and_push_container:
runs-on: ubuntu-22.04
name: Containerize ${{ matrix.edition }}
needs: check_jar_health
containerize:
needs: [check-jar-health, get-container-info]
strategy:
matrix:
edition: [ee, oss]
fail-fast: false
services:
registry:
image: registry:2
ports:
- 5000:5000
permissions:
id-token: write
contents: read
security-events: write
steps:
- name: Extract and clean branch name
shell: bash
run: echo "branch=$(echo $GITHUB_REF_NAME | sed 's/[^-._a-zA-Z0-9]/-/g')" >> $GITHUB_OUTPUT
id: extract_branch
- name: Verify the intended tag of the container image
run: echo "Container image will be tagged as ${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}"
- name: Check out the code (Dockerfile needed)
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit }}
- name: Download uploaded artifacts to insert into container
uses: actions/download-artifact@v4
with:
name: metabase-${{ matrix.edition }}-${{ github.sha }}-uberjar
path: bin/docker/
- name: Move the ${{ matrix.edition }} uberjar to the context dir
run: mv bin/docker/target/uberjar/metabase.jar bin/docker/.
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.5.0
with:
driver-opts: network=host
- name: Build ${{ matrix.edition }} container
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
network: host
tags: localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: true
push: true
- name: Launch ${{ matrix.edition }} container
run: docker run --rm -dp 3000:3000 localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}
timeout-minutes: 5
- name: Is Docker running?
run: docker ps
- name: Wait for Metabase to start and reach 100% health
run: while ! curl '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_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
uses: ./.github/workflows/containerize-jar.yml
secrets: inherit
with:
artifact-name: metabase-${{ matrix.edition }}-${{ github.event.inputs.commit || github.sha }}-uberjar
commit: ${{ github.event.inputs.commit || github.sha }}
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
tag: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).tag }}
release: "false"
- name: Retag and push images if master (ee) - should be cached at this point
if: ${{ (github.ref_name == 'master') && matrix.edition == 'ee' }}
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
network: host
tags: ${{ github.repository_owner }}/metabase-enterprise-head:latest
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: false
push: true
# TODO: remove when we're done testing metabot
- name: Retag and push images if metabot-v3-second-try (ee) - should be cached at this point
if: ${{ (github.ref_name == 'metabot-v3-second-try') && matrix.edition == 'ee' }}
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
network: host
tags: ${{ github.repository_owner }}/metabase-enterprise-head:metabot
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: false
push: true
- name: Retag and push images if master (oss) - should be cached at this point
if: ${{ (github.ref_name == 'master') && matrix.edition == 'oss' }}
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
network: host
tags: ${{ github.repository_owner }}/metabase-head:latest
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: false
push: true
- name: Retag and push images if dev branch - should be cached at this point
if: ${{ !(startsWith(github.ref_name,'master') || startsWith(github.ref_name,'backport')) && matrix.edition == 'ee' }}
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
network: host
tags: ${{ github.repository_owner }}/metabase-dev:${{ steps.extract_branch.outputs.branch }}
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: false
push: true
- name: Run Trivy vulnerability scanner if master (ee)
if: ${{ (github.ref_name == 'master') && matrix.edition == 'ee' }}
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_OFFLINE_SCAN: true
with:
image-ref: docker.io/${{ github.repository_owner }}/metabase-enterprise-head:latest
format: sarif
output: trivy-results.sarif
version: "v0.57.1"
- name: Run Trivy vulnerability scanner if master (oss)
if: ${{ (github.ref_name == 'master') && matrix.edition == 'oss' }}
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_OFFLINE_SCAN: true
with:
image-ref: docker.io/${{ github.repository_owner }}/metabase-head:latest
format: sarif
output: trivy-results.sarif
version: "v0.57.1"
- name: Run Trivy vulnerability scanner if dev branch
if: ${{ !(startsWith(github.ref_name,'master') || startsWith(github.ref_name,'backport')) && matrix.edition == 'ee' }}
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_OFFLINE_SCAN: true
with:
version: "v0.57.1"
image-ref: docker.io/${{ github.repository_owner }}/metabase-dev:${{ steps.extract_branch.outputs.branch }}
format: sarif
output: trivy-results.sarif
- name: Upload Trivy scan results to GitHub Security tab if master (ee)
if: ${{ (github.ref_name == 'master') && matrix.edition == 'ee' }}
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: "trivy-results.sarif"
- name: Upload Trivy scan results to GitHub Security tab if master (oss)
if: ${{ (github.ref_name == 'master') && matrix.edition == 'oss' }}
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: "trivy-results.sarif"
- name: Upload Trivy scan results to GitHub Security tab if dev branch
if: ${{ !(startsWith(github.ref_name,'master') || startsWith(github.ref_name,'backport')) && matrix.edition == 'ee' }}
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: "trivy-results.sarif"
containerize_multi_arch:
runs-on: ubuntu-22.04
name: Containerize multi-arch ${{ matrix.edition }}
needs: check_jar_health
run-trivy:
needs: [get-container-info, containerize]
strategy:
matrix:
edition: [ee, oss]
services:
registry:
image: registry:2
ports:
- 5000:5000
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
runs-on: ubuntu-22.04
name: Run Trivy vulnerability scanner
steps:
- name: Extract and clean branch name
shell: bash
run: echo "branch=$(echo $GITHUB_REF_NAME | sed 's/[^-._a-zA-Z0-9]/-/g')" >> $GITHUB_OUTPUT
id: extract_branch
- name: Verify the intended tag of the container image
run: echo "Container image will be tagged as ${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}"
- name: Check out the code (Dockerfile needed)
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit }}
- name: Download uploaded artifacts to insert into container
uses: actions/download-artifact@v4
ref: ${{ github.event.inputs.commit || github.sha }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.29.0
env:
TRIVY_OFFLINE_SCAN: true
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
tag: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).tag }}
with:
name: metabase-${{ matrix.edition }}-${{ github.sha }}-uberjar
path: bin/docker/
- name: Move the ${{ matrix.edition }} uberjar to the context dir
run: mv bin/docker/target/uberjar/metabase.jar bin/docker/.
# We need it for multi-arch build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
image-ref: docker.io/${{ env.repo }}:${{ env.tag }}
format: sarif
output: trivy-results.sarif
version: "v0.57.1"
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
platforms: "arm64"
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.5.0
with:
driver-opts: network=host
# Build experimental ubuntu-based images only for master
- name: Build ${{ matrix.edition }} Ubuntu based multi-arch container
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64,linux/arm64
file: bin/docker/Dockerfile_ubuntu
network: host
tags: localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}-ubuntu
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
no-cache: true
push: true
- name: Launch ${{ matrix.edition }} Ubuntu based container
run: docker run --rm -dp 3001:3000 localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-${{ matrix.edition }}-ubuntu
timeout-minutes: 5
- name: Is Docker with Ubuntu running?
run: docker ps
- name: Wait for Ubuntu-based Metabase container to start and reach 100% health
run: while ! curl 'http://localhost:3001/api/health' | grep '{"status":"ok"}'; do sleep 1; done
timeout-minutes: 3
- name: Login to Docker Hub
if: ${{ github.ref_name == 'master' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Push experimental ubuntu image only for versions based on a master
- name: Install regctl
if: ${{ github.ref_name == 'master' }}
uses: regclient/actions/regctl-installer@main
with:
release: "v0.4.7"
- name: Switch regctl to point to localhost:5000 via http
if: ${{ github.ref_name == 'master' }}
run: regctl registry set --tls disabled localhost:5000
- name: Retag and push ubuntu-based images if master (ee)
if: ${{ (matrix.edition == 'ee') && (github.ref_name == 'master') }}
run: regctl image copy localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-ee-ubuntu ${{ github.repository_owner }}/metabase-enterprise-head:latest-ubuntu
- name: Retag and push ubuntu-based images if master (oss)
if: ${{ (matrix.edition == 'oss') && (github.ref_name == 'master') }}
run: regctl image copy localhost:5000/metabase-dev:${{ steps.extract_branch.outputs.branch }}-oss-ubuntu ${{ github.repository_owner }}/metabase-head:latest-ubuntu
sarif_file: "trivy-results.sarif"

3
.gitignore vendored
View File

@@ -43,6 +43,9 @@
/frontend/test/snapshots/*
/e2e/snapshots/*
/e2e/tmp/*
!/e2e/tmp/db_dumps
/e2e/tmp/db_dumps/*
!/e2e/tmp/db_dumps/.gitkeep
/lein-plugins/*/target
/local
/locales/metabase-*.pot

View File

@@ -0,0 +1,41 @@
// Copied from the instance settings and re-ordered, may go out of sync with the real ones
// Locales in the format `xx_AA` were also changed to `xx-AA`
export const availableLocales = [
{ value: undefined, title: "default (instance locale)" },
{ value: "en", right: "🇺🇸", title: "en" },
{ value: "de", right: "🇩🇪", title: "de" }, // one that we know should be well supported
{ value: "ar", right: "🇸🇦", title: "ar" },
{ value: "ar-SA", right: "🇸🇦", title: "ar-SA" },
{ value: "bg", right: "🇧🇬", title: "bg" },
{ value: "ca", right: "🇪🇺", title: "ca" },
{ value: "cs", right: "🇨🇿", title: "cs" },
{ value: "da", right: "🇩🇰", title: "da" },
{ value: "es", right: "🇪🇺", title: "es" },
{ value: "fa", right: "🇮🇷", title: "fa" },
{ value: "fi", right: "🇫🇮", title: "fi" },
{ value: "fr", right: "🇫🇷", title: "fr" },
{ value: "he", right: "🇮🇱", title: "he" },
{ value: "hu", right: "🇭🇺", title: "hu" },
{ value: "id", right: "🇮🇩", title: "id" },
{ value: "it", right: "🇮🇹", title: "it" },
{ value: "ja", right: "🇯🇵", title: "ja" },
{ value: "ko", right: "🇰🇷", title: "ko" },
{ value: "lv", right: "🇱🇻", title: "lv" },
{ value: "ms", right: "🇲🇾", title: "ms" },
{ value: "nb", right: "🇳🇴", title: "nb" },
{ value: "nl", right: "🇳🇱", title: "nl" },
{ value: "pl", right: "🇵🇱", title: "pl" },
{ value: "pt-BR", right: "🇧🇷", title: "pt-BR" },
{ value: "ru", right: "🇷🇺", title: "ru" },
{ value: "sk", right: "🇸🇰", title: "sk" },
{ value: "sl", right: "🇸🇱", title: "sl" },
{ value: "sq", right: "🇦🇱", title: "sq" },
{ value: "sr", right: "🇷🇸", title: "sr" },
{ value: "sv", right: "🇸🇪", title: "sv" },
{ value: "tr", right: "🇹🇷", title: "tr" },
{ value: "uk", right: "🇺🇦", title: "uk" },
{ value: "vi", right: "🇻🇳", title: "vi" },
{ value: "zh-CN", right: "🇨🇳", title: "zh-CN" },
{ value: "zh-HK", right: "🇭🇰", title: "zh-HK" },
{ value: "zh-TW", right: "🇹🇼", title: "zh-TW" },
];

View File

@@ -5,6 +5,7 @@ import { GlobalTypes } from "@storybook/types";
window.METABASE_REMOVE_DELAYS = true;
import { storybookThemeOptions } from "embedding-sdk/test/storybook-themes";
import { availableLocales } from "./constants";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
@@ -41,4 +42,15 @@ export const globalTypes: GlobalTypes = {
dynamicTitle: true,
},
},
locale: {
name: "Locale",
description: "Locale to be passed to the MetabaseProvider",
defaultValue: undefined,
toolbar: {
icon: "globe",
items: availableLocales,
showName: true,
dynamicTitle: true,
},
},
};

View File

@@ -247,7 +247,7 @@
{:mvn/version "1.6.1"} ; Enables local profiling and heat map generation
com.clojure-goes-fast/clj-memory-meter
{:mvn/version "0.4.0"} ; Enables easy memory measurement
com.gfredericks/test.chuck {:mvn/version "0.2.14"} ; generating strings from regexes (useful with malli)
com.gfredericks/test.chuck {:mvn/version "0.2.15"} ; generating strings from regexes (useful with malli)
criterium/criterium {:mvn/version "0.4.6"} ; benchmarking library
dev.weavejester/hashp {:mvn/version "0.4.0"} ; debugging/spying utility
djblue/portal {:mvn/version "0.59.0"} ; ui for inspecting values

View File

@@ -21,7 +21,7 @@
;; - [Drivers](#metabase.driver)
;; - [Permissions](#metabase.models.permissions)
;; - [The Query Processor](#metabase.query-processor)
;; - [Application Settings](#metabase.models.setting)
;; - [Application Settings](#metabase.settings.models.setting)
;;
;; ## Important Libraries
;;
@@ -72,11 +72,11 @@
[metabase.driver :as driver]
[metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
[metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
[metabase.models.setting :as setting]
[metabase.query-processor.compile :as qp.compile]
[metabase.query-processor.timezone :as qp.timezone]
[metabase.server.handler :as handler]
[metabase.server.instance :as server]
[metabase.settings.core :as setting]
[metabase.sync.core :as sync]
[metabase.test :as mt]
[metabase.test-runner]

View File

@@ -220,15 +220,19 @@
static-deps (ns.parse/deps-from-ns-decl decl)
dynamic-deps (for [symb (find-dynamically-loaded-namespaces file)]
(vary-meta symb assoc ::dynamic :require-and-friends))
defenterprise-deps (for [symb (find-defenterprises file)]
(vary-meta symb assoc ::dynamic :defenterprise))
defenterprise-schema-deps (for [symb (find-defenterprise-schemas file)]
(vary-meta symb assoc ::dynamic :defenterprise-schema))
;;
;; excluded from the diff for now, see https://metaboat.slack.com/archives/C0669P4AF9N/p1745875106092029 for
;; rationale.
;;
;; defenterprise-deps (for [symb (find-defenterprises file)]
;; (vary-meta symb assoc ::dynamic :defenterprise))
;; defenterprise-schema-deps (for [symb (find-defenterprise-schemas file)]
;; (vary-meta symb assoc ::dynamic :defenterprise-schema))
deps (into (sorted-set) cat
[static-deps
dynamic-deps
defenterprise-deps
defenterprise-schema-deps])]
#_defenterprise-deps
#_defenterprise-schema-deps])]
{:namespace ns-symb
:module (module ns-symb)
:deps (sort-by pr-str

View File

@@ -8,12 +8,18 @@ title: Driver interface changelog
- Added a feature `:expression-literals` for drivers that support expressions consisting of a single string, number, or boolean literal value.
- Added a feature `:multi-level-schema` for drivers that support hierarchical levels between database and schema. Such as databricks' catalog. Defaults to false.
- Added the multi-method `adjust-schema-qualification` that allows drivers to to qualify, or unqualify table schemas based on enabling or disabling multi-level-schema support. Drivers may need to implement `sql.qp/->honeysql [driver ::h2x/identifier]` to properly quote fully qualified schemas.
- Added a new abstract driver mix-in `:metabase.driver.sql.query-processor.boolean-is-comparison/boolean-is-comparison` that can be inherited by drivers that require boolean literal constants to be converted to comparison expressions in the top level of WHERE, AND, OR, NOT, and CASE clauses.
- Added the multi-method `float-dbtype` which returns the name of the float type we coerce to for coercion strategies and the `float()` custom expression function.
- Added a feature `:expressions/float` for drivers that support casting text to floats.
- Added the multi-method `integer-dbtype` that allows the driver to control which type integers are cast to.
## Metabase 0.54.0
- Added the multi-method `allowed-promotions` that allows driver control over which column type promotions are supported for uploads.

View File

@@ -189,6 +189,8 @@ In order to run the tests locally, see [sdk docs about e2e](https://github.com/m
In order to check compatibility between Sample Apps and Embedding SDK, we have a special test suite for each sample app that pulls this Sample App, starts it and runs its Cypress tests against the local `metabase.jar` and local `@metabase/embedding-sdk-react` package.
#### Local runs
To run these tests locally, run:
```
ENTERPRISE_TOKEN=<token> TEST_SUITE=<sample_app_repo_name>-e2e OPEN_UI=false EMBEDDING_SDK_VERSION=local START_METABASE=false GENERATE_SNAPSHOTS=false START_CONTAINERS=false yarn test-cypress
@@ -199,6 +201,16 @@ For example for the `metabase-nodejs-react-sdk-embedding-sample`, run:
ENTERPRISE_TOKEN=<token> TEST_SUITE=metabase-nodejs-react-sdk-embedding-sample-e2e OPEN_UI=false EMBEDDING_SDK_VERSION=local START_METABASE=false GENERATE_SNAPSHOTS=false START_CONTAINERS=false yarn test-cypress
```
##### :warning: Obtaining the Shoppy's Metabase App DB Dump locally
For the Shoppy's Sample App Tests (`TEST_SUITE=shoppy-e2e`) locally, a proper App DB dump of the Shoppy's Metabase Instance must be placed to the `./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql`
You can get it by:
- Enabling the `Tailscale` and logging in using your work email address.
- Running `pg_dump "postgres://{{ username }}:{{ password }}@{{ host }}:{{ port }}/{{ database }}" > ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql` command.
- See the `Shoppy Coredev Appdb` record in `1password` for credentials.
#### CI runs
On our CI, test failures do not block the merging of a pull request (PR). However, if a test fails, it’s most likely due to one of the following reasons:
- **Build Failure**:

View File

@@ -109,7 +109,7 @@ You can add events to collections, and organize those events into timelines. See
## Uploading data
You can upload data to collections. See [Uploading data](./uploads.md)]
You can upload data to collections. See [Uploading data](./uploads.md).
## Further reading

View File

@@ -123,12 +123,9 @@ To view your conditional counts by plan, set the **Group by** column to "Plan".
## Related functions
Different ways to do the same thing, because it's fun to try new things.
**Metabase**
- [case](#case)
- [CumulativeCount](#cumulativecount)
- [Conditional running counts](#conditional-running-counts)
**Other tools**
@@ -136,43 +133,10 @@ Different ways to do the same thing, because it's fun to try new things.
- [Spreadsheets](#spreadsheets)
- [Python](#python)
### case
You can combine [`Count`](../expressions-list.md#count) with [`case`](./case.md):
### Conditional running counts
```
Count(case([Plan] = "Basic", [ID]))
```
to do the same thing as `CountIf`:
```
CountIf([Plan] = "Basic")
```
The `case` version lets you count a different column when the condition isn't met. For example, if you've got data from different sources:
| ID: Source A | Plan: Source A | ID: Source B | Plan: Source B |
| ------------ | -------------- | ------------ | -------------- |
| 1 | Basic | | |
| | | B | basic |
| | | C | basic |
| 4 | Business | D | business |
| 5 | Premium | E | premium |
To count the total number of Basic plans across both sources, you could create a `case` expression to:
- Count the rows in "ID: Source A" where "Plan: Source A = "Basic"
- Count the rows in "ID: Source B" where "Plan: Source B = "basic"
```
Count(case([Plan: Source A] = "Basic", [ID: Source A],
case([Plan: Source B] = "basic", [ID: Source B])))
```
### CumulativeCount
`CountIf` doesn't do running counts. You'll need to combine [CumulativeCount](../expressions-list.md#cumulativecount) with [`case`](./case.md).
`CountIf` doesn't do running counts, and [CumulativeCount](../expressions/cumulative) doesn't accept conditions (or any other arguments) . To do conditional running counts, you'll need to be creative: combine [CumulativeSum](../expressions/cumulative) (not `CumulativeCount`! ) with [`case`](./case.md). The idea is to use `case` to return `1` when a condition is satisfied and `0` when it isn't, then compute the running sum of all the 1's.
If our sample data is a time series:
@@ -194,7 +158,7 @@ And we want to get the running count of active plans like this:
Create an aggregation from **Summarize** > **Custom expression**:
```
CumulativeCount(case([Active Subscription] = true, [ID]))
CumulativeSum(case([Active Subscription] = true, 1,0))
```
You'll also need to set the **Group by** column to "Created Date: Month".

View File

@@ -0,0 +1,3 @@
import path from "path";
export const E2E_TMP_FOLDER_PATH = path.resolve(__dirname, "../../../tmp");

View File

@@ -1,10 +1,7 @@
import fs from "fs";
import path from "path";
import { shell } from "../../cypress-runner-utils";
const E2E_TMP_FOLDER_PATH = path.resolve(__dirname, "../../../tmp");
import { E2E_TMP_FOLDER_PATH } from "../constants/e2e-tmp-folder-path";
export function fetchApp({
appName,

View File

@@ -3,6 +3,7 @@ import fs from "fs";
import * as path from "path";
const ROOT_FOLDER_PATH = path.resolve(__dirname, "../../../..");
import { E2E_TMP_FOLDER_PATH } from "../constants/e2e-tmp-folder-path";
const METABASE_JAR_DIST_PATH = path.join(ROOT_FOLDER_PATH, "target/uberjar");
@@ -29,17 +30,31 @@ export function copyExampleEnvFile({
}
export function copyLocalMetabaseJar(rootPath: string) {
const metabaseJarToPath = path.join(rootPath, LOCAL_DIST_PATH);
const destinationPath = path.join(rootPath, LOCAL_DIST_PATH);
fs.cpSync(METABASE_JAR_DIST_PATH, metabaseJarToPath, { recursive: true });
fs.cpSync(METABASE_JAR_DIST_PATH, destinationPath, { recursive: true });
}
export function copyLocalEmbeddingSdkPackage(rootPath: string) {
const embeddingSdkToPath = path.join(
rootPath,
LOCAL_DIST_PATH,
"embedding-sdk",
const destinationPath = path.join(rootPath, LOCAL_DIST_PATH, "embedding-sdk");
fs.cpSync(EMBEDDING_SDK_DIST_PATH, destinationPath, { recursive: true });
}
// See `docs/developers-guide/e2e-tests.md` for more info
export function copyShoppyMetabaseAppDBDump(rootPath: string) {
const sourcePath = path.join(
E2E_TMP_FOLDER_PATH,
"db_dumps/shoppy_metabase_app_db_dump.sql",
);
fs.cpSync(EMBEDDING_SDK_DIST_PATH, embeddingSdkToPath, { recursive: true });
// The dump must be copied to the Shoppy's `local-dist` directory.
// After that it will be used by the Shoppy's Docker container when it is built and started.
const destinationPath = path.join(
rootPath,
LOCAL_DIST_PATH,
"metabase_dump.sql",
);
fs.cpSync(sourcePath, destinationPath);
}

View File

@@ -2,4 +2,10 @@ import type { SampleAppTestSuiteName } from "../sample-apps-shared/types";
import { startSampleAppContainers } from "./startSampleAppContainers";
startSampleAppContainers(process.env.TEST_SUITE as SampleAppTestSuiteName);
const testSuite = process.argv?.[2]?.trim() as SampleAppTestSuiteName;
if (!testSuite) {
throw new Error("Test suite parameter is required");
}
startSampleAppContainers(testSuite);

View File

@@ -6,6 +6,7 @@ import {
copyExampleEnvFile,
copyLocalEmbeddingSdkPackage,
copyLocalMetabaseJar,
copyShoppyMetabaseAppDBDump,
} from "../sample-apps-shared/helpers/prepare-app";
import { setupAppCleanup } from "../sample-apps-shared/helpers/setup-app-cleanup";
import { startContainers } from "../sample-apps-shared/helpers/start-containers";
@@ -59,6 +60,10 @@ export async function startSampleAppContainers(
copyLocalEmbeddingSdkPackage(rootPath);
}
if (testSuite === "shoppy-e2e") {
copyShoppyMetabaseAppDBDump(rootPath);
}
await startContainers({
cwd: rootPath,
env,

View File

@@ -0,0 +1,29 @@
import { CollectionBrowser } from "@metabase/embedding-sdk-react";
import { signInAsAdminAndEnableEmbeddingSdk } from "e2e/support/helpers/component-testing-sdk";
import {
mockAuthProviderAndJwtSignIn,
mountSdkContent,
} from "e2e/support/helpers/component-testing-sdk/component-embedding-sdk-helpers";
describe("scenarios > embedding-sdk > requests", () => {
describe("cache preflight requests", () => {
beforeEach(() => {
signInAsAdminAndEnableEmbeddingSdk();
cy.signOut();
mockAuthProviderAndJwtSignIn();
});
it("caches preflight requests by setting Access-Control-Max-Age header", () => {
cy.intercept("GET", "/api/user/current").as("getCurrentUser");
mountSdkContent(<CollectionBrowser />);
cy.wait("@getCurrentUser").then(({ response }) => {
const maxAgeHeader = response?.headers["access-control-max-age"];
expect(maxAgeHeader).to.equal("60");
});
});
});
});

View File

@@ -75,6 +75,27 @@ const INTEGER_TEST_CASES: CastTestCase[] = [
filterValue: "29",
expectedRowCount: 1,
},
{
name: "FloatLiteral",
expression: "integer(10.4)",
filterOperator: "Equal to",
filterValue: "10",
expectedRowCount: 200,
},
{
name: "FloatColumn",
expression: "integer([Price])",
filterOperator: "Equal to",
filterValue: "29",
expectedRowCount: 4,
},
{
name: "FloatExpression",
expression: "integer(42.333 + 0.56)",
filterOperator: "Equal to",
filterValue: "43",
expectedRowCount: 200,
},
];
const DATE_TEST_CASES: CastTestCase[] = [

View File

@@ -682,10 +682,8 @@ describe("issue 25990", () => {
cy.findByText("People").click();
cy.findByText("ID").click();
cy.findByPlaceholderText("Enter an ID").type("10").blur();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
cy.findByTestId("qb-filters-panel")
@@ -1208,9 +1206,8 @@ describe("issue 45252", { tags: "@external" }, () => {
H.popover().within(() => {
cy.findByText("Binary").click();
cy.findByLabelText("Is empty").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(0);
@@ -1226,9 +1223,8 @@ describe("issue 45252", { tags: "@external" }, () => {
H.popover().within(() => {
cy.findByText("Jsonb").click();
cy.findByLabelText("Not empty").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(2);
});

View File

@@ -94,9 +94,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("Quantity").click();
cy.findByText("20").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Quantity is equal to 20")
@@ -124,9 +124,9 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Summaries").click();
cy.findByText("Count").click();
cy.findByPlaceholderText("Min").type("500");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Count is greater than or equal to 500")
.should("be.visible");
@@ -140,9 +140,9 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Product").click();
cy.findByText("Category").click();
cy.findByText("Gadget").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Product → Category is Gadget")
.should("be.visible");
@@ -184,6 +184,7 @@ describe("scenarios > filters > bulk filtering", () => {
it("should be able to add and remove filters for all query stages", () => {
H.visitQuestionAdhoc(multiStageQuestionDetails);
cy.get("@dataset.all").should("have.length", 1);
cy.log("add filters for all stages in the filter modal");
cy.log("stage 0");
@@ -191,33 +192,37 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("Category").click();
cy.findByText("Gadget").click();
cy.button("Add filter").click();
cy.button("Add another filter").click();
});
cy.get("@dataset.all").should("have.length", 1);
cy.log("stage 1");
H.filter();
H.popover().within(() => {
cy.findByText("Summaries").click();
cy.findByText("Category").click();
cy.findByText("Widget").click();
cy.button("Add filter").click();
cy.button("Add another filter").click();
});
cy.get("@dataset.all").should("have.length", 1);
cy.log("stage 2");
H.filter();
H.popover().within(() => {
cy.findByText("Summaries (2)").click();
cy.findByText("Category").click();
cy.findByText("Gizmo").click();
cy.button("Add filter").click();
cy.button("Add another filter").click();
});
cy.get("@dataset.all").should("have.length", 1);
cy.log("stage 3");
H.filter();
H.popover().within(() => {
cy.findByText("Summaries (3)").click();
cy.findByText("Category").click();
cy.findByText("Doohickey").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
cy.get("@dataset.all").should("have.length", 2);
cy.log("check filters from all stages to be present in the filter panel");
H.queryBuilderFiltersPanel().within(() => {
@@ -295,7 +300,7 @@ describe("scenarios > filters > bulk filtering", () => {
});
H.popover().findByText(SEGMENT_2_NAME).click();
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText(SEGMENT_2_NAME)
.should("be.visible");
@@ -343,9 +348,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("boolean").click();
cy.findByText("True").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(2);
});
@@ -353,9 +358,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("boolean").click();
cy.findByText("True").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(2);
H.queryBuilderFiltersPanel().findByText("boolean is true").click();
@@ -371,9 +376,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("boolean").click();
cy.findByText("True").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(2);
H.queryBuilderFiltersPanel()
@@ -396,7 +401,7 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Created At").click();
cy.findByText("Today").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Created At is today")
@@ -408,7 +413,7 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Created At").click();
cy.findByText("Previous 3 months").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Created At is in the previous 3 months")
.should("be.visible");
@@ -425,9 +430,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("Source").click();
cy.findByText("Affiliate").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Source is Affiliate")
.should("be.visible");
@@ -438,9 +443,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("State").click();
cy.findByText("AZ").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("State is AZ")
.should("be.visible");
@@ -458,9 +463,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("ID").click();
cy.findByLabelText("Filter value").type("17,18");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(2);
H.tableInteractive().within(() => {
cy.findByText("131.68").should("be.visible"); // total for order id 17
@@ -472,9 +477,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("Product ID").click();
cy.findByLabelText("Filter value").type("65");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(107);
});
});
@@ -490,9 +495,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.selectFilterOperator("Contains");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("Indian");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(5);
});
@@ -501,9 +506,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.selectFilterOperator("Ends with");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("Valley");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.assertQueryBuilderRowCount(8);
});
@@ -513,9 +518,9 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByLabelText("Filter value")
.type("Indiantown,Indian Valley")
.blur();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("City is 2 selections")
.should("be.visible");
@@ -534,9 +539,9 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Price").click();
cy.findByPlaceholderText("Min").type("50");
cy.findByPlaceholderText("Max").type("80");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Price is between 50 and 80")
.should("be.visible");
@@ -548,9 +553,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.selectFilterOperator("Greater than");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("50");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Price is greater than 50")
.should("be.visible");
@@ -561,9 +566,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.popover().within(() => {
cy.findByText("Price").click();
cy.findByPlaceholderText("Max").type("50");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Price is less than or equal to 50")
.should("be.visible");
@@ -597,9 +602,9 @@ describe("scenarios > filters > bulk filtering", () => {
H.selectFilterOperator("Greater than");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("90");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
applyFilters();
cy.wait("@dataset");
H.queryBuilderFiltersPanel()
.findByText("Price is greater than 90")
.should("be.visible");
@@ -607,8 +612,3 @@ describe("scenarios > filters > bulk filtering", () => {
});
});
});
const applyFilters = () => {
H.runButtonOverlay().click();
cy.wait("@dataset");
};

View File

@@ -106,9 +106,8 @@ describe("scenarios > question > filter", () => {
H.popover().within(() => {
cy.findByText("Product ID").click();
cy.findByText("Aerodynamic Linen Coat").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.log("Reported failing on v0.36.4 and v0.36.5.1");
cy.findByTestId("loading-indicator").should("not.exist");
@@ -738,9 +737,8 @@ describe("scenarios > question > filter", () => {
H.popover().within(() => {
cy.findByText("Category").click();
cy.findByText("Gizmo").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
H.openNotebook();
H.verifyNotebookQuery("Products", [

View File

@@ -1330,9 +1330,8 @@ describe("issue 45300", () => {
cy.findAllByText("Product").should("have.length", 2).first().click();
cy.findByText("Category").click();
cy.findByText("Doohickey").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
cy.findByTestId("filter-pill").should(

View File

@@ -74,9 +74,8 @@ describe("scenarios > metrics > question", () => {
cy.findByText("Product").click();
cy.findByText("Category").click();
cy.findByText("Gadget").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.findByTestId("scalar-container")
.findByText("4,939")
.should("be.visible");

View File

@@ -393,9 +393,8 @@ describe("scenarios > models metadata", () => {
cy.findByText("User").click();
cy.findByText("Source").click();
cy.findByText("Twitter").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
cy.findByTestId("question-row-count")
.invoke("text")

View File

@@ -52,9 +52,8 @@ describe("scenarios > models", () => {
H.selectFilterOperator("Contains");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("Fisher");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
assertQuestionIsBasedOnModel({
@@ -103,9 +102,8 @@ describe("scenarios > models", () => {
H.selectFilterOperator("Contains");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("Fisher");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
assertQuestionIsBasedOnModel({
@@ -170,9 +168,8 @@ describe("scenarios > models", () => {
H.selectFilterOperator("Greater than");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("30");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset").then(({ response }) => {
expect(response.body.error).to.not.exist;
});
@@ -354,8 +351,8 @@ describe("scenarios > models", () => {
.and("contain.text", "Orders");
cy.findByText("Everywhere").click();
getResults().should("have.length", 6);
cy.findByText("6 results").should("be.visible");
getResults().should("have.length", 5);
cy.findByText("5 results").should("be.visible");
getResults()
.eq(0)
.should("have.attr", "data-model-type", "dataset")
@@ -374,10 +371,6 @@ describe("scenarios > models", () => {
.and("contain.text", "Orders, Count");
getResults()
.eq(4)
.should("have.attr", "data-model-type", "card")
.and("contain.text", "Orders");
getResults()
.eq(5)
.should("have.attr", "data-model-type", "table")
.and("contain.text", "Orders");
});
@@ -465,8 +458,7 @@ describe("scenarios > models", () => {
H.filter();
H.popover().findByText("Discount").click();
H.selectFilterOperator("Not empty");
H.popover().button("Add filter").click();
H.runButtonOverlay().click();
H.popover().button("Apply filter").click();
cy.wait("@dataset");
assertQuestionIsBasedOnModel({

View File

@@ -613,7 +613,6 @@ describe(
cy.findByText("Created At").click();
cy.findByText("Today").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
cy.get("[data-testid=cell-data]")
.should("have.length", 4)
@@ -956,9 +955,8 @@ describe("issue 28971", () => {
H.popover().within(() => {
cy.findByText("Quantity").click();
cy.findByText("20").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Quantity is equal to 20").should("exist");
@@ -997,9 +995,8 @@ describe("issue 28971", () => {
H.popover().within(() => {
cy.findByText("Quantity").click();
cy.findByText("20").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Quantity is equal to 20").should("exist");
@@ -2133,7 +2130,6 @@ describe("cumulative count - issue 33330", () => {
cy.findByText("Created At").click();
cy.findByText("Today").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
H.queryBuilderHeader().findByLabelText("Show filters").click();

View File

@@ -32,12 +32,7 @@ export function addWidgetStringFilter(
}
export function setWidgetStringFilter(value) {
popover()
.first()
.find("input")
.not("[type=hidden]")
.first()
.type(`${value}{enter}`);
popover().first().find("input").not("[type=hidden]").first().type(`${value}`);
}
/**
@@ -166,10 +161,10 @@ export function closeEntryForm() {
*/
function addBetweenFilter([low, high] = [], buttonLabel = "Add filter") {
popover().within(() => {
cy.get("input").first().type(`${low}{enter}`);
cy.get("input").first().type(`${low}`);
// eslint-disable-next-line no-unsafe-element-filtering
cy.get("input").last().type(`${high}{enter}`);
cy.get("input").last().type(`${high}`);
});
cy.button(buttonLabel).click();
@@ -180,7 +175,7 @@ function addBetweenFilter([low, high] = [], buttonLabel = "Add filter") {
* @param {string} value
*/
function addSimpleNumberFilter(value, buttonLabel = "Add filter") {
cy.findByPlaceholderText("Enter a number").type(`${value}{enter}`);
cy.findByPlaceholderText("Enter a number").type(`${value}`);
cy.button(buttonLabel).click();
}
@@ -190,9 +185,7 @@ function addSimpleNumberFilter(value, buttonLabel = "Add filter") {
*/
function enterDefaultValue(value, buttonLabel = "Add filter") {
cy.findByText("Enter a default value…").click();
cy.findByPlaceholderText("Enter a default value…")
.type(`${value}{enter}`)
.blur();
cy.findByPlaceholderText("Enter a default value…").type(`${value}`).blur();
cy.button(buttonLabel).click();
}

View File

@@ -197,9 +197,8 @@ describe("scenarios > question > native", () => {
H.selectFilterOperator(operator);
H.popover().within(() => {
cy.findByLabelText("Filter value").type("This has a value");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.log(
`**Mid-point assertion for "${operator}" filter| FAILING in v0.36.6**`,

View File

@@ -571,9 +571,8 @@ describe(
H.popover().within(() => {
cy.findAllByText("ID").should("have.length", 2).first().click();
cy.findByLabelText("Filter value").type(id).click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
H.assertQueryBuilderRowCount(1);
removeFilter();
@@ -2291,7 +2290,7 @@ describe("issue 48829", () => {
H.modal().should("not.exist");
});
it("should not show the unsaved changes warning when switching back to chill mode from the notebook editor after adding a filter via the filter modal (metabase#48829)", () => {
it("should not show the unsaved changes warning when switching back to chill mode from the notebook editor after adding a filter via the filter picker (metabase#48829)", () => {
H.createQuestion(questionDetails, { visitQuestion: true });
H.queryBuilderHeader()
@@ -2300,9 +2299,8 @@ describe("issue 48829", () => {
H.popover().within(() => {
cy.findByText("Category").click();
cy.findByText("Doohickey").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
H.queryBuilderHeader()
.button(/Editor/)

View File

@@ -1164,7 +1164,7 @@ describe("scenarios > question > multiple column breakouts", () => {
cy.findByText("Between").click();
cy.findByLabelText("Start date").clear().type(columnMinValue);
cy.findByLabelText("End date").clear().type(columnMaxValue);
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
}
@@ -1202,7 +1202,6 @@ describe("scenarios > question > multiple column breakouts", () => {
});
cy.log("assert query results");
H.runButtonOverlay().click();
cy.wait("@dataset");
}
@@ -1225,7 +1224,7 @@ describe("scenarios > question > multiple column breakouts", () => {
cy.findByPlaceholderText("Max")
.clear()
.type(String(columnMaxValue));
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
}
@@ -1263,7 +1262,6 @@ describe("scenarios > question > multiple column breakouts", () => {
});
cy.log("assert query results");
H.runButtonOverlay().click();
cy.wait("@dataset");
}

View File

@@ -482,9 +482,8 @@ describe("scenarios > question > nested", () => {
H.selectFilterOperator("Equal to");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("4");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.findAllByTestId("cell-data")
.should("contain", "Murray, Watsica and Wunsch")
@@ -524,9 +523,8 @@ describe("scenarios > question > nested", () => {
H.selectFilterOperator("Equal to");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("5");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.wait("@dataset");
H.queryBuilderFiltersPanel().findByText("Count is equal to 5");

View File

@@ -146,8 +146,12 @@ describe("scenarios > alert", () => {
const allowedDomain = "metabase.test";
const deniedDomain = "metabase.example";
const deniedEmail = `mailer@${deniedDomain}`;
const subscriptionError = `You're only allowed to email subscriptions to addresses ending in ${allowedDomain}`;
const alertError = `You're only allowed to email alerts to addresses ending in ${allowedDomain}`;
const subscriptionError =
"You're only allowed to email subscriptions to allowed domains";
const alertError =
"You're only allowed to email alerts to allowed domains";
const adminSubscriptionError = `You're only allowed to email subscriptions to addresses ending in ${allowedDomain}`;
const adminAlertError = `You're only allowed to email alerts to addresses ending in ${allowedDomain}`;
function addEmailRecipient(email) {
cy.findByRole("textbox").click().type(`${email}`).blur();
@@ -177,7 +181,7 @@ describe("scenarios > alert", () => {
addEmailRecipient(deniedEmail);
});
cy.findByText(alertError);
cy.findByText(adminAlertError);
cy.button("Done").should("be.disabled");
});
});
@@ -191,6 +195,34 @@ describe("scenarios > alert", () => {
addEmailRecipient(deniedEmail);
// Reproduces metabase#17977
cy.button("Send email now").should("be.disabled");
cy.button("Done").should("be.disabled");
cy.findByText(adminSubscriptionError);
});
});
it("should not display the list of approved domains for non-admins (metabase#57138)", () => {
cy.signInAsNormalUser();
H.visitQuestion(ORDERS_QUESTION_ID);
H.openSharingMenu("Create an alert");
H.modal().within(() => {
cy.findByText("New alert").should("be.visible");
cy.findByTestId("token-field").within(() => {
addEmailRecipient(deniedEmail);
});
cy.findByText(alertError);
cy.button("Done").should("be.disabled");
});
H.visitDashboard(ORDERS_DASHBOARD_ID);
H.openSharingMenu("Subscriptions");
H.sidebar().within(() => {
addEmailRecipient(deniedEmail);
cy.button("Send email now").should("be.disabled");
cy.button("Done").should("be.disabled");
cy.findByText(subscriptionError);

View File

@@ -172,6 +172,105 @@ describe("scenarios > question > download", () => {
);
});
describe("download format preference", { tags: "@flaky" }, () => {
it("should remember the selected format across page reloads", () => {
cy.intercept(
"PUT",
"/api/user-key-value/namespace/last_download_format/key/download_format_preference",
).as("saveFormat");
H.createQuestion(
{
name: "Format Preference Test",
query: {
"source-table": ORDERS_ID,
limit: 5,
},
display: "table",
},
{ visitQuestion: true },
);
cy.findByRole("button", { name: "Download results" }).click();
H.popover().within(() => {
cy.findByText(".xlsx")
.should("be.visible")
.click()
.then(() => {
cy.wait("@saveFormat");
});
});
cy.intercept(
"GET",
"/api/user-key-value/namespace/last_download_format/key/download_format_preference",
).as("fetchFormat");
cy.reload();
cy.wait("@fetchFormat");
cy.findByRole("button", { name: "Download results" }).click();
H.popover().within(() => {
cy.get("[data-checked='true']").should("contain", ".xlsx");
});
});
it("should remember the download format on dashboards", () => {
cy.intercept(
"PUT",
"/api/user-key-value/namespace/last_download_format/key/download_format_preference",
).as("saveFormat");
H.createQuestion({
name: "Dashboard Format Test",
query: {
"source-table": ORDERS_ID,
limit: 5,
},
display: "table",
}).then(({ body: { id: questionId } }) => {
H.createDashboard().then(({ body: { id: dashboardId } }) => {
H.addOrUpdateDashboardCard({
card_id: questionId,
dashboard_id: dashboardId,
});
H.visitDashboard(dashboardId);
H.getDashboardCard(0).realHover();
H.getDashboardCardMenu(0).click();
H.popover().within(() => {
cy.findByText("Download results").click();
});
H.popover().within(() => {
cy.findByText(".xlsx")
.should("be.visible")
.click()
.then(() => {
cy.wait("@saveFormat");
});
});
cy.intercept(
"GET",
"/api/user-key-value/namespace/last_download_format/key/download_format_preference",
).as("fetchFormat");
cy.reload();
cy.wait("@fetchFormat");
H.getDashboardCard(0).realHover();
H.getDashboardCardMenu(0).click();
H.popover().within(() => {
cy.findByText("Download results").click();
});
H.popover().within(() => {
cy.get("[data-checked='true']").should("contain", ".xlsx");
});
});
});
});
});
it("respects renamed columns in self-joins", () => {
const idLeftRef = [
"field",

View File

@@ -188,9 +188,8 @@ describe("scenarios > visualizations > bar chart", () => {
H.selectFilterOperator("Is not");
H.popover().within(() => {
cy.findByText("Gadget").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
H.getDraggableElements().should("have.length", 2);
H.getDraggableElements().eq(0).should("have.text", "Doohickey");
H.getDraggableElements().eq(1).should("have.text", "Widget");

View File

@@ -79,9 +79,8 @@ describe("scenarios > visualizations > funnel chart", () => {
H.selectFilterOperator("Is not");
H.popover().within(() => {
cy.findByText("Facebook").click();
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
H.getDraggableElements().should("have.length", 4);

View File

@@ -185,9 +185,8 @@ describe("issue 17524", () => {
H.selectFilterOperator("Greater than");
H.popover().within(() => {
cy.findByLabelText("Filter value").type("1");
cy.button("Add filter").click();
cy.button("Apply filter").click();
});
H.runButtonOverlay().click();
cy.get("polygon");
});
});

View File

@@ -334,3 +334,74 @@ where x < {{param}}`,
H.chartPathWithFillColor("#88BF4D").should("have.length", 4);
});
});
describe("issue 47757", () => {
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
});
it("should show correct tooltips for interpolated data points (metabase#47757)", () => {
H.visitQuestionAdhoc({
visualization_settings: {
"graph.dimensions": ["X"],
"graph.metrics": ["Y"],
series_settings: { Y: { "line.missing": "zero" } },
},
dataset_query: {
type: "native",
native: {
query: `select '2020-01-01' x, 10 y
union all select '2020-03-01' x, 30 y
union all select '2020-04-01' x, 40 y`,
},
database: SAMPLE_DB_ID,
},
display: "line",
});
H.cartesianChartCircleWithColor("#88BF4D").eq(0).trigger("mousemove");
H.assertEChartsTooltip({
header: "January 2020",
rows: [
{
color: "#88BF4D",
name: "Y",
value: 10,
},
],
footer: null,
blurAfter: true,
});
H.cartesianChartCircleWithColor("#88BF4D").eq(1).trigger("mousemove");
H.assertEChartsTooltip({
header: "February 2020",
rows: [
{
color: "#88BF4D",
name: "Y",
value: 0,
secondaryValue: "-100%",
},
],
footer: null,
blurAfter: true,
});
H.cartesianChartCircleWithColor("#88BF4D").eq(2).trigger("mousemove");
H.assertEChartsTooltip({
header: "March 2020",
rows: [
{
color: "#88BF4D",
name: "Y",
value: 30,
secondaryValue: "+∞%",
},
],
footer: null,
blurAfter: true,
});
});
});

View File

@@ -47,7 +47,9 @@ describe("issue 6010", () => {
.then(({ body: { id } }) => createQuestion(id))
.then(({ body: { id } }) => H.visitQuestion(id));
H.cartesianChartCircle().eq(0).click();
// Metric filters are transformed into aggregation case expression condition. The 21st point is first non filtered
// point.
H.cartesianChartCircle().eq(21).click();
H.popover().findByText("See these Orders").click();
cy.wait("@dataset");
@@ -1350,3 +1352,117 @@ describe("issue 57132", () => {
.should("be.visible");
});
});
describe("issue 52333", () => {
const baseQuery = `
SELECT *
FROM (
SELECT
category,
source,
state,
SUM(orders.discount) AS discount,
SUM(orders.total) AS total,
SUM(orders.quantity) AS quantity
FROM
orders
LEFT JOIN products ON orders.product_id = products.id
LEFT JOIN people ON orders.user_id = people.id
GROUP BY category, source, state
) AS filtered_orders
WHERE NOT (
category = 'Gizmo'
AND (
source IN ('Facebook', 'Google', 'Organic', 'Twitter')
OR state NOT IN ('AK')
)
);`;
const baseQuestionDetails = {
name: "52333",
display: "table",
native: {
query: baseQuery,
},
};
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("pivot table should show subtotals for a group of a single value (metabase#52333)", () => {
H.createNativeQuestion(baseQuestionDetails, {
visitQuestion: true,
wrapId: true,
});
cy.get("@questionId").then((id) => {
const questionDetails = {
query: {
"source-table": `card__${id}`,
aggregation: [["count"]],
breakout: [
[
"field",
"CATEGORY",
{
"base-type": "type/Text",
},
],
[
"field",
"SOURCE",
{
"base-type": "type/Text",
},
],
[
"field",
"STATE",
{
"base-type": "type/Text",
},
],
],
},
display: "pivot",
visualization_settings: {
"pivot_table.column_split": {
rows: ["CATEGORY", "SOURCE", "STATE"],
columns: [],
values: ["count", "avg"],
},
"pivot_table.column_widths": {
leftHeaderWidths: [104, 92, 80],
totalLeftHeaderWidths: 276,
valueHeaderWidths: {},
},
"pivot_table.collapsed_rows": {
value: ['["Doohickey"]', '["Gadget"]', '["Widget"]'],
rows: ["CATEGORY", "SOURCE", "STATE"],
},
},
};
H.createQuestion(questionDetails, { visitQuestion: true });
});
H.queryBuilderMain().within(() => {
cy.findByText("Affiliate");
cy.findByText("AK");
// Ensure it does not show subtotals for the single value by default
cy.findByText("Totals for Affiliate").should("not.exist");
H.openVizSettingsSidebar();
});
H.sidebar().findByText("Condense duplicate totals").click();
// Ensure it shows subtotals for the single value
H.queryBuilderMain()
.findByText("Totals for Affiliate")
.should("be.visible");
});
});

View File

View File

@@ -5,7 +5,7 @@
[medley.core :as m]
[metabase-enterprise.advanced-config.file.interface :as advanced-config.file.i]
[metabase.driver.util :as driver.u]
[metabase.models.setting :refer [defsetting]]
[metabase.settings.core :refer [defsetting]]
[metabase.util :as u]
[metabase.util.log :as log]
[metabase.util.quick-task :as quick-task]

View File

@@ -2,7 +2,7 @@
(:require
[clojure.spec.alpha :as s]
[metabase-enterprise.advanced-config.file.interface :as advanced-config.file.i]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[metabase.util :as u]
[metabase.util.log :as log]))

View File

@@ -1,8 +1,8 @@
(ns metabase-enterprise.advanced-config.models.pulse-channel
(:require
[clojure.string :as str]
[metabase.models.setting :refer [defsetting]]
[metabase.premium-features.core :as premium-features]
[metabase.settings.core :refer [defsetting]]
[metabase.util :as u]
[metabase.util.i18n :refer [deferred-tru tru]]))

View File

@@ -6,9 +6,9 @@
[metabase-enterprise.serialization.cmd :as serialization.cmd]
[metabase.audit :as audit]
[metabase.db :as mdb]
[metabase.models.setting :refer [defsetting]]
[metabase.plugins :as plugins]
[metabase.premium-features.core :refer [defenterprise]]
[metabase.settings.core :refer [defsetting]]
[metabase.sync.util :as sync-util]
[metabase.util :as u]
[metabase.util.files :as u.files]

View File

@@ -0,0 +1,3 @@
(ns metabase-enterprise.cache.init
(:require
[metabase-enterprise.cache.task.refresh-cache-configs]))

View File

@@ -1,8 +1,8 @@
(ns metabase-enterprise.cache.strategies
(:require
[java-time.api :as t]
[metabase.api.cache]
[metabase.models.cache-config :as cache-config]
[metabase.cache.api]
[metabase.cache.core :as cache]
[metabase.premium-features.core :refer [defenterprise defenterprise-schema]]
[metabase.query-processor.middleware.cache-backend.db :as backend.db]
[metabase.util.log :as log]
@@ -11,26 +11,26 @@
(set! *warn-on-reflection* true)
(comment metabase.api.cache/keep-me)
(comment metabase.cache.api/keep-me)
;; Data shape
(mr/def ::cache-strategy
"Schema for a caching strategy used internally"
[:and
:metabase.api.cache/cache-strategy.base
:metabase.cache.api/cache-strategy.base
[:multi {:dispatch :type}
[:nocache :metabase.api.cache/cache-strategy.nocache]
[:nocache :metabase.cache.api/cache-strategy.nocache]
[:ttl [:merge
:metabase.api.cache/cache-strategy.ttl
:metabase.cache.api/cache-strategy.ttl
[:map
[:invalidated-at {:optional true} some?]]]]
[:duration [:merge
:metabase.api.cache/cache-strategy.ee.duration
:metabase.cache.api/cache-strategy.ee.duration
[:map
[:invalidated-at {:optional true} some?]]]]
[:schedule [:merge
:metabase.api.cache/cache-strategy.ee.schedule
:metabase.cache.api/cache-strategy.ee.schedule
[:map
[:invalidated-at {:optional true} some?]]]]]])
@@ -56,30 +56,30 @@
:order-by :ordering
:limit [:inline 1]}
item (t2/select-one :model/CacheConfig :id q)]
(cache-config/card-strategy item card)))
(cache/card-strategy item card)))
;;; Strategy execution
(defmulti ^:private fetch-cache-stmt-ee*
"Generate prepared statement for a db cache backend for a given strategy"
{:arglists '([strategy query-hash conn])}
(fn [strategy _hash _conn] (:type strategy)))
{:arglists '([strategy query-hash])}
(fn [strategy _hash] (:type strategy)))
(defmethod fetch-cache-stmt-ee* :ttl [strategy query-hash conn]
(backend.db/fetch-cache-stmt-ttl strategy query-hash conn))
(defmethod fetch-cache-stmt-ee* :ttl [strategy query-hash]
(backend.db/fetch-cache-stmt-ttl strategy query-hash))
(defmethod fetch-cache-stmt-ee* :duration [strategy query-hash conn]
(defmethod fetch-cache-stmt-ee* :duration [strategy query-hash]
(if-not (and (:duration strategy) (:unit strategy))
(log/debugf "Caching strategy %s should have :duration and :unit" (pr-str strategy))
(let [duration (t/duration (:duration strategy) (keyword (:unit strategy)))
duration-ago (t/minus (t/offset-date-time) duration)
invalidated-at (t/max duration-ago (:invalidated-at strategy))]
(backend.db/prepare-statement conn query-hash invalidated-at))))
(backend.db/select-cache query-hash invalidated-at))))
(defmethod fetch-cache-stmt-ee* :schedule [{:keys [invalidated-at] :as strategy} query-hash conn]
(defmethod fetch-cache-stmt-ee* :schedule [{:keys [invalidated-at] :as strategy} query-hash]
(if-not invalidated-at
(log/debugf "Caching strategy %s has not run yet" (pr-str strategy))
(backend.db/prepare-statement conn query-hash invalidated-at)))
(backend.db/select-cache query-hash invalidated-at)))
(defmethod fetch-cache-stmt-ee* :nocache [_ _ _]
nil)
@@ -87,5 +87,5 @@
(defenterprise fetch-cache-stmt
"Returns prepared statement to query for db cache backend."
:feature :cache-granular-controls
[strategy query-hash conn]
(fetch-cache-stmt-ee* strategy query-hash conn))
[strategy query-hash]
(fetch-cache-stmt-ee* strategy query-hash))

View File

@@ -1,4 +1,4 @@
(ns metabase-enterprise.task.cache
(ns metabase-enterprise.cache.task.refresh-cache-configs
(:require
[clojurewerkz.quartzite.jobs :as jobs]
[clojurewerkz.quartzite.schedule.cron :as cron]
@@ -11,7 +11,7 @@
[metabase.util.log :as log]
[toucan2.core :as t2])
(:import
(java.util.concurrent Callable ExecutorService ThreadPoolExecutor TimeUnit SynchronousQueue)
(java.util.concurrent Callable ExecutorService SynchronousQueue ThreadPoolExecutor TimeUnit)
(org.apache.commons.lang3.concurrent BasicThreadFactory$Builder)
(org.quartz.spi MutableTrigger)))
@@ -317,8 +317,6 @@
(triggers/with-schedule
(cron/cron-schedule "0 * * * * ? *"))))
(defenterprise init-cache-task!
"Inits periodical task checking for cache expiration"
:feature :cache-granular-controls
[]
(task/schedule-task! cache-job cache-trigger))
(defmethod task/init! ::RefreshCacheConfigs [_]
(when (premium-features/has-feature? :cache-granular-controls)
(task/schedule-task! cache-job cache-trigger)))

View File

@@ -6,4 +6,5 @@
See https://metaboat.slack.com/archives/CKZEMT1MJ/p1736556522733279 for rationale behind this pattern."
(:require
[metabase-enterprise.cache.init]
[metabase-enterprise.enhancements.init]))

View File

@@ -5,7 +5,7 @@
[metabase.api.macros :as api.macros]
[metabase.api.routes.common :refer [+auth]]
[metabase.events :as events]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[metabase.util :as u]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
@@ -94,7 +94,7 @@
(api/check-404 db)
(api/check-400 (not (:router_database_id db)) "Cannot make a destination database a router database")
(api/check-400 (not (:uploads_enabled db)) "Cannot enable database routing for a database with uploads enabled")
(binding [setting/*database-local-values* (:settings db)]
(setting/with-database-local-values (:settings db)
(api/check-400 (not (setting/get :persist-models-enabled)) "Cannot enable database routing for a database with model persistence enabled")
(api/check-400 (not (setting/get :database-enable-actions)) "Cannot enable database routing for a database with actions enabled")))
(if (nil? user_attribute)

View File

@@ -1,7 +1,6 @@
(ns metabase-enterprise.enhancements.integrations.google
(:require
[metabase.models.setting :as setting]
[metabase.models.setting.multi-setting :refer [define-multi-setting-impl]]))
[metabase.settings.core :as setting :refer [define-multi-setting-impl]]))
(define-multi-setting-impl google-auth-auto-create-accounts-domain :ee
:getter (fn [] (setting/get-value-of-type :string :google-auth-auto-create-accounts-domain))

View File

@@ -2,9 +2,9 @@
"The Enterprise version of the LDAP integration is basically the same but also supports syncing user attributes."
(:require
[metabase-enterprise.sso.integrations.sso-utils :as sso-utils]
[metabase.models.setting :refer [defsetting]]
[metabase.models.user :as user]
[metabase.premium-features.core :refer [defenterprise-schema]]
[metabase.settings.core :refer [defsetting]]
[metabase.sso.core :as sso]
[metabase.util :as u]
[metabase.util.i18n :refer [deferred-tru]]

View File

@@ -167,6 +167,14 @@
(when-not (= delete-status :ok)
(log/debugf "Unable to delete gdrive connection %s." id))))))
(mu/defn hm-get-all-connections :- :hm-client/http-reply
"Get all connections from harbormaster."
[]
(hm.client/make-request :get "/api/v2/mb/connections"))
(comment
(hm-get-all-connections))
(mu/defn hm-get-gdrive-conn :- :hm-client/http-reply
"Get a specific gdrive connection by id."
[id]
@@ -269,7 +277,7 @@
cannot-check-message
{:gdrive/conn-id conn-id
:hm/exception e})))))
[hm-status {hm-body :body}] hm-response]
[hm-status {hm-status-code :status hm-body :body}] hm-response]
(if (= :ok hm-status)
(let [{:keys [status status-reason error last-sync-at last-sync-started-at]
:as _} (normalize-gdrive-conn hm-body)]
@@ -296,15 +304,30 @@
:last_sync_at (.getEpochSecond ^Instant (t/instant last-sync-at))
:hm/response (loggable-response hm-response))
(analytics/inc! :metabase-gsheets/connection-creation-error {:reason "status_error"})
(log/errorf "Error getting status of connection %s: %s %s" conn-id (:status-reason hm-body) (:error-detail hm-body)))))
(if (empty? saved-setting)
(log/errorf "Error getting status of connection %s: status-reason=`%s` error-detail=`%s`" conn-id (:status-reason hm-body) (:error-detail hm-body)))))
(cond
(empty? saved-setting)
{:status "not-connected"}
(= 403 hm-status-code)
(let [[list-status list-response] (hm-get-all-connections)]
(if (and (= :ok list-status) (not (some (partial = conn-id) (set (map :id (:body list-response))))))
(u/prog1 {:status "not-connected"}
(log/warnf "Removing google drive connection %s because harbormaster gives a 403 error for it." conn-id)
(reset-gsheets-status!))
;; It doesn't look like a spot where we can remove the connection, so we just return the error
(assoc (setting->response saved-setting)
:status "error"
:error_message cannot-check-message
:hm/response (loggable-response hm-response))))
:else
(u/prog1 (assoc (setting->response saved-setting)
:status "error"
:error_message cannot-check-message
:hm/response (loggable-response hm-response))
(analytics/inc! :metabase-gsheets/connection-creation-error {:reason "status_error"})
(log/errorf "Error getting status of connection %s: %s %s" conn-id (:status-reason hm-body) (:error-detail hm-body)))))))
(log/errorf "Error getting status of connection %s: status-reason=`%s` error-detail=`%s`" conn-id (:status-reason hm-body) (:error-detail hm-body)))))))
(api.macros/defendpoint :get "/connection" :- :gsheets/response
"Check the status of a connection. This endpoint gets polled by FE to determine when to

View File

@@ -2,8 +2,8 @@
(:require
[clojure.set :as set]
[metabase-enterprise.gsheets.constants :as gsheets.constants]
[metabase.models.setting :as setting :refer [defsetting]]
[metabase.premium-features.core :as premium-features]
[metabase.settings.core :as setting :refer [defsetting]]
[metabase.util :as u]
[metabase.util.i18n :refer [deferred-tru]]
[metabase.util.json :as json]

View File

@@ -1,7 +1,7 @@
(ns metabase-enterprise.internal-stats
(ns metabase-enterprise.internal-stats.core
(:require
[metabase.models.setting :as setting]
[metabase.premium-features.core :refer [defenterprise]]))
[metabase.premium-features.core :refer [defenterprise]]
[metabase.settings.core :as setting]))
(defenterprise embedding-settings
"Boolean values that report on the state of different embedding configurations."

View File

@@ -1,6 +1,6 @@
(ns metabase-enterprise.llm.settings
(:require
[metabase.models.setting :as setting :refer [defsetting]]
[metabase.settings.core :as setting :refer [defsetting]]
[metabase.util.i18n :refer [deferred-tru]]))
(defsetting ee-openai-model

View File

@@ -6,8 +6,7 @@
[metabase.api.open-api :as open-api]
[metabase.api.routes.common :refer [+auth]]
[metabase.models.collection :as collection]
[metabase.models.query-analysis :as query-analysis]
[metabase.public-settings :as public-settings]
[metabase.query-analysis.core :as query-analysis]
[metabase.request.core :as request]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
@@ -121,7 +120,7 @@
[handler]
(open-api/handler-with-open-api-spec
(fn [request respond raise]
(if (public-settings/query-analysis-enabled)
(if (query-analysis/query-analysis-enabled)
(handler request respond raise)
(respond {:status 429 :body "Query Analysis must be enabled to use the Query Reference Validator"})))
(fn [prefix]

View File

@@ -5,8 +5,8 @@
[metabase.api.common :as api]
[metabase.api.macros :as api.macros]
[metabase.models.api-key :as api-key]
[metabase.models.setting :refer [defsetting]]
[metabase.public-settings :as public-settings]
[metabase.settings.core :refer [defsetting]]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.util.i18n :refer [deferred-tru]]
[metabase.util.secret :as u.secret]
[toucan2.core :as t2]))

View File

@@ -13,7 +13,7 @@
[metabase.api.routes.common :refer [+auth]]
[metabase.logger :as logger]
[metabase.models.serialization :as serdes]
[metabase.public-settings :as public-settings]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.util :as u]
[metabase.util.compress :as u.compress]
[metabase.util.date-2 :as u.date]

View File

@@ -7,7 +7,7 @@
[metabase-enterprise.serialization.serialize :as serialize]
[metabase.config :as config]
[metabase.models.interface :as mi]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[metabase.util.log :as log]
[metabase.util.yaml :as yaml]
[toucan2.core :as t2]))

View File

@@ -14,8 +14,8 @@
[metabase.legacy-mbql.normalize :as mbql.normalize]
[metabase.legacy-mbql.util :as mbql.u]
[metabase.lib.util.match :as lib.util.match]
[metabase.models.setting :as setting]
[metabase.models.visualization-settings :as mb.viz]
[metabase.settings.core :as setting]
[metabase.util.date-2 :as u.date]
[metabase.util.i18n :refer [trs]]
[metabase.util.log :as log]

View File

@@ -4,8 +4,8 @@
[clojure.string :as str]
[metabase.api.common :as api]
[metabase.api.macros :as api.macros]
[metabase.models.setting :as setting]
[metabase.premium-features.core :as premium-features]
[metabase.settings.core :as setting]
[metabase.util.i18n :refer [tru]]
[saml20-clj.core :as saml]))

View File

@@ -7,7 +7,7 @@
[metabase-enterprise.sso.api.interface :as sso.i]
[metabase-enterprise.sso.integrations.sso-settings :as sso-settings]
[metabase-enterprise.sso.integrations.sso-utils :as sso-utils]
[metabase.embed.settings :as embed.settings]
[metabase.embedding.settings :as embed.settings]
[metabase.premium-features.core :as premium-features]
[metabase.request.core :as request]
[metabase.session.models.session :as session]

View File

@@ -37,9 +37,9 @@
[metabase-enterprise.sso.integrations.sso-utils :as sso-utils]
[metabase.api.common :as api]
[metabase.premium-features.core :as premium-features]
[metabase.public-settings :as public-settings]
[metabase.request.core :as request]
[metabase.session.core :as session]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.sso.core :as sso]
[metabase.util :as u]
[metabase.util.i18n :refer [trs tru]]

View File

@@ -4,9 +4,8 @@
information. Separating out this information creates a better dependency graph and avoids circular dependencies."
(:require
[metabase-enterprise.scim.api :as scim]
[metabase.models.setting :as setting :refer [defsetting]]
[metabase.models.setting.multi-setting :refer [define-multi-setting-impl]]
[metabase.public-settings :as public-settings]
[metabase.settings.core :as setting :refer [defsetting define-multi-setting-impl]]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.util.i18n :refer [deferred-tru tru]]
[metabase.util.log :as log]
[metabase.util.malli :as mu]

View File

@@ -6,7 +6,7 @@
[metabase.channel.email.messages :as messages]
[metabase.events :as events]
[metabase.events.notification :as events.notification]
[metabase.public-settings :as public-settings]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.sso.core :as sso]
[metabase.util :as u]
[metabase.util.i18n :refer [trs tru]]

View File

@@ -1,8 +1,8 @@
(ns metabase-enterprise.stale
(:require
[malli.experimental.time]
[metabase.embed.settings :as embed.settings]
[metabase.models.setting :as setting :refer [defsetting]]
[metabase.embedding.settings :as embed.settings]
[metabase.settings.core :as setting :refer [defsetting]]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.i18n :refer [deferred-tru]]
[metabase.util.malli :as mu]

View File

@@ -2,7 +2,7 @@
(:require
[clojure.test :refer :all]
[metabase-enterprise.advanced-config.file :as advanced-config.file]
[metabase.models.setting :refer [defsetting]]
[metabase.settings.core :refer [defsetting]]
[metabase.test :as mt]
[metabase.util :as u]))

View File

@@ -122,6 +122,11 @@
"A 400 response from HM. nb: if you change this, change it in test_resources/gsheets/mock_hm_responses.edn"
"93662bf7-b1c7-442b-80ec-18dee23894fa")
(def ^:private
gdrive-403-error-link
"A 403 response from HM. nb: if you change this, change it in test_resources/gsheets/mock_hm_responses.edn"
"e5b50d83-c1d6-4382-8351-ff95a23af60e")
(def ^:private
gdrive-200-error-link
"A 200 'error' response from HM. nb: if you change this, change it in test_resources/gsheets/mock_hm_responses.edn"
@@ -262,6 +267,41 @@
(is (partial= {:status "error", :url "test-url" :created_by_id 2} response))
(is (pos-int? (:db_id response)))))))))))
(deftest get-folder-test-invalid-connections
(with-sample-db-as-dwh
(let [mock-gsheet {:created-by-id 2
:url "test-url",
:created-at 15
:db-id 1}]
(mt/with-premium-features #{:etl-connections :attached-dwh :hosting}
(testing "when the connection does not exist, it is deleted"
(mt/with-temporary-setting-values [gsheets (assoc mock-gsheet :gdrive/conn-id gdrive-403-error-link)]
(with-redefs [hm.client/make-request (partial mock-make-request happy-responses)]
(let [response (mt/user-http-request :crowberto :get 200 "ee/gsheets/connection")]
(is (= {:status "not-connected"} response))
(is (= {} (gsheets)))))))
(testing "when the HM gives a 403 response for the connection, but it shows in the connection list then it is not deleted"
(mt/with-temporary-setting-values [gsheets (assoc mock-gsheet :gdrive/conn-id gdrive-active-link)]
(with-redefs [hm.client/make-request (partial mock-make-request (assoc happy-responses
{:method :get, :url (str "/api/v2/mb/connections/" gdrive-active-link), :body nil}
[:error
{:status 403,
:body {:error "User not authorized to act over resource."}}]))]
(let [response (mt/user-http-request :crowberto :get 200 "ee/gsheets/connection")]
(is (= "error" (:status response)))
(is (= 15 (:created-at (gsheets))))))))
(testing "when the HM gives a 403 response for the connection, and the connection list fails, then it is not deleted"
(mt/with-temporary-setting-values [gsheets (assoc mock-gsheet :gdrive/conn-id gdrive-403-error-link)]
(with-redefs [hm.client/make-request (partial mock-make-request
(assoc happy-responses
{:method :get, :url "/api/v2/mb/connections", :body nil}
[:error
{:status 403,
:body {:error "User not authorized to act over resource."}}]))]
(let [response (mt/user-http-request :crowberto :get 200 "ee/gsheets/connection")]
(is (= "error" (:status response)))
(is (= 15 (:created-at (gsheets))))))))))))
(deftest delete-folder-test
(with-sample-db-as-dwh
(mt/with-premium-features #{:etl-connections :attached-dwh :hosting}

View File

@@ -3,7 +3,7 @@
[clojure.test :refer :all]
[java-time.api :as t]
[metabase-enterprise.cache.strategies :as strategies]
[metabase-enterprise.task.cache :as task.cache]
[metabase-enterprise.cache.task.refresh-cache-configs :as task.cache]
[metabase.models.query :as query]
[metabase.query-processor :as qp]
[metabase.query-processor.card :as qp.card]

View File

@@ -1,9 +1,9 @@
(ns metabase-enterprise.task.cache-test
(ns metabase-enterprise.cache.task.refresh-cache-configs-test
(:require
[clojure.test :refer :all]
[java-time.api :as t]
[medley.core :as m]
[metabase-enterprise.task.cache :as task.cache]
[metabase-enterprise.cache.task.refresh-cache-configs :as task.cache]
[metabase.premium-features.core :as premium-features]
[metabase.query-processor :as qp]
[metabase.query-processor.card :as qp.card]

View File

@@ -3,7 +3,7 @@
[clojure.test :refer :all]
[metabase-enterprise.enhancements.integrations.ldap :as ldap-ee]
[metabase-enterprise.sso.integrations.sso-settings :as sso-settings]
[metabase.public-settings :as public-settings]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.sso.ldap :as ldap]
[metabase.sso.ldap-test-util :as ldap.test]
[metabase.test :as mt]

View File

@@ -2,7 +2,7 @@
(:require
[clojure.test :refer [deftest is testing]]
[metabase-enterprise.gsheets.settings :as gsettings]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[toucan2.core :as t2]))
(defmacro with-sample-db-as-dwh [& body]

View File

@@ -2,7 +2,8 @@
(:require
[clojure.test :refer [deftest is testing]]
[metabase-enterprise.harbormaster.client :as hm.client]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[metabase.settings.models.setting]
[metabase.test.util :as mt]))
(deftest ->config-good-test
@@ -24,11 +25,11 @@
;; mt/with-temporary-setting-values is not used here because we want to test the behavior of the function when the
;; api-key is blank or nil, and mt/with-temporary-setting-values will not allow us to set the api-key to blank
;; or nil.
(with-redefs [setting/get-raw-value (+value-for-setting grv :api-key "")]
(with-redefs [metabase.settings.models.setting/get-raw-value (+value-for-setting grv :api-key "")]
(is (thrown-with-msg? Exception
#"Missing api-key."
(#'hm.client/->config))))
(with-redefs [setting/get-raw-value (+value-for-setting grv :api-key nil)]
(with-redefs [metabase.settings.models.setting/get-raw-value (+value-for-setting grv :api-key nil)]
(is (thrown-with-msg? Exception
#"Missing api-key."
(#'hm.client/->config))))))

View File

@@ -1,7 +1,7 @@
(ns metabase-enterprise.internal-stats-test
(ns metabase-enterprise.internal-stats.core-test
(:require
[clojure.test :refer [deftest testing is]]
[metabase-enterprise.internal-stats :as sut]
[metabase-enterprise.internal-stats.core :as sut]
[metabase.test :as mt]))
(deftest enabled-embedding-static-test

View File

@@ -1,8 +1,8 @@
(ns metabase-enterprise.public-settings-test
(:require
[clojure.test :refer :all]
[metabase.embed.settings :as embed.settings]
[metabase.public-settings :as public-settings]
[metabase.embedding.settings :as embed.settings]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.test :as mt]
[metabase.test.fixtures :as fixtures]
[metabase.test.util :as tu]))

View File

@@ -11,7 +11,7 @@
[metabase-enterprise.serialization.v2.load :as serdes.load]
[metabase-enterprise.serialization.v2.storage :as storage]
[metabase.models.serialization :as serdes]
[metabase.models.setting :as setting]
[metabase.settings.core :as setting]
[metabase.test :as mt]
[metabase.test.generate :as test-gen]
[metabase.util.yaml :as yaml]
@@ -358,7 +358,7 @@
(testing "for settings"
(let [settings (get @entities "Setting")]
(is (every? @#'setting/export?
(is (every? setting/export?
(set (map (comp symbol :key) settings))))
(is (= (into {} (for [{:keys [key value]} settings]
[key value]))

View File

@@ -11,7 +11,7 @@
[metabase.config :as config]
[metabase.http-client :as client]
[metabase.premium-features.token-check :as token-check]
[metabase.public-settings :as public-settings]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.test :as mt]
[metabase.test.fixtures :as fixtures]
[metabase.util :as u]

View File

@@ -7,9 +7,9 @@
[metabase-enterprise.sso.integrations.sso-settings :as sso-settings]
[metabase.http-client :as client]
[metabase.premium-features.token-check :as token-check]
[metabase.public-settings :as public-settings]
[metabase.request.core :as request]
[metabase.session.core :as session]
[metabase.settings.deprecated-grab-bag :as public-settings]
[metabase.sso.init]
[metabase.test :as mt]
[metabase.test.fixtures :as fixtures]

View File

@@ -12,6 +12,7 @@ import {
setupDashboardEndpoints,
setupDashboardQueryMetadataEndpoint,
setupDatabasesEndpoints,
setupLastDownloadFormatEndpoints,
} from "__support__/server-mocks";
import { setupDashcardQueryEndpoints } from "__support__/server-mocks/dashcard";
import { setupNotificationChannelsEndpoints } from "__support__/server-mocks/pulse";
@@ -150,6 +151,8 @@ const setup = async ({
setupDatabasesEndpoints([createMockDatabase()]);
setupLastDownloadFormatEndpoints();
const user = createMockUser();
const state = setupSdkState({

View File

@@ -53,15 +53,22 @@ export const storybookSdkAuthDefaultConfig =
export const CommonSdkStoryWrapper = (Story: StoryFn, context: any) => {
const sdkTheme = context.globals.sdkTheme;
const theme = sdkTheme ? storybookThemes[sdkTheme] : undefined;
const locale = context.globals.locale;
const user = context.globals.user;
const key = `${user}-${locale}`;
const authConfig = useMemo(() => {
return getStorybookSdkAuthConfigForUser(user);
}, [user]);
return (
<MetabaseProvider authConfig={authConfig} theme={theme} key={user}>
<MetabaseProvider
authConfig={authConfig}
theme={theme}
key={key}
locale={locale}
>
<Story />
</MetabaseProvider>
);

View File

@@ -411,14 +411,15 @@ const HELPER_TEXT_STRINGS: HelpTextConfig[] = [
name: "integer",
structure: "integer",
category: "conversion",
description: () => t`Converts a string to an integer.`,
description: () =>
t`Converts a string or float value to an integer number.`,
args: [
{
get name() {
return t`value`;
},
get description() {
return t`The string column to convert to integers.`;
return t`The string or float column to convert to integers. Float values are rounded.`;
},
example: ["dimension", "User ID"],
},

View File

@@ -1,4 +1,8 @@
import type { Parameter, ParameterValues } from "metabase-types/api";
import type {
Parameter,
ParameterValues,
SearchParameterValuesRequest,
} from "metabase-types/api";
export const createMockParameter = (opts?: Partial<Parameter>): Parameter => ({
id: "1",
@@ -15,3 +19,12 @@ export const createMockParameterValues = (
has_more_values: false,
...opts,
});
export const createMockSearchParameterValuesRequest = (
opts?: Partial<SearchParameterValuesRequest>,
): SearchParameterValuesRequest => ({
parameter: createMockParameter(),
field_ids: [],
query: "",
...opts,
});

View File

@@ -4,10 +4,10 @@ import type {
Engine,
EngineField,
EngineSource,
EnterpriseSettingKey,
EnterpriseSettings,
FontFile,
SettingDefinition,
SettingKey,
Settings,
TokenFeatures,
TokenStatus,
@@ -129,7 +129,7 @@ export const createMockTokenFeatures = (
});
export const createMockSettingDefinition = <
Key extends SettingKey = SettingKey,
Key extends EnterpriseSettingKey = EnterpriseSettingKey,
>(
opts: SettingDefinition<Key>,
): SettingDefinition<Key> => ({

View File

@@ -255,17 +255,25 @@ export type PasswordComplexity = {
export type SessionCookieSameSite = "lax" | "strict" | "none";
export interface SettingDefinition<Key extends SettingKey = SettingKey> {
export interface SettingDefinition<
Key extends EnterpriseSettingKey = EnterpriseSettingKey,
> {
key: Key;
env_name?: string;
is_env_setting?: boolean;
value?: SettingValue<Key>;
default?: SettingValue<Key>;
value?: EnterpriseSettingValue<Key>;
default?: EnterpriseSettingValue<Key>;
display_name?: string;
description?: string | ReactNode | null;
type?: InputSettingType;
}
export type SettingDefinitionMap<
T extends EnterpriseSettingKey = EnterpriseSettingKey,
> = {
[K in T]: SettingDefinition<K>;
};
export type UpdateChannel = "latest" | "beta" | "nightly";
export interface OpenAiModel {

View File

@@ -121,6 +121,14 @@ export type UserKeyValue =
namespace: "user_acknowledgement";
key: string;
value: boolean;
}
| {
namespace: "last_download_format";
key: string;
value: {
last_download_format: "csv" | "xlsx" | "json" | "png";
last_table_download_format: "csv" | "xlsx" | "json";
};
};
export type UserKeyValueKey = Pick<UserKeyValue, "namespace" | "key">;

View File

@@ -245,11 +245,15 @@ describe("MetadataFieldSettings", () => {
).toBeInTheDocument();
});
it("should not display type casting settings for non-supported fields", async () => {
it("should display type casting settings for float fields", async () => {
await setup({ field: ORDERS_DISCOUNT_FIELD });
expect(
screen.queryByText("Cast to a specific data type"),
).not.toBeInTheDocument();
screen.getByText("Cast to a specific data type"),
).toBeInTheDocument();
await userEvent.click(screen.getByText("Don't cast"));
await userEvent.type(screen.getByPlaceholderText("Find..."), "Float");
expect(screen.getByText("Float → Integer")).toBeInTheDocument();
});
it("should show an access denied error if the foreign key field has an inaccessible target", async () => {

View File

@@ -1,5 +1,4 @@
import cx from "classnames";
import PropTypes from "prop-types";
import { useMemo, useRef, useState } from "react";
import { t } from "ttag";
@@ -9,29 +8,37 @@ import CS from "metabase/css/core/index.css";
import Typeahead from "metabase/hoc/Typeahead";
import { color } from "metabase/lib/colors";
import { Icon } from "metabase/ui";
import type { User } from "metabase-types/api";
import { AddMemberAutocompleteSuggestionRoot } from "./AddMemberRow.styled";
import { AddRow } from "./AddRow";
AddMemberRow.propTypes = {
users: PropTypes.array.isRequired,
excludeIds: PropTypes.object,
onCancel: PropTypes.func.isRequired,
onDone: PropTypes.func.isRequired,
};
interface AddMemberRowProps {
users: User[];
excludeIds: Set<number>;
onCancel: () => void;
onDone: (userIds: number[]) => void;
}
export default function AddMemberRow({ users, excludeIds, onCancel, onDone }) {
const rowRef = useRef(null);
export function AddMemberRow({
users,
excludeIds,
onCancel,
onDone,
}: AddMemberRowProps) {
const rowRef = useRef<HTMLDivElement>(null);
const [text, setText] = useState("");
const [selectedUsersById, setSelectedUsersById] = useState(new Map());
const [selectedUsersById, setSelectedUsersById] = useState<Map<number, User>>(
new Map(),
);
const handleRemoveUser = (user) => {
const handleRemoveUser = (user: User) => {
const newSelectedUsersById = new Map(selectedUsersById);
newSelectedUsersById.delete(user.id);
setSelectedUsersById(newSelectedUsersById);
};
const handleAddUser = (user) => {
const handleAddUser = (user: User) => {
const newSelectedUsersById = new Map(selectedUsersById);
newSelectedUsersById.set(user.id, user);
setSelectedUsersById(newSelectedUsersById);
@@ -52,7 +59,7 @@ export default function AddMemberRow({ users, excludeIds, onCancel, onDone }) {
return (
<tr>
<td colSpan="4" style={{ padding: 0 }}>
<td colSpan={4} style={{ padding: 0 }}>
<AddRow
ref={rowRef}
value={text}
@@ -108,42 +115,40 @@ const getColorPalette = () => [
color("accent4"),
];
const AddMemberTypeaheadPopoverPropTypes = {
suggestions: PropTypes.array,
selectedSuggestion: PropTypes.object,
onSuggestionAccepted: PropTypes.func.isRequired,
target: PropTypes.shape({
current: PropTypes.instanceOf(Element),
}),
};
interface AddMemberTypeaheadPopoverProps {
suggestions: User[];
selectedSuggestion: User | null;
onSuggestionAccepted: (user: User) => void;
target: React.RefObject<HTMLDivElement>;
}
const AddMemberTypeaheadPopover = Typeahead({
optionFilter: (text, user) =>
optionFilter: (text: string, user: User) =>
(user.common_name || "").toLowerCase().includes(text.toLowerCase()),
optionIsEqual: (userA, userB) => userA.id === userB.id,
optionIsEqual: (userA: User, userB: User) => userA.id === userB.id,
})(function AddMemberTypeaheadPopover({
suggestions,
selectedSuggestion,
onSuggestionAccepted,
target,
}) {
}: AddMemberTypeaheadPopoverProps) {
const colors = useMemo(getColorPalette, []);
return (
<TippyPopover
className={CS.bordered}
offset={0}
offset={[0, 0]}
placement="bottom-start"
visible={suggestions.length > 0}
reference={target}
content={
suggestions &&
suggestions.map((user, index) => (
suggestions.map((user: User, index: number) => (
<AddMemberAutocompleteSuggestion
key={index}
user={user}
color={colors[index % colors.length]}
selected={selectedSuggestion && user.id === selectedSuggestion.id}
selected={!!selectedSuggestion && user.id === selectedSuggestion.id}
onClick={onSuggestionAccepted.bind(null, user)}
/>
))
@@ -152,16 +157,19 @@ const AddMemberTypeaheadPopover = Typeahead({
);
});
AddMemberTypeaheadPopover.propTypes = AddMemberTypeaheadPopoverPropTypes;
interface AddMemberAutocompleteSuggestionProps {
user: User;
color: string;
selected: boolean;
onClick: () => void;
}
AddMemberAutocompleteSuggestion.propTypes = {
user: PropTypes.object.isRequired,
color: PropTypes.string.isRequired,
selected: PropTypes.bool,
onClick: PropTypes.func.isRequired,
};
function AddMemberAutocompleteSuggestion({ user, color, selected, onClick }) {
function AddMemberAutocompleteSuggestion({
user,
color,
selected,
onClick,
}: AddMemberAutocompleteSuggestionProps) {
return (
<AddMemberAutocompleteSuggestionRoot
isSelected={selected}

View File

@@ -1,11 +1,21 @@
import cx from "classnames";
import PropTypes from "prop-types";
import { forwardRef } from "react";
import { type ChangeEvent, type Ref, forwardRef } from "react";
import { t } from "ttag";
import ButtonsS from "metabase/css/components/buttons.module.css";
import CS from "metabase/css/core/index.css";
interface AddRowProps {
value: string;
isValid: boolean;
placeholder?: string;
onKeyDown?: (event: React.KeyboardEvent) => void;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onDone: () => void;
onCancel: () => void;
children?: React.ReactNode;
}
export const AddRow = forwardRef(function AddRow(
{
value,
@@ -16,8 +26,8 @@ export const AddRow = forwardRef(function AddRow(
onDone,
onCancel,
children,
},
ref,
}: AddRowProps,
ref: Ref<HTMLDivElement>,
) {
return (
<div
@@ -59,14 +69,3 @@ export const AddRow = forwardRef(function AddRow(
</div>
);
});
AddRow.propTypes = {
value: PropTypes.string.isRequired,
isValid: PropTypes.bool.isRequired,
placeholder: PropTypes.string,
onKeyDown: PropTypes.func,
onChange: PropTypes.func.isRequired,
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
children: PropTypes.node,
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import cx from "classnames";
import { Fragment, useEffect, useState } from "react";
import { msgid, ngettext, t } from "ttag";
@@ -16,6 +15,8 @@ import {
import { connect } from "metabase/lib/redux";
import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins";
import { getUser } from "metabase/selectors/user";
import type { Group, Member, User } from "metabase-types/api";
import type { State } from "metabase-types/store";
import {
createMembership,
@@ -27,7 +28,11 @@ import { getGroupMemberships, getMembershipsByUser } from "../selectors";
import GroupMembersTable from "./GroupMembersTable";
const GroupDescription = ({ group }) =>
interface GroupDescriptionProps {
group: Group;
}
const GroupDescription = ({ group }: GroupDescriptionProps) =>
isDefaultGroup(group) ? (
<div className={cx(CS.px2, CS.textMeasure)}>
<p>
@@ -49,13 +54,43 @@ const GroupDescription = ({ group }) =>
</div>
) : null;
const mapStateToProps = (state, props) => ({
interface GroupDetailStateProps {
groupMemberships: Member[];
membershipsByUser: Record<User["id"], Member[]>;
currentUser: User | null;
}
interface GroupDetailOwnProps {
group: Group;
users: User[];
}
const mapStateToProps = (
state: State,
props: GroupDetailOwnProps,
): GroupDetailStateProps => ({
// @ts-expect-error -- .js file imports with wrong type here, not worth fixing as we should just move to RTKQuery
groupMemberships: getGroupMemberships(state, props),
membershipsByUser: getMembershipsByUser(state),
currentUser: getUser(state),
});
const mapDispatchToProps = {
interface GroupDetailDispatchProps {
createMembership: (membership: { groupId: number; userId: number }) => void;
updateMembership: (membership: Member) => void;
deleteMembership: (membershipId: number) => void;
loadMemberships: () => void;
confirmDeleteMembershipAction: (
membershipId: number,
userMemberships: Member[],
) => void;
confirmUpdateMembershipAction: (
membership: Member,
userMemberships: Member[],
) => void;
}
const mapDispatchToProps: GroupDetailDispatchProps = {
createMembership,
deleteMembership,
updateMembership,
@@ -72,7 +107,11 @@ const mapDispatchToProps = {
),
};
const GroupDetail = ({
type GroupDetailProps = GroupDetailStateProps &
GroupDetailOwnProps &
GroupDetailDispatchProps;
const GroupDetailInner = ({
currentUser,
group,
users,
@@ -84,22 +123,22 @@ const GroupDetail = ({
loadMemberships,
confirmDeleteMembershipAction,
confirmUpdateMembershipAction,
}) => {
}: GroupDetailProps) => {
const { modalContent, show } = useConfirmation();
const [addUserVisible, setAddUserVisible] = useState(false);
const [alertMessage, setAlertMessage] = useState(null);
const [alertMessage, setAlertMessage] = useState<string | null>(null);
useEffect(() => {
loadMemberships();
}, [loadMemberships]);
const alert = (alertMessage) => setAlertMessage(alertMessage);
const alert = (message: string | null) => setAlertMessage(message);
const onAddUsersClicked = () => setAddUserVisible(true);
const onAddUserCanceled = () => setAddUserVisible(false);
const onAddUserDone = async (userIds) => {
const onAddUserDone = async (userIds: number[]) => {
setAddUserVisible(false);
try {
await Promise.all(
@@ -111,11 +150,18 @@ const GroupDetail = ({
}),
);
} catch (error) {
alert(error && typeof error.data ? error.data : error);
const errorMessage =
error instanceof Error ? error.message : String(error);
alert(errorMessage);
}
};
const handleChange = async (membership) => {
const handleChange = async (membership: Member) => {
if (!currentUser) {
throw new Error("currentUser is not defined");
return;
}
const confirmation = PLUGIN_GROUP_MANAGERS.getChangeMembershipConfirmation(
currentUser,
membership,
@@ -127,6 +173,7 @@ const GroupDetail = ({
show({
...confirmation,
title: confirmation.title ?? "",
onConfirm: () =>
confirmUpdateMembershipAction(
membership,
@@ -135,7 +182,12 @@ const GroupDetail = ({
});
};
const handleRemove = async (membershipId) => {
const handleRemove = async (membershipId: number) => {
if (!currentUser) {
throw new Error("currentUser is not defined");
return;
}
const confirmation = PLUGIN_GROUP_MANAGERS.getRemoveMembershipConfirmation(
currentUser,
membershipsByUser[currentUser.id],
@@ -148,6 +200,7 @@ const GroupDetail = ({
show({
...confirmation,
title: confirmation.title ?? "",
onConfirm: () =>
confirmDeleteMembershipAction(
membershipId,
@@ -171,7 +224,7 @@ const GroupDetail = ({
</Fragment>
}
buttonText={t`Add members`}
buttonAction={canEditMembership(group) ? onAddUsersClicked : null}
buttonAction={canEditMembership(group) ? onAddUsersClicked : undefined}
buttonDisabled={addUserVisible}
>
<GroupDescription group={group} />
@@ -193,4 +246,7 @@ const GroupDetail = ({
);
};
export default connect(mapStateToProps, mapDispatchToProps)(GroupDetail);
export const GroupDetail = connect(
mapStateToProps,
mapDispatchToProps,
)(GroupDetailInner);

View File

@@ -3,7 +3,7 @@ import { useMemo } from "react";
import { t } from "ttag";
import { useListApiKeysQuery } from "metabase/api";
import AdminContentTable from "metabase/components/AdminContentTable";
import { AdminContentTable } from "metabase/components/AdminContentTable";
import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
import { PaginationControls } from "metabase/components/PaginationControls";
import Link from "metabase/core/components/Link";
@@ -17,7 +17,7 @@ import { Icon, Text, Tooltip } from "metabase/ui";
import type { ApiKey, Group, User as IUser, Member } from "metabase-types/api";
import type { State } from "metabase-types/store";
import AddMemberRow from "../AddMemberRow";
import { AddMemberRow } from "../AddMemberRow";
const canEditMembership = (group: Group) =>
!isDefaultGroup(group) && PLUGIN_GROUP_MANAGERS.UserTypeCell;

Some files were not shown because too many files have changed in this diff Show More