Compare commits

...

59 Commits

Author SHA1 Message Date
ChrisVandoo
75df201252 more changes 2026-08-26 13:15:51 -04:00
ChrisVandoo
2779f14fd8 will this retrigger CI everywhere? 2026-08-26 13:15:51 -04:00
ChrisVandoo
b78ac4affd changes 2026-08-26 13:15:51 -04:00
Kevin Kelleher
acaaa73106 Use Markdown for Measure/Segment description fields (#80883)
* markdown for measure and segment descriptions

* Add unit tests that Measure/Segment descriptions render markdown
2026-08-26 17:14:10 +00:00
Ben Grabow
2e58c450be Skip extra server round-trips when loading data (#80958) 2026-08-26 12:13:54 -05:00
Ben Grabow
039a969544 Reuse the impersonation database for each impersonation deftest (#80750)
* Reuse the impersonation database for each impersonation deftest

* Remove dynamic layer of fixture
2026-08-26 16:48:24 +00:00
Ben Grabow
f5ac3e3c64 Fix flaky test caused by noise from batched query executions from other deftests (#80943)
* Eliminate noise from batched query executions from other deftests

* Update ratchets
2026-08-26 16:48:20 +00:00
Riley Thompson
44b811bdcc Use separate page-byte-budget for bigquery query execution and sync (#80849)
* save

* fix bigquery csv downloads

* remove debug
2026-08-26 16:32:51 +00:00
Maksym Yakubych
0421c77905 Normalize scalar search tool arguments (#80107) 2026-08-26 12:24:21 -03:00
Nicola Mometto
bd6f636e35 Quote ClickHouse impersonation role as a single identifier (#80820)
* Quote ClickHouse impersonation role as a single identifier

Stop splitting the role attribute on commas so the whole value is one
role identifier. This removes multi-role activation via a comma-separated
attribute.

* Escape backslashes when quoting the ClickHouse role identifier

A trailing backslash would otherwise escape the closing quote of the
role identifier. Escape backslashes before doubling interior quotes.
2026-08-26 16:17:27 +01:00
Nemanja Glumac
15ef5d5b97 Delete skipped e2e tests (#81011)
* Delete e2e tests tagged @skip

Removes 53 skipped blocks (86 test cases), the oldest of them skipped since
2020, plus the fixtures and helpers they left unreferenced. Three spec files
held nothing but skipped tests and are deleted outright.

The @skip tag itself still exists in the workflows and the matrix builder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop the @skip grep tag from the e2e workflows

No spec carries the tag anymore, so the exclusion is dead weight. With
`-@skip` gone the tagged-run expression no longer needs format() to append
it, and collapses to the input or the default tag set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:55:15 +02:00
Phoomparin Mano
b0525d2dc4 Fix tenant collections showing up as unknown in the dashboard question picker (#80824)
* Fix tenant collection question picker

* Fix tenant collection question picker root browsing

* Add tenant collection picker hierarchy

* refactor: extract tenant collection helper module

* Fix tenant collection question picker

* refactor: cleanup unit tests

* refactor: simplify unit tests

* chore: rename synthetic root to collection

* fix: only show root collection if user has access to it

* simplify condition for shouldIncludeRoot

* fix flaky dashboard-tenant-user-question-picker.cy.spec.ts test
2026-08-26 21:51:53 +07:00
Andrei Chernykh
c2f5f3bd7a Remove unused dev dependencies (#80724)
* Remove unused dev-tool dependencies

* Add openapi-typescript back
2026-08-26 07:50:06 -07:00
Paolo Rascunà
a9f25e8645 BOT-2010: Post Metabot's Slack answer as a markdown block (#80652)
* BOT-2010: Post Metabot's Slack answer as a markdown block

Channel answers went in an `mrkdwn` `section`: no table syntax, and a
3000-char cap that cut them short. They now go in a `markdown` block at 12000.

- `final-text-blocks` emits `{:type "markdown"}`, and the answer is elided to
  `markdown-text-limit` (12000) rather than the section limit
- the prompt switches bold and strikethrough to `**text**` and `~~text~~`;
  `*one*` renders italic and `~one~` does not render at all, so the old
  dialect would have silently lost both. Link syntax is untouched --
  `<url|label>` does render inside a `markdown` block (measured)
- the test harness models Slack's real rejection for each block type
  (`invalid_blocks` for `section`, bare `msg_too_long` for `markdown`), and
  charges the markdown budget across the payload as Slack documents it.
  Without this the answer would no longer be checked by anything and the
  limit assertions would pass vacuously

Verified against the live API: a `markdown` block takes exactly 12000 chars,
Slack expands one into several as it renders -- a heading becomes a `header`,
a rule a `divider`, a table a `table` -- and that expansion counts against
the 50-block ceiling, which is why the prompt still bans headings and rules.


Co-authored-by: Paolo Rascuna <1936871+prascuna@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:40:03 +02:00
Ryan Laurie
70a3c288ed Stabilize Embedding Parameters test (#80761)
* try some stuff

* limit query
2026-08-26 08:07:11 -06:00
Stas Gavrylov
3f2ad8bd17 Upgrade Node.js to 22.23.2 (#80995)
* Upgrade Node.js to 22.23.2

* Use .nvmrc as mise Node version source
2026-08-26 16:46:12 +03:00
Chris Vandoodewaard
fd13d19cad Fix rerun notification: don't notify for PR stacks and thread rerun notifications (#81012)
* don't notify for stacks

* thread Slack notifications

* use short commit

* use run id as key

* add error handling
2026-08-26 09:45:52 -04:00
Nicola Mometto
02a1dc223b Restrict notification markdown link schemes and diff pulse recipients by email (#80597)
* Restrict markdown link URI schemes in rendered notifications

Only http/https/mailto links and images survive markdown rendering for
email/Slack notifications; other schemes and unparseable URIs are
dropped instead of being passed through (or, for malformed URIs when a
site URL is set, throwing mid-render).

* Diff pulse recipients by id or email in the subscription-permission check

Key the added-recipients diff the same way pulse-channel keys
recipients (user recipients by :id, external ones by :email) so
external-address changes are covered by the check too.

* Allow inline base64 images in notification markdown

The app's markdown renderer accepts data:image/...;base64 URIs
(DATA_IMAGE_URI_PATTERN); let notification rendering accept the same
pattern so text-card images look the same in email.

* Cross-link DATA_IMAGE_URI_PATTERN to its backend counterpart

* Share the data-image URI pattern between backend and frontend

Lift the pattern into metabase.util.markdown.image (cljc), compile it
into the frontend bundle via shadow-cljs, and re-export it as
DATA_IMAGE_URI_PATTERN so both renderers accept the same inline images
by construction.

* Apply resolve-uri to Slack images too

* Render a dropped link-ref as its text alone instead of restoring brackets
2026-08-26 12:09:26 +00:00
Kevin Kelleher
da72c9e970 Fix Trend visualization for native MongoDB queries (#80751)
remove trend viz dependency on insights
2026-08-26 07:39:10 -04:00
Sébastien
e83b3f6abe Fix flaky drill-through tests in the sdk iframe embedding spec (#80971)
Scope embed drill-through cell lookups to the table
2026-08-26 13:02:34 +02:00
Fraser
28f667591b Give every bundler the side-effect-free declarations (#80956) 2026-08-26 22:30:21 +12:00
Romeo Van Snick
5a315ad0b4 Move the action creator and embedding data picker off the metadata mirror (#80975)
* Compute the action model's write permission from the API, not the metadata mirror

* Read the embedding picker's source card from the API, not the metadata mirror
2026-08-26 20:12:41 +10:00
github-automation-metabase
a04aaf3714 Fix flaky bulk-table 'edit attributes for db' e2e (#78169)
Clicking the database row in the TablePicker before its expand handler is
wired drops the click, so the schema fetch never fires and cy.wait("@getSchema")
times out with "No request ever occurred." Gate on the expand toggle's
aria-expanded state (collapsed -> click -> expanded) so the schema request
reliably fires before selecting the database — same pattern already proven in
the sibling publish/unpublish test.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-26 12:01:39 +02:00
YB
24a9ec6182 auth-check transform source (#80328)
A transform's :query source is auth-checked against its source database but leaves out some entities named by the query. This checks those too by leveraging on QP's check-query-permissions*.
2026-08-26 09:59:55 +00:00
github-automation-metabase
49716266bf Fix flaky data-model field-description preview hovercard (#78284)
The shared verifyTablePreview helper opened the column-description
hovercard with a single one-shot realHover(). That hovercard opens on a
delayed openDelay timer, so a lone realHover can miss it: after
cy.wait("@dataset") the preview grid re-renders on a microtask and
Chrome v133+ headless hit-tests CDP mouse events differently, either of
which drops the hover and cancels the open timer. hovercard() then only
retries the query, never re-hovers, so the card stays permanently
absent for that attempt.

Replace realHover() with the re-queried synthetic mouseenter+mouseover
dispatch already proven in data-model-shared-1's local verifyTablePreview,
so the events land on the post-render DOM node.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-26 11:34:25 +02:00
Paolo Rascunà
9cd6391ce9 BOT-522: Seed the Slack agent loop with persisted conversation state (#80204)
* BOT-522: Seed the Slack agent loop with persisted conversation state

The Slack loop wrote `metabot_message.state` on every turn but always started
`run-agent-loop` with `:state {}`, so it never read back what it had persisted —
unlike the in-app path, which seeds from `conversation-state`.

This makes sure we feed the state back into the agent loop, filtering out errored messages.

Co-authored-by: Paolo Rascuna <1936871+prascuna@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:45:44 +02:00
YB
f11ceeac6e [GDGT-3088] gate slack bug reports on bug-reporting-enabled and enforce attribution to session user when non-anon (#80467)
This feature was not following the bug-reporting-enabled gate on the BE previously, and we now enforce using the session user when making a non-anonymous report.

As this is a endpoint that has been in the wild for some time, we'll first report any non-compliance first while overriding with the desired behaviour of only using the session user instead of breaking the API by returning an error
2026-08-26 14:58:41 +08:00
Romeo Van Snick
9b3874e114 Delete the entity-framework hooks (#80969) 2026-08-26 15:52:04 +10:00
Romeo Van Snick
4480309da3 Read a field's remappings through a store-owned selector (#80968) 2026-08-26 15:34:06 +10:00
Romeo Van Snick
c7690123cf Hold a dashboard's click-behavior link targets in dashboard state (#80787) 2026-08-26 12:37:40 +10:00
Romeo Van Snick
3fe2998633 Read the permissions tree's databases from the API, not the metadata mirror (#80403)
* Read the permissions tree's databases from the API, not the metadata mirror

* Record each edit's database in permissions state instead of holding subscriptions

* Take each database field from the endpoint that returns it

Two e2e failures traced to the same cause: no single endpoint returns
everything the permissions tree reads.

GET /api/database is the only endpoint that hydrates router_user_attribute
(warehouses_rest/api.clj:361 and :501). Reading the database from
getDatabaseMetadata therefore dropped it, and the "(Database routing enabled)"
breadcrumb stopped rendering.

GET /api/database/:id/metadata is the only endpoint that returns tables. The
group page's database list passes listDatabases entries, which have none, so
hasPermissionValueInSubgraph found no children and the blocked-child
confirmation never opened.

The mirror had merged both, and held them for the session. getPermissionsDatabase
now merges them explicitly, taking router_user_attribute from the list and tables
from the metadata response. The tables are kept in permissions state rather than
read back from the request's cache entry, which is dropped as soon as the admin
navigates away from that database.

databases.unit.spec.ts pins the merge with fixtures shaped like the two real
responses, which is what the previous specs could not catch: they seeded one
complete database, making the two sources indistinguishable.
2026-08-26 12:18:00 +10:00
Romeo Van Snick
7c8f7b450b Load the whitelabel settings pages lazily (#80788) 2026-08-26 11:59:21 +10:00
Alexander Polyankin
4a7f73029a Store passwords and reset tokens in auth_identity (#80718) 2026-08-25 21:52:54 -04:00
Ryan Laurie
88432c1055 Stabilize table selection in flaky segments/datamodel test (#80681)
* stabilize table selection

* wait on the network requests
2026-08-25 17:18:47 -06:00
Andrei Chernykh
b3bcce1e6b Fix timezone warning for equivalent timezones (UXW-133) (#80131)
Report the results timezone under the requested name when the two are aliases, so charts stop warning about equivalent timezones
2026-08-25 15:31:19 -07:00
github-automation-metabase
5fced6e7da Auto-update documentation for master (#80938)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-25 21:26:11 +00:00
dpsutton
d90b61d26d use authoritative query database id from actions (#80363)
like cards, the actions table has a top level column for database
id. But we can use the canonical value on the action's query and let the
db value be for bulk lookups and as an index. But we make sure to keep
it in lockstep as we go.
2026-08-25 16:09:11 -05:00
Andrei Chernykh
6cd29dedb0 Replace legacy button CSS with Mantine buttons (#80386)
* Replace legacy button CSS with Mantine buttons

* Do not click the disabled map update button in the e2e test

* Polish admin header and add-row wrapping

* Use static view-section paddings instead of clamp
2026-08-25 14:04:03 -07:00
dpsutton
9c2ee7c41d Refuse analytics-dev DB connection/creation unless analytics-dev-mode… (#80901)
* Refuse analytics-dev DB connection/creation unless analytics-dev-mode is on

An :is-audit-dev database is a handle onto the app-db; it carries no real
connection details of its own. Reusing the app-db datasource for it is only
ever valid while analytics dev mode is enabled. Previously the connection
router only *skipped* the datasource reuse when the mode was off, silently
falling through to build a credential-less pool against the wrong host
(surfacing in CI as `FATAL: role "runner" does not exist`).

Make the invariant explicit and fail loudly at every site:
- db->pooled-connection-spec throws for an :is-audit-dev DB when the mode is off
- can-connect? throws likewise instead of reporting the DB as connectable
- create-analytics-dev-database! refuses to create the DB when the mode is off

Tests create/connect these DBs, so they now run under analytics-dev-mode=true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b

* Update is-audit-dev routing test for the throw-on-invariant-violation behavior

The connection layer now throws for an :is-audit-dev database reached with
analytics-dev-mode off, rather than silently declining to route it to the app
DB. Update the test's dev-mode-off case to assert the throw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:47:14 -05:00
Riley Thompson
bed8a61829 External PR: db-tables as multimethod to enable driver implementation (#78842)
* clean(materialized-tables): consolidate changes for materialized tables v2

* small changes

---------

Co-authored-by: damienchambe <damien.chambe@gmail.com>
Co-authored-by: Damien Chambe <42462209+damienchambe@users.noreply.github.com>
2026-08-25 16:34:18 -04:00
Chris Vandoodewaard
5a65c04cda add -F msgcat to sort and stablize .po files (#80919)
* add -s to msgcat to sort entries by msgid

* actually sort by file
2026-08-25 16:33:18 -04:00
Denis Berezin
11e969a619 UXW-5034 New UI for database caching settings (#80034)
* UXW-5034 New UI for database caching settings

* Fix issues

* Address feedback

* Address design review fixes

* Fix e2e

* Fix wrapping description text
2026-08-25 23:30:15 +03:00
Ben Grabow
0312bafbc9 Prefix PR title with target version number (#80917)
* Prefix PR title with target version number

* Robot emoji is most important
2026-08-25 19:38:21 +00:00
github-automation-metabase
3eabd5847f Deflake dependency broken list spec (#78600)
* Deflake dependency broken list spec

visitBrokenDependencies did a bare cy.visit with no wait, so tests
immediately queried dynamically-loaded list items with only Cypress's
default 4s timeout - flaky when the breaking-dependencies API is slow.

Mirror the sibling visitUnreferencedEntities helper: intercept the
/api/ee/dependencies/graph/breaking request, wait on it, and assert the
list is visible before the test interacts with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wait for the test's own broken entities before asserting

The prior deflake waited for the page's breaking-dependencies request, but
the underlying data still raced: setup gated on a bare count
(nodes.length >= 3), which the instance's pre-existing Usage Analytics
breaking dependencies satisfy before the test's transform-table break is
analyzed. The page then rendered only Usage Analytics rows and the test's
"Test Transform Table" / question / model were absent.

Gate setup on the specific expected entities by name instead of a count,
and align the breaking helper to include personal collections (matching its
sibling waitForUnreferencedEntities) so the personal-collection question is
visible to the check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 20:59:28 +02:00
Chris Vandoodewaard
b3cddb3504 Notify Slack when "Run Tests" re-runs more than 5 times (#80854)
* add workflow to notify slack on repeated reruns

* fixes

* a few more fixes

* cleanup whitespace

* update to use triggering_actor
2026-08-25 14:36:54 -04:00
Ben Grabow
b1d98f6538 Divide redshift tests first by enterprise/oss, then split by var count (#80789) 2026-08-25 18:29:11 +00:00
Anderson
adc97eaff2 Deflake dashboard title-drill anchor href assertions (#80739)
* Deflake dashboard title-drill anchor href assertions

* Park the cursor without an extra page visit
2026-08-25 17:29:31 +00:00
Ben Grabow
0a3659ea3d Fix error in Summarize test wall times (#80876) 2026-08-25 19:26:09 +02:00
Ben Grabow
4539b47457 Include full log output in profiling bundle (#80848) 2026-08-25 19:24:35 +02:00
Andrei Chernykh
6be03b850b Remove mutationobserver-shim (#80748) 2026-08-25 10:24:23 -07:00
Andrei Chernykh
38346d6268 Remove unused prod dependencies (#80663) 2026-08-25 10:23:46 -07:00
Anderson
6059647f51 Fix flaky Other pie slice drill-through e2e test (#80717)
Fix flaky Other pie slice drill e2e test and skip copy value action for grouped clicks
2026-08-25 13:53:56 -03:00
metamben
87841b3fe6 Compile current time in filters the converted timezone for Postgres (#80244)
Closes QUE2-807
2026-08-25 16:04:25 +00:00
metamben
4e60ba6fad Properly set the report timezone for ClickHouse (#80196)
* Properly set the report timezone for ClickHouse

Closes QUE2-792

* Add clickhouse to the list of tricky to stub drivers
2026-08-25 19:01:58 +03:00
metamben
36fd11b0f7 Take ClickHouse's date bucketing into account (#80110)
Closes QUE2-790
2026-08-25 19:01:46 +03:00
metamben
6ca347e9e2 Enable native pivot support for hive-like drivers (#80333)
* Enable native pivot support for hive-like drivers
2026-08-25 19:01:25 +03:00
Riley Thompson
e58b5a46fb Only rewrite temporal unit on order bys and breakouts (#80124)
* only rewrite breakouts and order bys

* remove test stuff
2026-08-25 12:01:21 -04:00
Egor Grushin
27bfc0d005 Checkbox component redesign (#80262)
* checkbox redesign

* small fix

* fix disabled checkbox visual size

* disallow checkbox labelPosition

* add new loki tests

* update loki snapshots

* fixup! update loki snapshots

* fix e2e specs relying on checkbox Icon-check/Icon-dash classes

* checkbox card border

add border-highlight token, use it for selected card

* disallow checkbox size

single 16px size; xs/sm were both 16px and md was unused

* disallow checkbox card children

* drop redundant role from checkbox icon

* checkbox card body via mantine text/stack

* forwardRef checkbox card

* drop border on disabled checkbox card

* drop disabled checkbox border ring

alpha border over alpha bg stacked into a visible ring

* hide unchecked card indicator icon

mantine hides indicator icon via color:transparent; .icon color override defeated it

* update colors loki snapshot
2026-08-25 16:23:38 +01:00
Ben Grabow
b7322989e3 Use explicit hierarchy for temporal literal classes (#79461)
The java.time classes grouped under ::->absolute-datetime and ::->time were derived in
Clojure's global hierarchy, sharing one mutable namespace with model keywords, hook traits and
every other subsystem -- and registering concrete JDK classes there is especially easy to
collide with.

Move them into a hierarchy var private to this namespace, mutated through derive! -- the same
shape as metabase.events.hierarchy, kept in-file because add-type-info is private and is the
only reader.

The prefer-method calls against Object still apply: class-to-class isa? goes through Java
assignability rather than any keyword hierarchy, so Object continues to match every temporal
class regardless of which hierarchy is in use.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:29:38 -05:00
418 changed files with 8900 additions and 10174 deletions

View File

@@ -451,8 +451,7 @@
bug-reporting
{:team "UX West"
:api #{metabase.bug-reporting.api
metabase.bug-reporting.init}
:api #{metabase.bug-reporting.api metabase.bug-reporting.init metabase.bug-reporting.settings}
:uses #{analytics
api
app-db
@@ -505,6 +504,7 @@
api
app-db
appearance
bug-reporting
collections
config
dashboards
@@ -1962,6 +1962,7 @@
metabase.query-processor.pivot
metabase.query-processor.preprocess
metabase.query-processor.schema
metabase.query-processor.setup
metabase.query-processor.store
metabase.query-processor.streaming
metabase.query-processor.streaming.common
@@ -2264,7 +2265,7 @@
setup
system
util}
:model-imports #{:model/AuthIdentity :model/User}}
:model-imports #{:model/User}}
slackbot
{:team "Metabot"
@@ -2523,6 +2524,7 @@
premium-features
query-processor
remote-sync
request
revisions
run-tracking
search
@@ -2696,6 +2698,7 @@
metabase.users.settings}
:uses #{analytics
api
auth-identity
batch-processing
channel
config
@@ -2711,11 +2714,9 @@
util
enterprise/advanced-permissions}
:model-exports #{:model/User :model/UserParameterValue}
:model-imports #{:model/AuthIdentity
:model/Dashboard
:model-imports #{:model/Dashboard
:model/Database
:model/PermissionsGroupMembership
:model/Session
:model/Tenant}}
users-rest
@@ -2737,8 +2738,7 @@
users
util
enterprise/advanced-permissions}
:model-imports #{:model/AuthIdentity
:model/Card
:model-imports #{:model/Card
:model/Collection
:model/Dashboard
:model/LoginHistory
@@ -2972,6 +2972,7 @@
:uses #{api
api-keys
app-db
auth-identity
driver
lib
permissions

View File

@@ -27,12 +27,11 @@
:metabase/validate-defendpoint-has-response-schema 440
:metabase/validate-defendpoint-query-params-use-kebab-case 50
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 9
:metabase/validate-deftest 10
:missing-docstring 10
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3
:redundant-fn-wrapper 1
:syntax 2
:uninitialized-var 1
:unquote-not-syntax-quoted 1

View File

@@ -18,6 +18,10 @@ runs:
steps:
# Unlike a JUnit failure report, these run on a green build too — a suite that passes slowly is
# exactly what profiling is for.
#
# `awk 'NR<=25'` rather than `head -25`: the runner ignores SIGPIPE, so `head` exiting early
# leaves `sort` with an EPIPE write error and `pipefail` fails the step. `|| true` covers `grep`
# finding no junit files.
- name: Summarize test wall times
if: ${{ inputs.profiled == 'true' }}
continue-on-error: true
@@ -28,18 +32,21 @@ runs:
echo '```'
grep -h '<testsuite' target/junit/*.xml 2>/dev/null \
| sed -E 's|.*<testsuite name="([^"]*)" time="([^"]*)".*|\2 \1|' \
| sort -rn | head -25
| sort -rn | awk 'NR<=25' || true
echo '```'
echo "### Slowest tests (seconds)"
echo '```'
grep -h '<testcase' target/junit/*.xml 2>/dev/null \
| sed -E 's|.*classname="([^"]*)" name="([^"]*)" time="([^"]*)".*|\3 \1/\2|' \
| sort -rn | head -25
| sort -rn | awk 'NR<=25' || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Only the rendered flamegraph is kept: the raw collapsed stacks it was built from run to
# hundreds of MB on a long suite, and are reproducible locally from the same test selection.
#
# The info-level test log rides along on a green profiled run too. The failure-only log artifact
# the test jobs upload is not written when nothing failed, which is exactly the run being profiled.
- name: Upload profile
if: ${{ inputs.profiled == 'true' }}
continue-on-error: true
@@ -49,5 +56,6 @@ runs:
path: |
target/clj-async-profiler/results/*flamegraph.html
target/junit/*.xml
logs/test-log.json
retention-days: 14
if-no-files-found: warn

View File

@@ -73,7 +73,7 @@ runs:
git push -u origin ${BACKPORT_BRANCH}
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 v${TARGET_VERSION} backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
if [ "$CONFLICTS" -gt 0 ]; then
PR_ARGS+=(--draft)
fi

View File

@@ -955,17 +955,44 @@ jobs:
test-args: >-
:only-tags [:mb/upload-tests]
:fail-fast? true
- name: Redshift (Excluding Upload Tests) Part 1
# `:partition/*` balances on test-var count, so it cannot break up a block of namespaces that is
# cheap in vars and expensive in seconds. The `metabase-enterprise.*` driver tests are exactly that:
# ~10% of the vars and ~40% of the wall clock, and they sort ahead of `metabase.*`, so a plain 2-way
# split hands one job the whole block and times out. Splitting on the namespace pattern first puts
# them in their own pair of jobs, and each pair then partitions by count within a comparable cost.
# The two patterns are exhaustive over the default `:namespace-pattern` -- every namespace lands in
# exactly one of them -- so nothing stops running when a namespace is added.
- name: Redshift EE (Excluding Upload Tests) Part 1
skip: false
test-args: >-
:namespace-pattern '"^metabase-enterprise[.].*"'
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 0
- name: Redshift (Excluding Upload Tests) Part 2
- name: Redshift EE (Excluding Upload Tests) Part 2
skip: false
test-args: >-
:namespace-pattern '"^metabase-enterprise[.].*"'
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 1
- name: Redshift OSS (Excluding Upload Tests) Part 1
skip: false
test-args: >-
:namespace-pattern '"^(?:metabase[.]|hooks[.]).*"'
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 0
- name: Redshift OSS (Excluding Upload Tests) Part 2
skip: false
test-args: >-
:namespace-pattern '"^(?:metabase[.]|hooks[.]).*"'
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true

View File

@@ -97,7 +97,7 @@ jobs:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
node e2e/runner/run_cypress_ci.js e2e \
--expose grepTags="-@mongo+-@python+-@OSS+-@skip"
--expose grepTags="-@mongo+-@python+-@OSS"
# Don't let a single failing/flaky spec abort the shard — we still want
# coverage from the specs that did run.
continue-on-error: true

View File

@@ -181,7 +181,7 @@ jobs:
run: |
echo "Running tests with specs: $SPEC_PATH"
node e2e/runner/run_cypress_ci.js e2e \
--expose grepTags="-@mongo+-@python+-@OSS+-@skip" \
--expose grepTags="-@mongo+-@python+-@OSS" \
--spec "$SPEC_PATH"
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true
@@ -198,7 +198,7 @@ jobs:
run: |
echo "Running tests with specs: $SPEC_PATH"
node e2e/runner/run_cypress_ci.js e2e \
--expose grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@mongo+-@python+-@OSS+-@skip'}}" \
--expose grepTags="${{ inputs.tags || '-@mongo+-@python+-@OSS' }}" \
--spec "$SPEC_PATH"
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true

137
.github/workflows/notify.rerun-limit.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: Notify On Re-run Limit
on:
workflow_run:
workflows:
- "Run tests"
types:
- completed
jobs:
notify:
name: Send Slack Notification If Workflow Run Attempts Exceed Limit
if: ${{ github.event.workflow_run.run_attempt > 5 && github.repository == 'metabase/metabase' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Send Slack Notification
uses: actions/github-script@v9
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_NAME: ${{ github.event.workflow_run.name }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
TRIGGERING_ACTOR_LOGIN: ${{ github.event.workflow_run.triggering_actor.login }}
with:
script: |
const { findSlackMessage, mentionUserByGithubLogin, sendSlackMessage, sendSlackReply } = require(`${{ github.workspace }}/release/dist/index.cjs`);
const CHANNEL_NAME = 'engineering-ci';
// made a few changes...
// even more changes
const ATTEMPT = Number(process.env.ATTEMPT);
const COMMIT = process.env.COMMIT;
const SHORT_COMMIT = COMMIT.slice(0, 7);
const RUN_ID = process.env.RUN_ID;
const RUN_NAME = process.env.RUN_NAME;
const RUN_URL = process.env.RUN_URL;
const TRIGGERING_ACTOR_LOGIN = process.env.TRIGGERING_ACTOR_LOGIN;
// Every pull request in a stack re-runs as the stack is rebased, so re-runs shouldn't trigger notifications.
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: COMMIT,
}).catch(error => {
core.warning(`Could not fetch pull requests for ${SHORT_COMMIT}: ${error.message}`);
return { data: [] };
});
const stackedPullRequest = pullRequests.find(pullRequest => pullRequest.stack);
if (stackedPullRequest) {
const { number, stack } = stackedPullRequest;
core.info(`Skipping notification: PR #${number} is ${stack.position} of ${stack.size} in stack #${stack.number}.`);
return;
}
// Clean up PR title to avoid breaking the link to the PR in Slack
const pullRequest = pullRequests.find(pullRequest => pullRequest.state === 'open') ?? pullRequests[0];
const pullRequestLink = pullRequest
? ` in <${pullRequest.html_url}|#${pullRequest.number} ${pullRequest.title.replace(/[<>|]/g, '')}>`
: '';
const messageText = `Workflow Retry Limit Exceeded for run ${RUN_ID} of ${RUN_NAME}`;
const existingMessage = await findSlackMessage({
channelName: CHANNEL_NAME,
text: messageText,
limit: 200,
}).catch(error => {
core.warning(`Could not search #${CHANNEL_NAME} for an existing message: ${error.message}`);
return null;
});
if (existingMessage) {
await sendSlackReply({
channelName: CHANNEL_NAME,
messageId: existingMessage.id,
message: `:repeat: <${RUN_URL}|${RUN_NAME}> ran again: that is ${ATTEMPT - 1} re-runs for this workflow run.`,
}).catch(error => {
core.warning(`Could not post a reply to #${CHANNEL_NAME}: ${error.message}`);
});
return;
}
await sendSlackMessage({
channelName: CHANNEL_NAME,
message: messageText,
blocks: [
{
"type": "header",
"text": {
"type": "plain_text",
"text": `:rotating_light: Workflow Rerun Limit Exceeded :rotating_light:`,
"emoji": true,
}
},
],
attachments: [{
color: "#ff4500",
blocks: [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `<${RUN_URL}|${RUN_NAME}> has been re-run ${ATTEMPT - 1} times by ${mentionUserByGithubLogin(TRIGGERING_ACTOR_LOGIN)} for <https://github.com/metabase/metabase/commit/${COMMIT}|${SHORT_COMMIT}> commit${pullRequestLink}.`
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Repeatedly re-running CI workflows wastes CI resources and may cause more issues. If re-running a workflow a handful of times does not resolve an issue, reach out to the github-admins Slack group to force merge the PR."
}
}
],
}],
}).catch(error => {
core.warning(`Could not post a message to #${CHANNEL_NAME}: ${error.message}`);
});

View File

@@ -1,6 +1,8 @@
name: Update Release Log
run-name: Update Release Log ${{ inputs.version || github.ref_name }}
# some other changes
on:
push:
branches:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 418 KiB

2
.nvmrc
View File

@@ -1 +1 @@
v22.13.1
v22.23.2

View File

@@ -6,6 +6,9 @@ const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CSS_CONFIG } = require("../frontend/build/shared/rspack/css-config");
const {
SIDE_EFFECT_FREE_RULE,
} = require("../frontend/build/shared/rspack/side-effect-free-modules");
const mainAppStories = [
"../frontend/**/*.mdx",
@@ -80,6 +83,7 @@ const config: StorybookConfig = {
module: {
...config.module,
rules: [
SIDE_EFFECT_FREE_RULE,
...(config.module?.rules ?? []).filter(
(rule) => !isCSSRule(rule) && !isSvgRule(rule),
),

View File

@@ -57,4 +57,4 @@ clojure -M:generate-automagic-dashboards-pot
# merge all pots #
##################
msgcat --add-location=file "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME"
msgcat --add-location=file -F "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME"

437
bun.lock

File diff suppressed because it is too large Load Diff

263
docs/api.json generated
View File

@@ -16623,6 +16623,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/agent" ]
}
},
@@ -17728,6 +17738,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/apps" ]
}
},
@@ -21890,6 +21910,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/comment" ]
},
"post" : {
@@ -21975,6 +22005,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/comment" ]
}
},
@@ -23383,6 +23423,18 @@
"pattern" : "^[A-Za-z0-9_\\-]{21}$"
} ]
}
}, {
"in" : "query",
"name" : "dashboard_load_id",
"required" : false,
"schema" : {
"oneOf" : [ {
"type" : "string",
"minLength" : 1
}, {
"type" : "null"
} ]
}
} ],
"responses" : {
"2XX" : {
@@ -24132,6 +24184,18 @@
"pattern" : "^[A-Za-z0-9_\\-]{21}$"
} ]
}
}, {
"in" : "query",
"name" : "dashboard_load_id",
"required" : false,
"schema" : {
"oneOf" : [ {
"type" : "string",
"minLength" : 1
}, {
"type" : "null"
} ]
}
} ],
"responses" : {
"2XX" : {
@@ -25204,6 +25268,18 @@
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
}, {
"in" : "query",
"name" : "include_editable_data_model",
"required" : false,
"schema" : {
"default" : false,
"oneOf" : [ {
"type" : "boolean"
}, {
"type" : "null"
} ]
}
} ],
"responses" : {
"2XX" : {
@@ -25755,7 +25831,14 @@
"operationId" : "get-api-database-virtual-db-datasets-schema",
"summary" : "GET /api/database/{virtual-db}/datasets/{schema}",
"description" : "Returns a list of Tables for the datasets virtual database.",
"parameters" : [ ],
"parameters" : [ {
"in" : "path",
"name" : "schema",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"2XX" : {
"description" : "Successful response"
@@ -25795,7 +25878,14 @@
"operationId" : "get-api-database-virtual-db-schema-schema",
"summary" : "GET /api/database/{virtual-db}/schema/{schema}",
"description" : "Returns a list of Tables for the saved questions virtual database.",
"parameters" : [ ],
"parameters" : [ {
"in" : "path",
"name" : "schema",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"2XX" : {
"description" : "Successful response"
@@ -28166,6 +28256,17 @@
"type" : "string",
"minLength" : 1
}
}, {
"in" : "query",
"name" : "locale",
"required" : false,
"schema" : {
"oneOf" : [ {
"type" : "string"
}, {
"type" : "null"
} ]
}
} ],
"responses" : {
"2XX" : {
@@ -28709,6 +28810,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/custom-viz-plugin" ]
}
},
@@ -28919,6 +29030,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/custom-viz-plugin" ]
}
},
@@ -29094,6 +29215,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/custom-viz-plugin" ]
}
},
@@ -29390,6 +29521,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/data-complexity-score" ]
}
},
@@ -30570,6 +30711,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/library" ]
},
"get" : {
@@ -30605,6 +30756,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/library" ]
}
},
@@ -31929,6 +32090,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/permission_debug" ]
}
},
@@ -32564,6 +32735,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/remote-sync" ]
}
},
@@ -33397,7 +33578,15 @@
"operationId" : "put-api-ee-scim-v2-Groups-id",
"summary" : "PUT /api/ee/scim/v2/Groups/{id}",
"description" : "Update a group.",
"parameters" : [ ],
"parameters" : [ {
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string",
"minLength" : 1
}
} ],
"responses" : {
"2XX" : {
"description" : "Successful response"
@@ -33680,7 +33869,15 @@
"operationId" : "put-api-ee-scim-v2-Users-id",
"summary" : "PUT /api/ee/scim/v2/Users/{id}",
"description" : "Update a user.",
"parameters" : [ ],
"parameters" : [ {
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string",
"minLength" : 1
}
} ],
"responses" : {
"2XX" : {
"description" : "Successful response"
@@ -34854,6 +35051,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/ee/tenant" ]
}
},
@@ -38162,6 +38369,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/geojson" ]
}
},
@@ -38190,6 +38407,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/geojson" ]
}
},
@@ -38384,7 +38611,16 @@
"operationId" : "get-api-health-inspector",
"summary" : "GET /api/health-inspector",
"description" : "Get a list of recent health check runs.",
"parameters" : [ ],
"parameters" : [ {
"in" : "query",
"name" : "limit",
"required" : false,
"schema" : {
"type" : "integer",
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
} ],
"responses" : {
"2XX" : {
"description" : "Successful response",
@@ -45806,6 +46042,13 @@
"type" : "string",
"minLength" : 1
}
}, {
"in" : "query",
"name" : "value",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"2XX" : {
@@ -49214,6 +49457,16 @@
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : { }
}
}
}
},
"tags" : [ "/api/session" ]
}
},

View File

@@ -6,6 +6,13 @@ title: API changelog
## Metabase 0.64.0
- `POST /api/slack/bug-report` now requires bug reporting to be enabled (`MB_BUG_REPORTING_ENABLED`).
`diagnosticInfo.reporter` is now a boolean: `true` attributes the report to the authenticated user, `false` (or
omitting it) submits the report anonymously. The previous `{ "name": ..., "email": ... }` object is still accepted
and treated as `true`; the name and email in it are ignored. The request body is validated against a fixed set of
keys; undeclared keys are dropped. This change is
backported to 0.58 and later; see the per-version entries below.
- Self-hosted environments must now explicitly enable transforms before beginning to use them via the API. Admins can enable transforms in Data Studio or by setting the MB_TRANSFORMS_ENABLED environment variable to true.
- The endpoints that fetch prefill values for action forms have been converted from GET to POST so that parameter values are sent in the JSON request body instead of the URL query string. `parameters` is now a JSON object in the request body rather than a JSON-encoded query-string parameter:
@@ -18,12 +25,42 @@ title: API changelog
The GET variants have been removed without a deprecation period.
## Metabase 0.63.15
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.62.18
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.61.20
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.61.0
- `POST /api/metabot/describe/card` and `POST /api/metabot/describe/dashboard/:id` have been removed. These endpoints
provided LLM-powered autodescription of cards and dashboards using the legacy OpenAI client. This functionality has
been superseded by the Metabot agent.
## Metabase 0.60.26
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.59.30
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.58.32
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
0.64.0 entry.
## Metabase 0.57.0
- MBQL queries (in Cards and elsewhere) are now serialized as MBQL 5 as opposed to MBQL 4 (aka legacy MBQL) in the

View File

@@ -30,6 +30,9 @@ title: Driver interface changelog
form for the pivot-grouping bitmask. The default emits `GROUPING(exprs...)` (the Postgres/Oracle/Snowflake
multi-arg extension); drivers whose SQL dialect uses a different function or shape override this method.
- `:native-pivot-tables` is now enabled for `:hive-like` drivers.
Hive-family dialects synthesise the pivot-grouping bitmask from single-arg `GROUPING(x)` calls.
- Index Manager: drivers can now read and create table indexes, in the broad sense (secondary indexes, sort keys,
distribution keys, clustering, etc.). New driver feature flags:
@@ -58,11 +61,14 @@ title: Driver interface changelog
- `metabase.driver/compile-create-index` `[driver schema table structured]` -- compiles a `:standalone` index into
the DDL statement(s) that create it.
- `metabase.driver-api.core/aggregation-name` is now deprecated, as it operates on legacy MBQL; use
`metabase.driver-api.core/mbql-5-aggregation-name` going forward.
- `metabase.driver.sql-jdbc.sync.interface/db-tables` is now a multimethod for retrieving JDBC metadata
tables. SQL JDBC drivers can override this method to customize which database objects are discovered during sync.
- The method signature for `metabase.driver/substitute-native-parameters-in-stage-method`, introduced in 63, has
changed; please update your implementations accordingly.
- `metabase.driver-api.core/aggregation-name` is now deprecated, as it operates on legacy MBQL; use
`metabase.driver-api.core/mbql-5-aggregation-name` going forward.
- The method signature for `metabase.driver/substitute-native-parameters-in-stage-method`, introduced in 63, has
changed; please update your implementations accordingly.
## Metabase 0.63.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -122,6 +122,7 @@ type MetabaseColorKey =
| "background_surface-warning-strong"
| "border-brand"
| "border-filter"
| "border-highlight"
| "border-neutral"
| "border-neutral-strong"
| "border-neutral-strongest"

View File

@@ -133,6 +133,7 @@ type MetabaseEmbeddingColorKeyV2 = Exclude<
| "background_surface-warning-strong"
| "border-brand"
| "border-filter"
| "border-highlight"
| "border-neutral"
| "border-neutral-strong"
| "border-neutral-strongest"

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,10 @@ const {
} = require("@esbuild-plugins/node-modules-polyfill");
const cypressSplit = require("cypress-split");
const {
sideEffectFreeModulesPlugin,
} = require("../../frontend/build/shared/esbuild/side-effect-free-modules-plugin");
const isInstrumented = process.env.INSTRUMENT_COVERAGE === "true";
// The Cypress config process runs with cwd = this file's directory
// (e2e/support), so @cypress/code-coverage writes .nyc_output/out.json here.
@@ -238,7 +242,11 @@ const defaultConfig = {
loader: {
".svg": "text",
},
plugins: [NodeModulesPolyfillPlugin(), assetsResolverPlugin],
plugins: [
NodeModulesPolyfillPlugin(),
assetsResolverPlugin,
sideEffectFreeModulesPlugin,
],
sourcemap: "inline",
}),
);

View File

@@ -3,7 +3,7 @@ export const cacheStrategySidesheet = () =>
export const cacheStrategySelect = () =>
cy
.findByRole("form", { name: "Select the cache invalidation policy" })
.findByRole("form", { name: "Cache invalidation policy" })
.findByTestId("cache-strategy-select");
/** Open the strategy dropdown and pick an option by its title (e.g. /Duration/).
@@ -13,3 +13,8 @@ export const selectCacheStrategy = (name: RegExp) => {
cacheStrategySelect().click();
cy.findByRole("option", { name }).click();
};
/** Selecting the Duration strategy leaves the duration field empty, and the
* form cannot be saved until it is filled. */
export const fillCacheDuration = (value: number) =>
cy.findByRole("spinbutton", { name: /Cache duration/ }).type(String(value));

View File

@@ -589,7 +589,8 @@ function getSegmentEditorNameInput() {
}
function getSegmentEditorDescriptionInput() {
return getSegmentEditor().findByLabelText("Give it a description");
getSegmentEditor().findByLabelText("Give it a description").click();
return getSegmentEditor().findByPlaceholderText("Only if it really needs it");
}
function getSegmentEditorFilterPlaceholder() {
@@ -681,7 +682,8 @@ function getMeasureEditorNameInput() {
}
function getMeasureEditorDescriptionInput() {
return getMeasureEditor().findByLabelText("Give it a description");
getMeasureEditor().findByLabelText("Give it a description").click();
return getMeasureEditor().findByPlaceholderText("Only if it really needs it");
}
function getMeasureEditorAggregationPlaceholder() {
@@ -805,7 +807,7 @@ function verifyTablePreview({
});
if (description != null) {
cy.findByTestId("header-cell").realHover();
hoverHeaderCell();
}
});
@@ -814,6 +816,26 @@ function verifyTablePreview({
}
}
// Open the preview's column-description hovercard. The hovercard opens on a
// delayed `openDelay` timer, and a single one-shot `realHover()` can miss it:
// after `cy.wait("@dataset")` the table re-renders on a microtask (fetch
// resolution) and Chrome v133+ headless hit-tests CDP mouse events
// differently, either of which drops the hover and cancels the open timer,
// leaving the hovercard permanently absent for that attempt. Re-query the cell
// for each dispatch so the events land on the post-render DOM node, and fire
// both `mouseover` (bubbles → React 18 synthetic onMouseEnter for any wrapper)
// and `mouseenter` (for native useEventListener handlers Mantine attaches
// directly to the cell).
function hoverHeaderCell() {
const headerCell = () =>
cy
.findByTestId("header-cell")
.findByTestId("cell-data")
.should("be.visible");
headerCell().trigger("mouseenter", { force: true });
headerCell().trigger("mouseover", { force: true });
}
function verifyObjectDetailPreview({
rowNumber,
row,

View File

@@ -16,8 +16,14 @@ export const DependencyGraph = {
};
export const DependencyDiagnostics = {
visitBrokenDependencies: () =>
cy.visit("/monitor/dependency-diagnostics/broken"),
visitBrokenDependencies: () => {
cy.intercept("GET", "/api/ee/dependencies/graph/breaking*").as(
"breakingEntities",
);
cy.visit("/monitor/dependency-diagnostics/broken");
cy.wait("@breakingEntities");
DependencyDiagnostics.list().should("be.visible");
},
visitUnreferencedEntities: () => {
cy.intercept("GET", "/api/ee/dependencies/graph/unreferenced*").as(
"unreferencedEntities",
@@ -123,7 +129,7 @@ export function waitForBreakingDependencies(
return cy
.request<ListBreakingGraphNodesResponse>(
"GET",
"/api/ee/dependencies/graph/breaking",
"/api/ee/dependencies/graph/breaking?include-personal-collections=true",
)
.then((response) => {
if (filter(response.body.data)) {

View File

@@ -8,12 +8,11 @@ import {
NORMAL_USER_ID,
} from "e2e/support/cypress_sample_instance_data";
const { sandboxed, normal, admin, nodata, nocollection } = USERS;
const { normal, admin, nocollection } = USERS;
const { ALL_USERS_GROUP, DATA_GROUP } = USER_GROUPS;
const TOTAL_USERS = Object.entries(USERS).length;
const TOTAL_GROUPS = Object.entries(USER_GROUPS).length;
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
const { COLLECTION_GROUP } = USER_GROUPS;
const { ORDERS_ID } = SAMPLE_DATABASE;
const TEST_USER = {
first_name: "Testy",
@@ -26,8 +25,6 @@ const adminUserName = H.getFullName(admin);
const noCollectionUserName = H.getFullName(nocollection);
const normalUserName = H.getFullName(normal);
const totalUsers = Object.keys(USERS).length;
describe("scenarios > admin > people", () => {
beforeEach(() => {
cy.intercept("GET", "/api/permissions/group").as("getGroups");
@@ -892,77 +889,6 @@ describe("scenarios > admin > people > group managers", () => {
});
});
// TODO: remove skip when this issue gets fixed. Initial attempt: https://github.com/metabase/metabase/pull/23825,
// reverted in https://github.com/metabase/metabase/pull/24760 to fix a security vulnerability.
describe("issue 23689", { tags: "@skip" }, () => {
function findUserByFullName(user) {
const { first_name, last_name } = user;
return cy.findByText(`${first_name} ${last_name}`);
}
function visitGroupPermissionsPage(groupId) {
cy.visit(`/admin/people/groups/${groupId}`);
cy.wait("@membership");
}
beforeEach(() => {
cy.intercept("GET", "/api/permissions/membership").as("membership");
H.restore();
cy.signInAsAdmin();
H.activateToken("pro-self-hosted");
visitGroupPermissionsPage(COLLECTION_GROUP);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("3 members");
findUserByFullName(normal);
findUserByFullName(nodata);
// Make sandboxed user a group manager
findUserByFullName(sandboxed)
.closest("tr")
.findByTestId("user-type-toggle")
.click({ force: true });
// Sanity check instead of waiting for the PUT request
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Manager");
cy.sandboxTable({
table_id: ORDERS_ID,
attribute_remappings: {
attr_uid: ["dimension", ["field", ORDERS.USER_ID, null]],
},
});
cy.signOut();
cy.signInAsSandboxedUser();
});
it("sandboxed group manager should see all other members (metabase#23689)", () => {
visitGroupPermissionsPage(COLLECTION_GROUP);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("3 members");
findUserByFullName(sandboxed);
findUserByFullName(normal);
findUserByFullName(nodata);
cy.visit("/admin/people");
cy.wait("@membership");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(`${totalUsers} people found`);
findUserByFullName(sandboxed);
findUserByFullName(normal);
findUserByFullName(nodata);
findUserByFullName(nocollection);
});
});
function showUserOptions(full_name) {
cy.findByText(full_name)
.closest("tr")

View File

@@ -256,21 +256,33 @@ describe("issue 45890", () => {
cy.visit("/admin/performance/databases");
H.main()
.findByLabelText(/Edit policy for database 'Sample Database'/)
.findByText("No caching")
.click();
// The strategy picker is a dropdown; its options render in a portal.
H.main().findByTestId("cache-strategy-select").click();
// The strategy form opens in a side panel; the picker options render in a portal.
cy.findByTestId("cache-policy-panel")
.findByTestId("cache-strategy-select")
.click();
cy.findByRole("option", { name: /Schedule/ }).click();
H.main().button("Save changes").click();
cy.intercept("PUT", "/api/cache").as("putCacheConfig");
cy.findByTestId("cache-policy-panel").button("Save").click();
cy.wait("@putCacheConfig");
});
it("should correctly reset caching schedule form when discarding changes", () => {
H.main().findByLabelText("Frequency").click();
cy.findByTestId("cache-policy-panel").findByLabelText("Frequency").click();
H.popover().findByText("weekly").click();
H.main().button("Discard changes").click();
H.main().findByLabelText("Frequency").should("have.value", "hourly");
cy.log("Cancel the dirty form and confirm discarding the changes");
cy.findByTestId("cache-policy-panel").button("Cancel").click();
cy.findByTestId("confirm-modal").button("Discard").click();
cy.log("Reopening the form shows the saved schedule, not the draft");
H.main()
.findByLabelText(/Edit policy for database 'Sample Database'/)
.click();
cy.findByTestId("cache-policy-panel")
.findByLabelText("Frequency")
.should("have.value", "hourly");
});
});

View File

@@ -9,15 +9,8 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import type { TableId } from "metabase-types/api";
const { H } = cy;
const {
ORDERS,
ORDERS_ID,
PEOPLE_ID,
PRODUCTS,
REVIEWS,
REVIEWS_ID,
PRODUCTS_ID,
} = SAMPLE_DATABASE;
const { ORDERS, ORDERS_ID, PEOPLE_ID, REVIEWS, REVIEWS_ID, PRODUCTS_ID } =
SAMPLE_DATABASE;
const { ALL_USERS_GROUP } = USER_GROUPS;
const { FieldSection, PreviewSection, TablePicker, TableSection } = H.DataModel;
@@ -423,54 +416,6 @@ describe("scenarios > admin > datamodel", () => {
H.queryBuilderHeader().findByText("View-only").should("be.visible");
});
it("question with joins (metabase#15947-2)", { tags: "@skip" }, () => {
H.createQuestion({
name: "15947",
query: {
"source-table": ORDERS_ID,
joins: [
{
fields: "all",
"source-table": PRODUCTS_ID,
condition: [
"=",
["field", ORDERS.PRODUCT_ID, null],
["field", PRODUCTS.ID, { "join-alias": "Products" }],
],
alias: "Products",
},
],
filter: [
"and",
["=", ["field", ORDERS.QUANTITY, null], 1],
[
">",
["field", PRODUCTS.RATING, { "join-alias": "Products" }],
3,
],
],
aggregation: [
["sum", ["field", ORDERS.TOTAL, null]],
[
"sum",
["field", PRODUCTS.RATING, { "join-alias": "Products" }],
],
],
breakout: [
["field", ORDERS.CREATED_AT, { "temporal-unit": "year" }],
["field", PRODUCTS.CATEGORY, { "join-alias": "Products" }],
],
},
}).then(({ body: { id: QUESTION_ID } }) => {
turnTableVisibilityOff(PRODUCTS_ID);
cy.visit(`/question/${QUESTION_ID}/notebook`);
cy.findByText("Products");
cy.findByText("Quantity is equal to 1");
cy.findByText("Rating is greater than 3");
H.queryBuilderHeader().findByText("View-only").should("be.visible");
});
});
});
});
});

View File

@@ -191,6 +191,8 @@ describe("issue 15542", () => {
describe("issue 52411", { tags: "@external" }, () => {
beforeEach(() => {
cy.intercept("GET", "/api/database/*/schema/*").as("getSchema");
cy.intercept("GET", "/api/database").as("getDatabases");
H.restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: "multi_schema" });
cy.signInAsAdmin();
@@ -199,8 +201,10 @@ describe("issue 52411", { tags: "@external" }, () => {
it("should be able to select a table in a database with multiple schemas on segments list page when there are multiple databases and there is a saved question (metabase#52411)", () => {
cy.visit("/admin/datamodel/segments");
cy.wait(["@getDatabases", "@getSchema"]);
cy.findByTestId("segment-list-table").findByText("Filter by table").click();
H.popover().within(() => {
cy.icon("chevronleft").click(); // go back
cy.findByText("Writable Postgres12").click();
cy.findByText("Wild").click();
cy.findByText("Birds").click();

View File

@@ -10,7 +10,7 @@ const preemptiveCachingSwitch = () =>
const saveCacheStrategyForm = () => {
cy.intercept("PUT", "/api/cache").as("putCacheConfig");
cy.findByRole("form", { name: "Select the cache invalidation policy" })
cy.findByRole("form", { name: "Cache invalidation policy" })
.button(/Save/)
.click();
cy.wait("@putCacheConfig");
@@ -57,7 +57,7 @@ const cancelConfirmationModal = () =>
* frontend/.../Schedule/{cron,Schedule}.unit.spec.{ts,tsx}.
* - Strategy form input binding (typing into Min Duration / Multiplier),
* strategy-switch flows (Adaptive <-> Duration <-> No caching),
* strategy -> launcher-label mapping, policy-options counts,
* strategy -> row-label mapping, policy-options counts,
* preemptive caching switch availability for root/database targets:
* frontend/.../StrategyEditorForDatabases.unit.spec.tsx and
* enterprise/.../caching/components/StrategyEditorForDatabases.unit.spec.tsx.
@@ -72,6 +72,10 @@ describe("scenarios > admin > performance > caching", () => {
it("saves the default-policy strategy and reflects the saved state", () => {
cy.visit("/admin/performance");
H.selectCacheStrategy(/Adaptive/);
cy.findByRole("spinbutton", { name: /minimum query duration/i }).type(
"1",
);
cy.findByRole("spinbutton", { name: /multiplier/i }).type("10");
saveCacheStrategyForm();
H.cacheStrategySelect().should("have.value", "Adaptive");
});
@@ -95,6 +99,7 @@ describe("scenarios > admin > performance > caching", () => {
cy.log("Set Sample Database to Duration and save");
H.selectCacheStrategy(/Duration/);
H.fillCacheDuration(24);
saveCacheStrategyForm();
cy.findByTestId("admin-layout-content").findByLabelText(
/Edit.*Sample Database.*currently.*Duration/,
@@ -109,7 +114,7 @@ describe("scenarios > admin > performance > caching", () => {
cy.button(/Clear cache for this database/).click();
cy.log("Confirm in the dialog");
cy.findByRole("dialog")
cy.findByTestId("confirm-modal")
.button(/Clear cache/)
.click();
cy.wait("@invalidateCacheForSampleDatabase");
@@ -122,6 +127,8 @@ describe("scenarios > admin > performance > caching", () => {
H.visitQuestion(ORDERS_QUESTION_ID);
openSidebarCacheStrategyForm("question");
H.selectCacheStrategy(/Duration/);
// 24 keeps the "Duration: 24h" admin-tab assertion below true
H.fillCacheDuration(24);
preemptiveCachingSwitch().within(() => {
cy.findByRole("switch").should("not.be.checked");
cy.findByRole("switch").parent("label").click();
@@ -216,6 +223,10 @@ describe("scenarios > admin > performance > caching", () => {
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
openSidebarCacheStrategyForm("question");
H.selectCacheStrategy(/Adaptive/);
cy.findByRole("spinbutton", { name: /minimum query duration/i }).type(
"1",
);
cy.findByRole("spinbutton", { name: /multiplier/i }).type("10");
saveCacheStrategyForm();
cy.log("Both entries are visible on the admin tab");

View File

@@ -16,40 +16,6 @@ describe("personal collections", () => {
cy.signInAsAdmin();
});
/**
* This reproduction is here only as a placeholder until a proper backend tests is added.
*
* Not entirely sure how this issue will be resolved!
* Thus, test might not work as expected by that point.
*
* For example:
* 1. FE might decide not to fetch the full collection tree on the home page or
* 2. BE might alter this endpoint
*
* TODO:
* - When the solution for this problem is ready, either adjust the test or completely remove it!
*/
it(
"shouldn't get API response containing all other personal collections when visiting the home page (metabase#24330)",
{ tags: "@skip" },
() => {
cy.intercept("GET", "/api/collection/tree*").as("getCollections");
cy.visit("/");
cy.wait("@getCollections").then(({ response: { body } }) => {
const personalCollections = body.filter(({ personal_owner_id }) => {
return personal_owner_id !== null;
});
// Admin can only see their own personal collection, so this list should return only that
// Loading all other users' personal collections can lead to performance issues!
expect(personalCollections).to.have.lengthOf(1);
});
},
);
it("should see a link to other users' personal collections only if there are other users", () => {
cy.intercept("GET", "/api/session/properties", (req) => {
req.continue((res) => {

View File

@@ -164,32 +164,6 @@ describe("scenarios > question > custom column", () => {
H.getNotebookStep("summarize").findByText("UserLAT").should("be.visible");
});
// flaky test (#19454)
it(
"should show info popovers when hovering over custom column dimensions in the summarize sidebar",
{ tags: "@skip" },
() => {
H.openOrdersTable({ mode: "notebook" });
cy.findByLabelText("Custom column").click();
H.enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
cy.button("Done").click();
H.visualize();
H.summarize();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Group by")
.parent()
.findByText("Math")
.trigger("mouseenter");
H.popover().contains("Math");
H.popover().contains("No description");
},
);
it("can create a custom column with an existing column name", () => {
const customFormulas = [
{

View File

@@ -6,42 +6,6 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
const { PRODUCTS, PRODUCTS_ID, ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
describe("issue 12445", { tags: ["@external", "@skip"] }, () => {
const CC_NAME = "Abbr";
beforeEach(() => {
H.restore("mysql-8");
cy.signInAsAdmin();
});
it("should correctly apply substring for a custom column (metabase#12445)", () => {
H.withDatabase(2, ({ PEOPLE, PEOPLE_ID }) => {
cy.log("Create a question with `Source` column and abbreviated CC");
H.createQuestion(
{
name: "12445",
query: {
"source-table": PEOPLE_ID,
breakout: [["expression", CC_NAME]],
expressions: {
[CC_NAME]: [
"substring",
["field", PEOPLE.SOURCE, null],
1,
4, // we want 4 letter abbreviation
],
},
},
database: 2,
},
{ visitQuestion: true },
);
cy.findByText(CC_NAME);
cy.findByText("Goog");
});
});
});
describe("issue 13751", { tags: "@external" }, () => {
const CC_NAME = "C-States";
const PG_DB_NAME = "QA Postgres12";
@@ -81,42 +45,6 @@ describe("issue 13751", { tags: "@external" }, () => {
});
});
describe(
"postgres > question > custom columns",
{ tags: ["@external", "@skip"] },
() => {
const PG_DB_NAME = "QA Postgres12";
// Ironically, both Prettier and Cypress remove escape characters from our code as well
// We're testing for the literal sting `(?<=\/\/)[^\/]*`, but we need to escape the escape characters to make it work
const ESCAPED_REGEX = "(?<=\\/\\/)[^\\/]*";
beforeEach(() => {
H.restore("postgres-12");
cy.signInAsAdmin();
H.startNewQuestion();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(PG_DB_NAME).should("be.visible").click();
cy.findByTextEnsureVisible("People").click();
});
it("should not remove regex escape characters (metabase#14517)", () => {
cy.log("Create custom column using `regexExtract()`");
cy.findByLabelText("Custom Column").click();
H.popover().within(() => {
cy.get("[contenteditable='true']")
.type(`regexExtract([State], "${ESCAPED_REGEX}")`)
.blur();
// It removes escaped characters already on blur
cy.log("Reported failing on v0.36.4");
cy.contains(ESCAPED_REGEX);
});
});
},
);
describe("issue 14843", () => {
const { PEOPLE, PEOPLE_ID } = SAMPLE_DATABASE;
const CC_NAME = "City Length";
@@ -486,7 +414,10 @@ describe("issue 20229", () => {
}
function unselectColumn(column) {
cy.findByText(column).siblings().find(".Icon-check").click({ force: true });
cy.findByText(column)
.siblings()
.find('input[type="checkbox"]')
.click({ force: true });
}
beforeEach(() => {
@@ -671,78 +602,6 @@ describe("issue 24922", () => {
});
});
describe("issue 25189", { tags: "@skip" }, () => {
const ccTable = "Custom Created";
const ccFunction = "Custom Total";
const questionDetails = {
name: "25189",
query: {
"source-table": ORDERS_ID,
limit: 5,
expressions: {
[ccTable]: ["field", ORDERS.CREATED_AT, null],
[ccFunction]: [
"case",
[[[">", ["field", ORDERS.TOTAL, null], 100], "Yay"]],
{
default: "Nay",
},
],
},
},
};
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
H.restore();
cy.signInAsAdmin();
H.createQuestion(questionDetails).then(
({ body: { id: baseQuestionId } }) => {
H.createQuestion(
{
name: "Nested 25189",
query: { "source-table": `card__${baseQuestionId}` },
},
{ visitQuestion: true },
);
},
);
});
it("custom column referencing only a single column should not be dropped in a nested question (metabase#25189)", () => {
// 1. Column should not be dropped
cy.findAllByTestId("header-cell")
.should("contain", ccFunction)
.and("contain", ccTable);
// 2. We shouldn't see duplication in the bulk filter modal
H.filter();
H.modal().within(() => {
// Implicit assertion - will fail if more than one element is found
cy.findByText(ccFunction);
cy.findByText(ccTable);
cy.findByText("Today").click();
cy.button("Apply Filters").click();
});
cy.wait("@dataset");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("No results");
// 3. We shouldn't see duplication in the breakout fields
H.summarize();
cy.findByTestId("sidebar-content").within(() => {
// Another implicit assertion
cy.findByText(ccFunction);
cy.findByText(ccTable);
});
});
});
["postgres" /*, "mysql" */].forEach((dialect) => {
describe(`issue 27745 (${dialect})`, { tags: "@external" }, () => {
const tableName = "colors27745";
@@ -790,140 +649,6 @@ describe("issue 25189", { tags: "@skip" }, () => {
});
});
});
// broken. see https://github.com/metabase/metabase/issues/55673
describe("issue 42949", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("should correctly show available shortcuts for date and number columns (metabase#42949)", () => {
H.createNativeQuestion(
{
native: {
query: `
SELECT DATE '2024-05-21' AS created_at, null as v
UNION ALL SELECT DATE '2024-05-20', 1
UNION ALL SELECT DATE '2024-05-19', 2
ORDER BY created_at
`,
},
},
{ visitQuestion: true },
);
cy.findByTestId("qb-header").findByText("Explore results").click();
cy.log("Verify header drills - CREATED_AT");
H.tableHeaderClick("CREATED_AT");
H.popover().findByText("Extract day, month…").should("be.visible");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
cy.log("Verify header drills - V");
H.tableHeaderClick("V");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
cy.log("Verify plus button - extract column");
cy.button("Add column").click();
H.popover().findByText("Extract part of column").click();
H.popover().findByText("CREATED_AT").click();
H.popover().within(() => {
cy.findByText("Day of month").should("be.visible");
cy.findByText("Day of week").should("be.visible");
cy.findByText("Month of year").should("be.visible");
cy.findByText("Quarter of year").should("be.visible");
cy.findByText("Year").should("be.visible").click();
});
cy.findAllByTestId("header-cell").eq(2).should("have.text", "Year");
cy.log("Verify plus button - combine columns");
cy.button("Add column").click();
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
// eslint-disable-next-line metabase/no-unsafe-element-filtering
H.popover()
.last()
.within(() => {
cy.findByText("CREATED_AT").should("be.visible");
cy.findByText("V").should("be.visible");
cy.findByText("Year").should("be.visible").click();
});
H.popover().button("Done").click();
cy.findAllByTestId("header-cell")
.eq(3)
.should("have.text", "Combined Year, V");
cy.findAllByTestId("cell-data").eq(6).should("have.text", "2,024");
cy.findAllByTestId("cell-data").eq(7).should("have.text", "2024 2");
cy.findAllByTestId("cell-data").eq(10).should("have.text", "2,024");
cy.findAllByTestId("cell-data").eq(11).should("have.text", "2024 1");
cy.findAllByTestId("cell-data").eq(13).should("have.text", "2,024");
cy.findAllByTestId("cell-data").eq(14).should("have.text", "2024 ");
});
it("should correctly show available shortcuts for a number column (metabase#42949)", () => {
H.createNativeQuestion(
{
native: {
query: "select 1 as n",
},
},
{ visitQuestion: true },
);
cy.findByTestId("qb-header").findByText("Explore results").click();
cy.findByLabelText("Switch to data").click();
cy.log("Verify header drills");
H.tableHeaderClick("N");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("not.exist");
cy.realPress("Escape");
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
cy.log("Verify plus button");
cy.button("Add column").click();
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
// eslint-disable-next-line metabase/no-unsafe-element-filtering
H.popover().last().findByText("N").should("be.visible");
});
it("should correctly show available shortcuts for a string column (metabase#42949)", () => {
H.createNativeQuestion(
{
native: {
query: "select 'abc'",
},
},
{ visitQuestion: true },
);
cy.findByTestId("qb-header").findByText("Explore results").click();
cy.findByLabelText("Switch to data").click();
cy.log("Verify header drills");
H.tableHeaderClick("'abc'");
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").should("be.visible");
cy.realPress("Escape");
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
cy.log("Verify plus button");
cy.button("Add column").click();
H.popover().findByText("Extract part of column").should("not.exist");
H.popover().findByText("Combine columns").click();
H.popover().findAllByTestId("column-input").eq(0).click();
// eslint-disable-next-line metabase/no-unsafe-element-filtering
H.popover().last().findByText("'abc'").should("be.visible");
});
});
describe("issue 49342", () => {
beforeEach(() => {
@@ -1035,23 +760,6 @@ describe("issue 49882", () => {
},
);
// TODO: we no longer have wrapped lines (for now)
it(
"should allow moving cursor between wrapped lines with arrow up and arrow down keys (metabase#49882-3)",
{ tags: "@skip" },
() => {
H.enterCustomColumnDetails({
formula:
'case([Tax] > 1, case([Total] > 200, [Total], "Nothing"), [Tax]){leftarrow}{leftarrow}{uparrow}x{downarrow}y',
});
H.CustomExpressionEditor.value().should(
"equal",
'case([Tax] > 1, xcase([Total] > 200, [Total], "Nothing"), [Tax]y)',
);
},
);
it("should update currently selected suggestion when suggestions list is updated (metabase#49882-4)", () => {
const selectProductRating =
"{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}";

View File

@@ -473,78 +473,6 @@ describe("issue 55687", () => {
});
});
describe("issue 58371", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
cy.request("PUT", `/api/field/${ORDERS.PRODUCT_ID}`, {
display_name: null,
});
const baseQuestion = {
name: "Base Question",
query: {
"source-table": PRODUCTS_ID,
aggregation: [
[
"aggregation-options",
["count-where", ["=", ["field", PRODUCTS.TITLE, null], "OK"]],
{ "display-name": "Aggregation with Dash-in-name" },
],
],
breakout: [["field", PRODUCTS.ID, null]],
},
};
H.createQuestion(baseQuestion, { wrapId: true }).then((questionId) => {
const questionDetails = {
query: {
"source-table": ORDERS_ID,
joins: [
{
fields: "all",
"source-table": `card__${questionId}`,
alias: "Other Question",
condition: [
"=",
["field", ORDERS.PRODUCT_ID, null],
["field", PRODUCTS.ID, { "join-alias": "Other Question" }],
],
},
],
expressions: {
Foo: [
"+",
0,
[
"field",
"count_where",
{
"base-type": "type/Float",
"join-alias": "Other Question",
},
],
],
},
},
};
H.createQuestion(questionDetails, { visitQuestion: true });
});
H.openNotebook();
});
it("should allow using names with a dash in them from joined tables (metabase#58371)", () => {
H.getNotebookStep("expression").findByText("Foo").click();
H.CustomExpressionEditor.value().should(
"eq",
"0 + [Other Question → Aggregation with Dash-in-name]",
);
});
});
describe("Issue 58230", () => {
beforeEach(() => {
H.restore();
@@ -585,27 +513,6 @@ describe("issue 57674", () => {
H.openOrdersTable({ mode: "notebook" });
});
// TODO: re-enable this test once we have a fix for metabase#61264
it(
"should show an error when using a case or if expression with mismatched types (metabase#57674)",
{ tags: "@skip" },
() => {
H.getNotebookStep("data").button("Custom column").click();
H.CustomExpressionEditor.clear();
H.popover().findByText("Types are incompatible.").should("not.exist");
H.CustomExpressionEditor.type(
'case([Total] > 100, [Created At], "foo")',
{
allowFastSet: true,
},
).blur();
H.popover().findByText("Types are incompatible.").should("be.visible");
},
);
it("should not show an error when using a case or if expression with compatible types (metabase#57674)", () => {
H.getNotebookStep("data").button("Custom column").click();

View File

@@ -431,14 +431,6 @@ describe("issue 17160", () => {
});
}
function visitPublicSourceDashboard() {
cy.get("@sourceDashboardUUID").then((uuid) => {
cy.visit(`/public/dashboard/${uuid}`);
cy.findByTextEnsureVisible("Enormous Wool Car");
});
}
beforeEach(() => {
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
@@ -478,37 +470,6 @@ describe("issue 17160", () => {
assertMultipleValuesFilterState();
});
it(
"should pass multiple filter values to public questions and dashboards (metabase#17160-2)",
{ tags: "@skip" },
() => {
// FIXME: setup public dashboards
setup();
// 1. Check click behavior connected to a public question
visitPublicSourceDashboard();
cy.findAllByText("click-behavior-question-label").eq(0).click();
cy.url().should("include", "/public/question");
assertMultipleValuesFilterState();
// 2. Check click behavior connected to a publicdashboard
visitPublicSourceDashboard();
cy.findAllByText("click-behavior-dashboard-label").eq(0).click();
cy.url().should("include", "/public/dashboard");
cy.location("search").should("eq", "?category=Doohickey&category=Gadget");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(TARGET_DASHBOARD_NAME);
assertMultipleValuesFilterState();
},
);
});
describe("issue 18454", () => {

View File

@@ -302,49 +302,6 @@ describe("scenarios > dashboard > dashboard drill", () => {
});
});
// This was flaking. Example: https://dashboard.cypress.io/projects/a394u1/runs/2109/test-results/91a15b66-4b80-40bf-b569-de28abe21f42
it("should handle cross-filter on a table", { tags: "@skip" }, () => {
createDashboardWithQuestion({}, (dashboardId) =>
H.visitDashboard(dashboardId),
);
cy.icon("pencil").click();
H.showDashboardCardActions();
cy.findByTestId("dashboardcard-actions-panel").within(() => {
cy.icon("click").click();
});
// configure clicks on "MY_NUMBER to update the param
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("On-click behavior for each column")
.parent()
.parent()
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("MY_NUMBER").click());
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Update a dashboard filter").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Pick one or more filters to update")
.parent()
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("My Param").click());
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
H.selectDropdown().within(() => cy.findByText("MY_STRING").click());
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// click on table value
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("111").click();
// check that param was set to "foo"
cy.location("search").should("eq", "?my_param=foo");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("My Param")
.parent()
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
.within(() => cy.findByText("foo"));
});
describe("should pass multiple filters for numeric column on drill-through (metabase#13062)", () => {
const questionDetails = {
name: "13062Q",

View File

@@ -1934,102 +1934,6 @@ describe("issue 56716", () => {
H.getDashboardCard().findByText("200 rows").should("be.visible");
});
});
describe("Issue 46337", () => {
const MODEL_NAME = "Model 46337";
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.createQuestion({
type: "model",
name: MODEL_NAME,
query: {
"source-table": ORDERS_ID,
fields: [
[
"field",
ORDERS.ID,
{
"base-type": "type/BigInteger",
},
],
[
"field",
ORDERS.TAX,
{
"base-type": "type/Float",
},
],
[
"field",
ORDERS.TOTAL,
{
"base-type": "type/Float",
},
],
[
"field",
ORDERS.DISCOUNT,
{
"base-type": "type/Float",
},
],
[
"field",
ORDERS.QUANTITY,
{
"base-type": "type/Integer",
},
],
[
"field",
ORDERS.CREATED_AT,
{
"base-type": "type/DateTime",
},
],
[
"field",
ORDERS.PRODUCT_ID,
{
"base-type": "type/Integer",
},
],
],
joins: [
{
fields: "all",
alias: "Products",
"source-table": PEOPLE_ID,
strategy: "left-join",
condition: [
"=",
["field", ORDERS.USER_ID, {}],
["field", PEOPLE.ID, { "join-alias": "Products" }],
],
},
],
},
}).then(({ body: model }) => {
cy.visit(`/auto/dashboard/model/${model.id}`);
});
});
// TODO: unskip when metabase#46337 is fixed
// See: https://github.com/metabase/metabase/issues/46337
it("should (metabase#46337)", { tags: "@skip" }, () => {
cy.log("ensure the dashcards render data not errors");
cy.findByTestId("dashboard-grid").within(() => {
cy.findByText("There was a problem displaying this chart.").should(
"not.exist",
);
cy.findByText(`Total ${MODEL_NAME}`).should("be.visible");
});
});
});
function slowDownCardQuery() {
return cy.intercept("POST", "/api/card/*/query", (req) => {
req.on("response", (res) => {
@@ -2246,7 +2150,9 @@ describe("issue 64138", () => {
})
.within(() => {
cy.findByLabelText("Zoom in").should("not.exist");
cy.findByText("Set as default view").should("be.visible").click();
// the update button is disabled until the map is panned, so only
// assert that it is shown while the zoom controls are hidden
cy.findByText("Set as default view").should("be.visible");
});
cy.log("hovering marker icons should not open their tooltips");

View File

@@ -0,0 +1,99 @@
const { H } = cy;
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import type { Collection, Dashboard, Tenant } from "metabase-types/api";
const { ORDERS_ID } = SAMPLE_DATABASE;
describe("scenarios > dashboard > admins using tenant-specific question picker", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("pro-self-hosted");
H.updateSetting("use-tenants", true);
cy.request("POST", "/api/ee/tenant", {
name: "Acme",
slug: "acme",
})
.its("body")
.as("tenant");
cy.get<Tenant>("@tenant")
.then(({ tenant_collection_id }) =>
H.createCollection({
name: "Tenant questions",
parent_id: tenant_collection_id,
}),
)
.its("body")
.as("tenantCollection");
cy.get<Collection>("@tenantCollection").then((tenantCollection) =>
H.createQuestion({
name: "Tenant orders",
collection_id: tenantCollection.id,
query: { "source-table": ORDERS_ID },
}),
);
});
it("allows adding questions from the dashboard's tenant-specific collection (EMB-2312)", () => {
cy.get<Tenant>("@tenant")
.then(({ tenant_collection_id }) =>
H.createDashboard({
name: "Tenant dashboard",
collection_id: tenant_collection_id,
}),
)
.its("body")
.as("dashboard");
cy.get<Dashboard>("@dashboard").then(({ id }) => {
H.visitDashboard(id);
H.editDashboard();
H.openQuestionsSidebar();
H.sidebar()
.findByTestId("breadcrumbs")
.should("contain", "Acme")
.and("not.contain", "Unknown");
H.sidebar().findByText("Tenant questions").click();
H.sidebar().findByText("Tenant orders").click();
H.getDashboardCards()
.should("have.length", 1)
.and("contain", "Tenant orders");
});
});
// Verify that Tenant collections and Our analytics are separate top-level entries.
it("allows browsing into a tenant-specific collection from Our Analytics (EMB-2312)", () => {
H.createDashboard({ name: "Our analytics dashboard" })
.its("body")
.as("dashboard");
cy.get<Dashboard>("@dashboard").then(({ id }) => {
H.visitDashboard(id);
H.editDashboard();
H.openQuestionsSidebar();
H.sidebar()
.findByTestId("breadcrumbs")
.should("contain", "Collections")
.and("contain", "Our analytics");
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
H.sidebar().findByText("Tenant collections").click();
H.sidebar().findByText("Acme").click();
H.sidebar().findByText("Tenant questions").click();
H.sidebar().findByText("Tenant orders").click();
H.getDashboardCards()
.should("have.length", 1)
.and("contain", "Tenant orders");
});
});
});

View File

@@ -0,0 +1,159 @@
const { H } = cy;
import { JWT_SHARED_SECRET } from "e2e/support/helpers/e2e-jwt-helpers";
import type { Collection, Dashboard, Tenant, User } from "metabase-types/api";
const TENANT_USER = {
first_name: "acme",
last_name: "user",
email: "acme.user@email.com",
"@tenant": "acme",
};
const loginAsTenantUser = (returnTo: string) => {
cy.task<string>("signJwt", {
payload: TENANT_USER,
secret: JWT_SHARED_SECRET,
}).then((key) =>
cy.visit(`/auth/sso?return_to=${encodeURIComponent(returnTo)}&jwt=${key}`),
);
};
describe("scenarios > dashboard > tenant users using question picker", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("pro-self-hosted");
cy.request("PUT", "/api/setting", {
"jwt-attribute-email": "email",
"jwt-attribute-firstname": "first_name",
"jwt-attribute-lastname": "last_name",
"jwt-enabled": true,
"jwt-identity-provider-uri": "localhost:4000",
"jwt-shared-secret": JWT_SHARED_SECRET,
"jwt-user-provisioning-enabled?": true,
"use-tenants": true,
});
cy.request("POST", "/api/ee/tenant", {
name: "Acme",
slug: "acme",
})
.its("body")
.as("tenant");
H.createSharedTenantCollection("Finance")
.its("body")
.as("financeCollection");
H.createSharedTenantCollection("Marketing")
.its("body")
.as("marketingCollection");
cy.get<Tenant>("@tenant")
.then(({ tenant_collection_id }) =>
H.createCollection({
name: "Tenant questions",
parent_id: tenant_collection_id,
}),
)
.its("body")
.as("tenantCollection");
});
// Tenant users do not know what is a 'Tenant-specific collection' or 'Shared collection'.
// Only show a flattened structure for them, with "Our data" being their tenant collection.
it("shows flattened tenant collections without grouping by namespaces (EMB-2312)", () => {
cy.get<Collection>("@tenantCollection")
.then(({ id }) =>
H.createDashboard({
name: "Tenant dashboard",
collection_id: id,
}),
)
.its("body")
.as("dashboard");
cy.get<Dashboard>("@dashboard").then(({ id }) => {
loginAsTenantUser(`/dashboard/${id}`);
H.editDashboard();
H.openQuestionsSidebar();
H.sidebar()
.findByTestId("breadcrumbs")
.should("contain", "Collections")
.and("contain", "Our data");
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
H.sidebar().findByText("Our data").should("be.visible");
H.sidebar().findByText("Finance").should("be.visible");
H.sidebar().findByText("Marketing").should("be.visible");
H.sidebar().findByText("Our analytics").should("not.exist");
H.sidebar().findByText("Shared collections").should("not.exist");
H.sidebar().findByText("Tenant collections").should("not.exist");
});
});
// The API omits 'Our analytics' when tenant users cannot read it, but it still
// returns their personal collection. Show that collection directly under the
// synthetic collections level instead of adding another 'collections' folder.
it("shows the personal collection under top-level collections (EMB-2312)", () => {
loginAsTenantUser("/");
cy.request<User>("GET", "/api/user/current").its("body").as("tenantUser");
cy.get<User>("@tenantUser")
.then(({ personal_collection_id }) => {
if (personal_collection_id == null) {
throw new Error("Tenant user has no personal collection");
}
return H.createDashboard({
name: "Personal dashboard",
collection_id: personal_collection_id,
});
})
.its("body")
.as("personalDashboard");
cy.get<Dashboard>("@personalDashboard").then(({ id }) => {
cy.intercept({
method: "GET",
pathname: "/api/collection/tree",
query: {
namespace: "shared-tenant-collection",
"exclude-archived": "true",
},
}).as("sharedTenantCollections");
H.visitDashboard(id);
H.editDashboard();
H.openQuestionsSidebar();
cy.wait("@sharedTenantCollections");
H.sidebar()
.findByTestId("breadcrumbs")
.should("contain", "Collections")
.and("contain", "My personal collection");
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
H.sidebar()
.findByRole("menuitem", { name: "My personal collection" })
.should("be.visible");
// There should not be a second nested "Collections" folder.
H.sidebar()
.findByTestId("breadcrumbs")
.findAllByText("Collections")
.should("have.length", 1);
H.sidebar()
.findByRole("menuitem", { name: "Collections" })
.should("not.exist");
});
});
});

View File

@@ -1535,70 +1535,6 @@ function assertScrollBarExists() {
});
}
describe("LOCAL TESTING ONLY > dashboard", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
/**
* WARNING:
* https://github.com/metabase/metabase/issues/15656
* - We are currently not able to test translations in CI
* - DO NOT unskip this test even after the issue is fixed
* - To be used for local testing only
* - Make sure you have translation resources built first.
* - Run `./bin/i18n/build-translation-resources`
* - Then start the server and Cypress tests
*/
it(
"dashboard filter should not show placeholder for translated languages (metabase#15694)",
{ tags: "@skip" },
() => {
cy.request("GET", "/api/user/current").then(
({ body: { id: USER_ID } }) => {
cy.request("PUT", `/api/user/${USER_ID}`, { locale: "fr" });
},
);
H.createQuestionAndDashboard({
questionDetails: {
name: "15694",
query: { "source-table": PEOPLE_ID },
},
dashboardDetails: {
parameters: [
{
name: "Location",
slug: "location",
id: "5aefc725",
type: "string/=",
sectionId: "location",
},
],
},
}).then(({ body: { card_id, dashboard_id } }) => {
H.addOrUpdateDashboardCard({
card_id,
dashboard_id,
card: {
parameter_mappings: [
{
parameter_id: "5aefc725",
card_id,
target: ["dimension", ["field", PEOPLE.STATE, null]],
},
],
},
});
cy.visit(`/dashboard/${dashboard_id}?location=AK&location=CA`);
H.filterWidget().contains(/\{0\}/).should("not.exist");
});
},
);
});
describe("scenarios > dashboard > permissions", () => {
let dashboardId;

View File

@@ -499,12 +499,12 @@ describe("scenarios > dashboard > title drill", () => {
{ row: 6, col: 6, size_x: 6, size_y: 6 },
],
}).then(({ dashboard, questions }) => {
H.visitDashboard(dashboard.id);
// Park the cursor off the grid before the dashcards render: a pointer
// resting over a title computes that title's href before it is asserted.
cy.get("body").realHover({ position: "topLeft" });
// make cursor start from a place where subsequent realHover() calls
// won't make the cursor move over the other cards during test
// (which would interfere with assertions)
cy.findByTestId("sidebar-toggle").realHover();
H.visitDashboard(dashboard.id);
H.waitForDashcardsToLoad({ count: 4 });
H.getDashboardCard(0)
.findByRole("link", { name: "Line chart" })
@@ -519,35 +519,58 @@ describe("scenarios > dashboard > title drill", () => {
.findByRole("link", { name: "Funnel chart" })
.as("funnel-chart-title");
assertTitleHrefOnFocus({
elementAlias: "@line-chart-title",
href: `/question/${questions[0].id}-line-chart`,
});
assertTitleHrefOnFocus({
elementAlias: "@row-chart-title",
href: `/question/${questions[1].id}-row-chart`,
});
assertTitleHrefOnHover({
elementAlias: "@map-chart-title",
href: `/question/${questions[2].id}-map-chart`,
});
assertTitleHrefOnHover({
elementAlias: "@funnel-chart-title",
href: `/question/${questions[3].id}-funnel-chart`,
const titles = [
{
elementAlias: "@line-chart-title",
href: `/question/${questions[0].id}-line-chart`,
trigger: focusTitle,
},
{
elementAlias: "@row-chart-title",
href: `/question/${questions[1].id}-row-chart`,
trigger: focusTitle,
},
{
elementAlias: "@map-chart-title",
href: `/question/${questions[2].id}-map-chart`,
trigger: hoverTitle,
},
{
elementAlias: "@funnel-chart-title",
href: `/question/${questions[3].id}-funnel-chart`,
trigger: hoverTitle,
},
];
cy.log("every title is an inert placeholder before focus and hover");
titles.forEach(({ elementAlias }) => {
cy.get(elementAlias).should("have.attr", "href", "#");
});
cy.log("focusing or hovering a title turns it into a question anchor");
titles.forEach(assertTitleBecomesQuestionAnchor);
});
});
function assertTitleHrefOnFocus({ elementAlias, href }) {
cy.get(elementAlias).should("have.attr", "href", "#");
cy.get(elementAlias).focus();
cy.get(elementAlias).should("have.attr", "href", href);
function focusTitle(element) {
element.focus();
}
function assertTitleHrefOnHover({ elementAlias, href }) {
cy.get(elementAlias).should("have.attr", "href", "#");
cy.get(elementAlias).realHover();
cy.get(elementAlias).should("have.attr", "href", href);
function hoverTitle(element) {
const { MouseEvent } = element.ownerDocument.defaultView;
element.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
}
/**
* The question href is computed lazily on focus/mouseenter, and the anchor
* is replaced when it resolves. Re-evaluating the trigger on each retry
* keeps the assertion anchored to the node currently in the DOM.
*/
function assertTitleBecomesQuestionAnchor({ elementAlias, href, trigger }) {
cy.get(elementAlias).should(($el) => {
trigger($el[0]);
expect($el).to.have.attr("href", href);
});
}
});

View File

@@ -1,78 +0,0 @@
const { H } = cy;
import {
PRODUCTS_AVERAGE_BY_CATEGORY,
PRODUCTS_COUNT_BY_CATEGORY,
} from "e2e/support/test-visualizer-data";
describe("scenarios > dashboard > visualizer > filters", () => {
beforeEach(() => {
H.restore();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
"dashcardQuery",
);
cy.signInAsNormalUser();
H.createQuestion(PRODUCTS_COUNT_BY_CATEGORY, {
idAlias: "productsCountByCategoryQuestionId",
wrapId: true,
});
H.createQuestion(PRODUCTS_AVERAGE_BY_CATEGORY, {
idAlias: "productsAvgByCreatedAtQuestionId",
wrapId: true,
});
});
// TODO those two datasets should be compatible with each other
it(
"should create and update a dashcard with 'Visualize another way' button",
{ tags: "@skip" },
() => {
H.createDashboard().then(({ body: { id: dashboardId } }) => {
H.visitDashboard(dashboardId);
});
H.editDashboard();
H.openQuestionsSidebar();
H.clickVisualizeAnotherWay(PRODUCTS_COUNT_BY_CATEGORY.name);
H.modal().within(() => {
H.switchToAddMoreData();
H.selectDataset(PRODUCTS_AVERAGE_BY_CATEGORY.name);
H.assertWellItemsCount({
vertical: 2,
});
});
H.saveDashcardVisualizerModal({ mode: "create" });
H.setFilter("Text or Category", "Is");
// Doing it twice to populate the two filters
H.selectDashboardFilter(H.getDashboardCard(0), "Category");
H.selectDashboardFilter(H.getDashboardCard(0), "Category");
H.saveDashboard();
H.getDashboardCard(0).within(() => {
cy.findByText("Doohickey").should("exist");
});
H.filterWidget().contains("Text").click();
H.popover().within(() => {
cy.findByText("Gadget").click();
cy.button("Add filter").click();
});
H.getDashboardCard(0).within(() => {
cy.findByText("Doohickey").should("not.exist");
});
},
);
});

View File

@@ -328,8 +328,20 @@ describe("bulk table operations", { viewportWidth: 1600 }, () => {
cy.log(
"Expand the rows up front - we'll need them later for the assertion",
);
TablePicker.getDatabase("Writable Postgres12").should("be.visible").click();
// The picker tree keeps mounting after the databases request resolves, so
// clicking the database row before its expand handler is wired drops the click
// and the schema fetch that populates the tables never fires. Wait for the
// expand toggle to render collapsed, click it, then confirm it expanded so the
// schema request reliably occurs before we select the database.
TablePicker.getDatabaseToggle("Writable Postgres12")
.should("have.attr", "aria-expanded", "false")
.click();
cy.wait("@getSchema");
TablePicker.getDatabaseToggle("Writable Postgres12").should(
"have.attr",
"aria-expanded",
"true",
);
TablePicker.getDatabase("Writable Postgres12")
.find('input[type="checkbox"]')

View File

@@ -407,6 +407,7 @@ describe("scenarios > data studio > library > metrics", () => {
H.selectDropdown()
.findByRole("option", { name: /Duration/ })
.click();
H.fillCacheDuration(24);
H.modal().findByTestId("strategy-form-submit-button").click();
cy.wait("@updateCacheConfig");

View File

@@ -1,6 +1,6 @@
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
import { ADMIN_PERSONAL_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
import type { TransformId } from "metabase-types/api";
import type { DependencyNode, TransformId } from "metabase-types/api";
const { H } = cy;
@@ -505,11 +505,24 @@ function createModelContent() {
}
function waitForBreakingDependencies() {
H.waitForBreakingDependencies(
(nodes) => nodes.length >= BROKEN_DEPENDENCIES.length,
// The instance already has unrelated (Usage Analytics) breaking dependencies,
// so a bare count check can pass before the test's own entities are analyzed.
// Wait until every expected entity is present by name instead.
H.waitForBreakingDependencies((nodes) =>
BROKEN_DEPENDENCIES.every((name) =>
nodes.some((node) => getNodeName(node) === name),
),
);
}
function getNodeName(node: DependencyNode): string | undefined {
const { data } = node;
if ("display_name" in data && data.display_name) {
return data.display_name;
}
return "name" in data ? data.name : undefined;
}
function checkList({
visibleEntities = [],
hiddenEntities = [],

View File

@@ -6,178 +6,6 @@ import { defer } from "metabase/utils/promise";
const { PRODUCTS, PRODUCTS_ID, ORDERS, ORDERS_ID, FEEDBACK, FEEDBACK_ID } =
SAMPLE_DATABASE;
describe("issue 15860", { tags: "@skip" }, () => {
const q1IdFilter = {
name: "Q1 ID",
slug: "q1_id",
id: "fde6db8b",
type: "id",
sectionId: "id",
default: [1],
};
const q1CategoryFilter = {
name: "Q1 Category",
slug: "q1_category",
id: "e8ff3175",
type: "string/=",
sectionId: "string",
filteringParameters: [q1IdFilter.id],
};
const q2IdFilter = {
name: "Q2 ID",
slug: "q2_id",
id: "t3e6hb7b",
type: "id",
sectionId: "id",
default: [3],
};
const q2CategoryFilter = {
name: "Q2 Category",
slug: "q2_category",
id: "ca1n357o",
type: "string/=",
sectionId: "string",
filteringParameters: [q2IdFilter.id],
};
function setDefaultValueForLockedFilter(filter, value) {
cy.findByText("Previewing locked parameters")
.parent()
.within(() => {
cy.findByText(filter).click({ force: true });
});
cy.findByPlaceholderText("Enter an ID").type(`${value}{enter}`);
cy.button("Add filter").click();
}
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.createQuestionAndDashboard({
questionDetails: {
name: "Q1",
query: { "source-table": PRODUCTS_ID },
},
dashboardDetails: {
embedding_params: {
q1_id: "locked",
q1_category: "enabled",
q2_id: "locked",
q2_category: "enabled",
},
enable_embedding: true,
parameters: [
q1IdFilter,
q1CategoryFilter,
q2IdFilter,
q2CategoryFilter,
],
},
cardDetails: {
size_x: 11,
size_y: 6,
},
}).then(({ body: { card_id: q1, dashboard_id } }) => {
// Create a second question with the same source table
H.createQuestion({
name: "Q2",
query: { "source-table": PRODUCTS_ID },
}).then(({ body: { id: q2 } }) => {
H.updateDashboardCards({
dashboard_id,
cards: [
// Add card for second question with parameter mappings
{
card_id: q2,
row: 0,
col: 8,
size_x: 13,
size_y: 6,
parameter_mappings: [
{
parameter_id: q2IdFilter.id,
card_id: q2,
target: ["dimension", ["field", PRODUCTS.ID, null]],
},
{
parameter_id: q2CategoryFilter.id,
card_id: q2,
target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
},
],
},
// Add parameter mappings to first question's card
{
card_id: q1,
parameter_mappings: [
{
parameter_id: q1IdFilter.id,
card_id: q1,
target: ["dimension", ["field", PRODUCTS.ID, null]],
},
{
parameter_id: q1CategoryFilter.id,
card_id: q1,
target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
},
],
},
],
});
});
cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
embedding_params: {
q1_id: "locked",
q1_category: "enabled",
q2_id: "locked",
q2_category: "enabled",
},
enable_embedding: true,
});
H.visitDashboard(dashboard_id);
H.openLegacyStaticEmbeddingModal({
resource: "dashboard",
resourceId: dashboard_id,
activeTab: "parameters",
unpublishBeforeOpen: false,
});
});
});
it("should work for locked linked filters connected to different cards with the same source table (metabase#15860)", () => {
setDefaultValueForLockedFilter("Q1 ID", 1);
setDefaultValueForLockedFilter("Q2 ID", 3);
H.visitIframe();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Q1 Category").click();
H.popover().within(() => {
cy.findByRole("listitem")
.should("have.length", 1)
.and("contain", "Gizmo");
});
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Q2 Category").click();
H.popover().within(() => {
cy.findByRole("listitem")
.should("have.length", 1)
.and("contain", "Doohickey");
});
});
});
describe("issue 20438", () => {
const questionDetails = {
name: "20438",
@@ -1003,45 +831,6 @@ describe("issue 40660", () => {
});
});
// Skipped since it does not make sense when CSP is disabled
describe("issue 49142", { tags: "@skip" }, () => {
const questionDetails = {
name: "Products",
query: { "source-table": PRODUCTS_ID, limit: 2 },
};
const dashboardDetails = {
name: "Embeddable dashboard",
enable_embedding: true,
};
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("embedding preview should be always working", () => {
H.createQuestionAndDashboard({
questionDetails,
dashboardDetails,
}).then(({ body: { dashboard_id } }) => {
H.visitDashboard(dashboard_id);
H.openLegacyStaticEmbeddingModal({
resource: "dashboard",
resourceId: dashboard_id,
activeTab: "lookAndFeel",
previewMode: "preview",
});
});
cy.findByTestId("embed-preview-iframe")
.its("0.contentDocument.body")
.should("be.visible")
.and("contain", "Embeddable dashboard");
});
});
describe("issue 8490", () => {
beforeEach(() => {
H.restore();

View File

@@ -106,30 +106,6 @@ describe(suiteTitle, () => {
});
});
it("shows exploration template when selected", { tags: "@skip" }, () => {
visitNewEmbedPage();
getEmbedSidebar().within(() => {
cy.findByLabelText("Metabase account (SSO)").click();
cy.findByText("Exploration").click();
cy.findByText("Next").click();
});
H.expectUnstructuredSnowplowEvent({
event: "embed_wizard_experience_completed",
event_detail:
"authType=sso,experience=exploration,isDefaultExperience=false",
});
H.waitForSimpleEmbedIframesToLoad();
H.getSimpleEmbedIframeContent().within(() => {
cy.log("data picker is visible");
cy.findByText("Pick your starting data").should("be.visible");
});
});
it("shows browser template when selected", () => {
visitNewEmbedPage();
@@ -193,38 +169,6 @@ describe(suiteTitle, () => {
cy.findByText("Person detail").should("be.visible");
});
});
it(
"shows question of id=1 when activity log is empty and chart is selected",
{ tags: "@skip" },
() => {
visitNewEmbedPage();
cy.wait("@emptyRecentItems");
getEmbedSidebar().within(() => {
cy.findByLabelText("Guest").click();
});
embedModalEnableEmbedding();
getEmbedSidebar().within(() => {
cy.findByText("Chart").click();
cy.findByText("Next").click();
});
H.expectUnstructuredSnowplowEvent({
event: "embed_wizard_experience_completed",
event_detail:
"authType=guest-embed,experience=chart,isDefaultExperience=false",
});
H.waitForSimpleEmbedIframesToLoad();
H.getSimpleEmbedIframeContent().within(() => {
cy.log("question title of id=1 is visible");
cy.findByText("Query log").should("be.visible");
});
},
);
});
it("should show a fake loading indicator in embed preview", () => {

View File

@@ -10,6 +10,21 @@ const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
const { H } = cy;
const LIMITED_ORDERS_ROW_COUNT = 5;
const clickEmbeddedTableCell = (value: string, rowsCount: number) => {
// Scoped to the table on purpose: column auto-sizing renders a hidden copy of
// the cells on the iframe's body and drops it once measured, so a body-wide
// query can latch onto a copy that is about to detach.
const tableRoot = () =>
H.getSimpleEmbedIframeContent().findByTestId("table-root");
// Anchor on the fully rendered table before interacting with it.
tableRoot().should("have.attr", "data-rows-count", String(rowsCount));
tableRoot().findByText(value).should("be.visible").click({ force: true });
};
describe("scenarios > embedding > sdk iframe embedding > custom elements api", () => {
beforeEach(() => {
cy.signInAsAdmin();
@@ -160,7 +175,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
H.createQuestionAndDashboard({
questionDetails: {
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: 5 },
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
},
}).then(({ body: { dashboard_id } }) => {
H.visitCustomHtmlPage(`
@@ -169,11 +184,10 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
<metabase-dashboard dashboard-id="${dashboard_id}" drills />
`);
H.getSimpleEmbedIframeContent()
.findAllByText("37.65")
.first()
.should("be.visible")
.click();
cy.wait("@getDashCardQuery");
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)
.should("be.visible");
@@ -184,7 +198,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
H.createQuestionAndDashboard({
questionDetails: {
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: 5 },
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
},
}).then(({ body: { dashboard_id } }) => {
H.visitCustomHtmlPage(`
@@ -195,11 +209,8 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getDashCardQuery");
H.getSimpleEmbedIframeContent()
.findAllByText("37.65")
.first()
.should("be.visible")
.click({ force: true });
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)
.should("not.exist");
@@ -294,7 +305,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
it("should enable drill-through when drills is true", () => {
H.createQuestion({
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: 5 },
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
}).then(({ body: { id: questionId } }) => {
H.visitCustomHtmlPage(`
${H.getNewEmbedScriptTag()}
@@ -304,17 +315,8 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getCardQuery");
// Wait for the table to finish rendering before interacting,
// as column auto-sizing can cause re-renders that detach elements.
H.getSimpleEmbedIframeContent()
.findByTestId("table-root")
.should("have.attr", "data-rows-count", "5");
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
H.getSimpleEmbedIframeContent()
.findAllByText("37.65")
.first()
.should("be.visible")
.click({ force: true });
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)
.should("be.visible");
@@ -324,7 +326,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
it("should disable drill-through when drills is false", () => {
H.createQuestion({
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: 5 },
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
}).then(({ body: { id: questionId } }) => {
H.visitCustomHtmlPage(`
${H.getNewEmbedScriptTag()}
@@ -334,16 +336,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getCardQuery");
// Wait for the table to finish rendering before interacting,
// as column auto-sizing can cause re-renders that detach elements.
H.getSimpleEmbedIframeContent()
.findByTestId("table-root")
.should("have.attr", "data-rows-count", "5");
H.getSimpleEmbedIframeContent()
.findByText("37.65")
.should("be.visible")
.click({ force: true });
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)

View File

@@ -335,20 +335,19 @@ describe("scenarios > embedding > sdk iframe embed options passthrough", () => {
frame.within(() => {
cy.findByText("Pick your starting data").should("be.visible");
H.popover().findByText("Orders").click();
H.popover().within(() => {
cy.findByText("Orders").click();
});
cy.findByRole("button", { name: "Visualize" }).click();
// if we don't limit this query, layout shifts can push the first row out of the viewport
cy.icon("list").click();
cy.findByPlaceholderText("Enter a limit").type("5").blur({ force: true }); // blur hack
H.visualize();
cy.log("1. clicking on the filter should drill down");
cy.findAllByText("37.65").first().should("be.visible").click();
cy.findByText(/Filter by this value/).should("be.visible");
cy.findByTestId("click-actions-filter-section")
.find("button")
.first()
.click();
cy.findByText("37.65").should("be.visible").click();
H.popover().within(() => {
cy.findByText(/Filter by this value/).should("be.visible");
cy.button("<").click();
});
cy.findAllByText("29.8").first().should("be.visible");
cy.findByTestId("interactive-question-result-toolbar").should(
"be.visible",

View File

@@ -414,67 +414,6 @@ describe("issue 12985 > dashboard filter dropdown/search", () => {
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Ergonomic Silk Coat");
});
it(
"should work for aggregated questions (metabase#12985-2)",
{ tags: "@skip" },
() => {
const questionDetails = {
name: "12985-v2",
query: {
"source-query": {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
breakout: [["field", PRODUCTS.CATEGORY, null]],
},
filter: [">", ["field", "count", { "base-type": "type/Integer" }], 1],
},
};
H.createQuestionAndDashboard({ questionDetails, dashboardDetails }).then(
({ body: { id, card_id, dashboard_id } }) => {
cy.log("Connect dashboard filter to the aggregated card");
cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
dashcards: [
{
id,
card_id,
row: 0,
col: 0,
size_x: 11,
size_y: 6,
series: [],
visualization_settings: {},
// Connect filter to the card
parameter_mappings: [
{
parameter_id: categoryFilter.id,
card_id,
target: [
"dimension",
["field", "CATEGORY", { "base-type": "type/Text" }],
],
},
],
},
],
});
H.visitDashboard(dashboard_id);
},
);
H.filterWidget().contains("Category").click();
// It will fail at this point until the issue is fixed because popover never appears
H.popover().contains("Gadget").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Add filter").click();
cy.url().should("contain", "?category=Gadget");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Ergonomic Silk Coat");
},
);
});
describe("issues 15119 and 16112", () => {

View File

@@ -1266,63 +1266,6 @@ describe("issue 17061", () => {
});
});
// TODO ranquild unskip after v54 release
describe("issue 48824", { tags: "@skip" }, () => {
const dateParameter = {
id: "abc",
name: "Date filter",
slug: "filter-date",
type: "date/all-options",
default: "past30days-from-7days",
};
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
});
it("should correctly translate relative filters in dashboards (metabase#48824)", () => {
cy.log("set locale");
cy.request("GET", "/api/user/current").then(({ body: user }) => {
cy.request("PUT", `/api/user/${user.id}`, { locale: "en-ZZ" });
});
cy.log("add a date parameter with a relative default value to a dashboard");
cy.request("PUT", `/api/dashboard/${ORDERS_DASHBOARD_ID}`, {
parameters: [dateParameter],
});
H.updateDashboardCards({
dashboard_id: ORDERS_DASHBOARD_ID,
cards: [
{
card_id: ORDERS_QUESTION_ID,
parameter_mappings: [
{
card_id: ORDERS_QUESTION_ID,
parameter_id: dateParameter.id,
target: ["dimension", ["field", ORDERS.CREATED_AT, null]],
},
],
},
],
});
cy.log("check translations");
H.visitDashboard(ORDERS_DASHBOARD_ID, {
params: { [dateParameter.slug]: "past30days" },
});
cy.log("Previous 30 days");
H.filterWidget().findByText("[zz] Previous 30 days").should("be.visible");
H.filterWidget().icon("revert").click();
cy.log("Previous 30 days, starting 7 days ago");
H.filterWidget()
.findByText("[zz] Previous 30 days, [zz] starting 7 days ago")
.should("be.visible");
});
});
describe("issue 62627", () => {
beforeEach(() => {
H.restore();

View File

@@ -357,57 +357,6 @@ describe("issue 45410", () => {
});
});
});
describe("issue 26861", { tags: "@skip" }, () => {
const filter = {
id: "a3b95feb-b6d2-33b6-660b-bb656f59b1d7",
name: "filter",
"display-name": "Filter",
type: "dimension",
dimension: ["field", ORDERS.CREATED_AT, null],
"widget-type": "date/all-options",
default: null,
};
const nativeQuery = {
name: "26861",
native: {
query: "select * from orders where {{filter}} limit 2",
"template-tags": {
filter,
},
},
};
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
H.restore();
cy.signInAsAdmin();
H.createNativeQuestion(nativeQuery, { visitQuestion: true });
});
it("exclude filter shouldn't break native questions with field filters (metabase#26861)", () => {
H.filterWidget().click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Exclude…").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Days of the week…").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Tuesday").click();
cy.button("Update filter").click();
// In all other places in application, POST /api/dataset fires immediately after "Update filter"
// A part of this bug is that we have to manually run the query so the next step will fail
cy.wait("@dataset");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("CREATED_AT excludes Tuesday");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("117.03").should("not.exist");
});
});
describe("issue 27123", () => {
const questionDetails = {

View File

@@ -466,35 +466,6 @@ describe("scenarios > question > filter", () => {
});
});
it(
"column filters should work for metrics (metabase#15333)",
{ tags: "@skip" },
() => {
H.visitQuestionAdhoc({
dataset_query: {
type: "query",
query: {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
breakout: [["field-id", PRODUCTS.CATEGORY]],
},
database: SAMPLE_DB_ID,
},
display: "table",
});
cy.get("[data-testid=cell-data]").contains("Count").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Filter by this column").click();
cy.findByPlaceholderText("Enter a number").type("42");
cy.button("Update filter").should("not.be.disabled").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Doohickey");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Gizmo").should("not.exist");
},
);
it("custom expression filter should reference fields by their name, not by their id (metabase#15748)", () => {
H.openOrdersTable({ mode: "notebook" });

View File

@@ -5,10 +5,7 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import type { StructuredQuestionDetails } from "e2e/support/helpers";
import type { NativeQuestionDetails } from "e2e/support/helpers/api/createQuestion";
import {
openDetailsSidebar,
turnIntoModel,
} from "./helpers/e2e-models-helpers";
import { turnIntoModel } from "./helpers/e2e-models-helpers";
const { ORDERS_ID, ORDERS, PRODUCTS_ID } = SAMPLE_DATABASE;
@@ -103,110 +100,6 @@ describe("issue 20045", () => {
});
});
describe("issue 20624", { tags: "@skip" }, () => {
const renamedColumn = "TITLE renamed";
const questionDetails: NativeQuestionDetails = {
name: "20624",
type: "model",
native: { query: "select * from PRODUCTS limit 2" },
visualization_settings: {
column_settings: { '["name","TITLE"]': { column_title: renamedColumn } },
},
};
beforeEach(() => {
cy.intercept("PUT", "/api/card/*").as("updateCard");
H.restore();
cy.signInAsAdmin();
H.createNativeQuestion(questionDetails, { visitQuestion: true });
});
it("models metadata should override previously defined column settings (metabase#20624)", () => {
openDetailsSidebar();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Customize metadata").click();
// Open settings for this column
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText(renamedColumn).click();
// Let's set a new name for it
cy.findByDisplayValue(renamedColumn).clear().type("Foo").blur();
cy.button("Save changes").click();
cy.wait("@updateCard");
cy.get("[data-testid=cell-data]").should("contain", "Foo");
});
});
describe("issue 22517", () => {
function renameColumn(column: string, newName: string) {
cy.findByDisplayValue(column).clear().type(newName).blur();
}
beforeEach(() => {
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.intercept("PUT", "/api/card/*").as("updateMetadata");
H.restore();
cy.signInAsAdmin();
H.createNativeQuestion(
{
name: "22517",
native: { query: "select * from orders" },
type: "model",
},
{ visitQuestion: true },
);
H.openQuestionActions();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Edit metadata").click();
renameColumn("ID", "Foo");
cy.button("Save changes").click();
cy.wait("@updateMetadata");
});
it(
"adding or removing a column should not drop previously edited metadata (metabase#22517)",
{ tags: "@skip" },
() => {
H.openQuestionActions();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Edit query definition").click();
// Make sure previous metadata changes are reflected in the UI
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Foo");
// This will edit the original query and add the `SIZE` column
// Updated query: `select *, case when quantity > 4 then 'large' else 'small' end size from orders`
H.NativeEditor.focus().type(
"{leftarrow}".repeat(" from orders".length) +
", case when quantity > 4 then 'large' else 'small' end size ",
);
cy.findByTestId("native-query-editor-container").icon("play").click();
cy.wait("@dataset");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Foo");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save changes").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Foo");
},
);
});
describe("issue 22518", () => {
beforeEach(() => {
H.restore();

View File

@@ -1,13 +1,11 @@
const { H } = cy;
import { SAMPLE_DB_ID, SAMPLE_DB_SCHEMA_ID } from "e2e/support/cypress_data";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
ORDERS_DASHBOARD_ID,
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
import { turnIntoModel } from "./helpers/e2e-models-helpers";
const { ORDERS_ID, REVIEWS, REVIEWS_ID, PRODUCTS } = SAMPLE_DATABASE;
describe("issue 19737", () => {
@@ -109,48 +107,6 @@ describe("issue 19737", () => {
});
});
describe("issue 22519", { tags: "@skip" }, () => {
const questionDetails = {
query: {
"source-table": REVIEWS_ID,
},
};
beforeEach(() => {
cy.intercept("PUT", "/api/field/*").as("updateField");
cy.intercept("POST", "/api/dataset").as("dataset");
H.restore();
cy.signInAsAdmin();
H.DataModel.visit({
databaseId: SAMPLE_DB_ID,
schemaName: SAMPLE_DB_SCHEMA_ID,
tableId: REVIEWS_ID,
fieldId: REVIEWS.REVIEWS,
});
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Don't cast").click();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("UNIX seconds → Datetime").click();
cy.wait("@updateField");
});
it("model query should not fail when data model is using casting (metabase#22519)", () => {
H.createQuestion(questionDetails, { visitQuestion: true });
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("xavier");
turnIntoModel();
cy.wait("@dataset");
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("xavier");
});
});
describe("issue 23024", () => {
function addModelToDashboardAndVisit() {
H.createDashboard().then(({ body: { id } }) => {

View File

@@ -201,72 +201,6 @@ describe("issue 12581", () => {
});
});
describe("issue 13961", { tags: "@skip" }, () => {
const categoryFilter = {
id: "00315d5e-4a41-99da-1a41-e5254dacff9d",
name: "category",
"display-name": "Category",
type: "dimension",
default: "Doohickey",
dimension: ["field", PRODUCTS.CATEGORY, null],
"widget-type": "category",
};
const productIdFilter = {
id: "4775bccc-e82a-4069-fc6b-2acc90aadb8b",
name: "prodid",
"display-name": "ProdId",
type: "number",
default: null,
};
const nativeQuery = {
name: "13961",
native: {
query:
"SELECT * FROM PRODUCTS WHERE 1=1 AND {{category}} [[AND ID={{prodid}}]]",
"template-tags": {
category: categoryFilter,
prodid: productIdFilter,
},
},
};
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.createNativeQuestion(nativeQuery, { visitQuestion: true });
});
it("should clear default filter value in native questions (metabase#13961)", () => {
cy.findAllByText("Small Marble Shoes"); // Product ID 2, Doohickey
cy.location("search").should("eq", "?category=Doohickey");
// Remove default filter (category)
H.filterWidget().findByRole("button").click();
cy.icon("play").first().should("be.visible").as("rerunQuestion").click();
cy.wait("@cardQuery");
cy.url().should("not.include", "?category=Doohickey");
// Add value `1` to the ID filter
cy.findByPlaceholderText(productIdFilter["display-name"]).type("1");
cy.get("@rerunQuestion").click();
cy.wait("@cardQuery");
cy.log("Reported tested and failing on v0.34.3 through v0.37.3");
cy.log("URL is correct at this point, but there are no results");
cy.location("search").should("eq", `?${productIdFilter.name}=1`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Rustic Paper Wallet"); // Product ID 1, Gizmo
});
});
describe("issue 14302", () => {
const priceFilter = {
id: "39b51ccd-47a7-9df6-a1c5-371918352c79",

View File

@@ -218,26 +218,6 @@ describe("scenarios > filters > sql filters > basic filter types", () => {
.findByPlaceholderText("Testingparamvisbility77")
.should("be.visible");
});
// flaky test (#19454)
it(
"should show an info popover when hovering over fields in the field filter field picker",
{ tags: "@skip" },
() => {
SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{cat}}");
SQLFilter.openTypePickerFromDefaultFilterType();
SQLFilter.chooseType("Field Filter");
H.popover().within(() => {
cy.findByText("People").click();
cy.findByText("City").trigger("mouseenter");
});
H.popover().contains("City");
H.popover().contains("1,966 distinct values");
},
);
});
describe("scenarios > filters > sql filters > multiple values", () => {

View File

@@ -294,114 +294,6 @@ describe("issue 20044", () => {
});
});
describe("issue 20625", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.updateSetting("native-query-autocomplete-match-style", "prefix");
cy.signInAsNormalUser();
});
// realpress messes with cypress 13
it("should continue to request more prefix matches from the server when the limit was hit (metabase#20625)", () => {
cy.intercept("GET", "/api/database/*/autocomplete_suggestions**", {
statusCode: 200,
body: [
// This result has 50 items, which is the limit
// as set by the backend.
// This is needed to trigger the second autocomplete.
["ORDERS", "Table"],
["PEOPLE", "Table"],
["REVIEWS", "Table"],
["ACTIVE_SUBSCRIPTION", "ACCOUNTS :type/Boolean :type/Category"],
["ADDRESS", "PEOPLE :type/Text"],
["BIRTH_DATE", "PEOPLE :type/Date"],
["BUTTON_LABEL", "ANALYTIC_EVENTS :type/Text :type/Category"],
["CANCELED_AT", "ACCOUNTS :type/DateTime :type/CancelationTimestamp"],
["CATEGORY", "PRODUCTS :type/Text :type/Category"],
["CREATED_AT", "ACCOUNTS :type/DateTime :type/CreationTimestamp"],
["CREATED_AT", "ORDERS :type/DateTime :type/CreationTimestamp"],
["CREATED_AT", "PEOPLE :type/DateTime :type/CreationTimestamp"],
["CREATED_AT", "PRODUCTS :type/DateTime :type/CreationTimestamp"],
["CREATED_AT", "REVIEWS :type/DateTime :type/CreationTimestamp"],
["DATE_RECEIVED", "FEEDBACK :type/DateTime"],
["DATE_RECEIVED", "INVOICES :type/DateTime"],
["EAN", "PRODUCTS :type/Text"],
["EMAIL", "ACCOUNTS :type/Text :type/Email"],
["EMAIL", "FEEDBACK :type/Text :type/Email"],
["EMAIL", "PEOPLE :type/Text :type/Email"],
["EVENT", "ANALYTIC_EVENTS :type/Text :type/Category"],
["EXPECTED_INVOICE", "INVOICES :type/Boolean :type/Category"],
["FIRST_NAME", "ACCOUNTS :type/Text :type/Name"],
["LAST_NAME", "ACCOUNTS :type/Text :type/Name"],
["LATITUDE", "ACCOUNTS :type/Float :type/Latitude"],
["LATITUDE", "PEOPLE :type/Float :type/Latitude"],
["LEGACY_PLAN", "ACCOUNTS :type/Boolean :type/Category"],
["LONGITUDE", "ACCOUNTS :type/Float :type/Longitude"],
["LONGITUDE", "PEOPLE :type/Float :type/Longitude"],
["NAME", "PEOPLE :type/Text :type/Name"],
["PAGE_URL", "ANALYTIC_EVENTS :type/Text :type/URL"],
["PAYMENT", "INVOICES :type/Float"],
["PRICE", "PRODUCTS :type/Float"],
["RATING_MAPPED", "FEEDBACK :type/Text :type/Category"],
["REVIEWER", "REVIEWS :type/Text"],
["SEATS", "ACCOUNTS :type/Integer"],
["SOURCE", "ACCOUNTS :type/Text :type/Source"],
["SOURCE", "PEOPLE :type/Text :type/Source"],
["STATE", "PEOPLE :type/Text :type/State"],
["TIMESTAMP", "ANALYTIC_EVENTS :type/DateTime"],
["TITLE", "PRODUCTS :type/Text :type/Title"],
["TRIAL_CONVERTED", "ACCOUNTS :type/Boolean :type/Category"],
["TRIAL_ENDS_AT", "ACCOUNTS :type/DateTime"],
["USER_ID", "ORDERS :type/Integer :type/FK"],
["VENDOR", "PRODUCTS :type/Text :type/Company"],
["VENDOR_ID", "PRODUCTS :type/Integer :type/FK"],
["USER_NAME", "PRODUCTS :type/Text :type/Name"],
["TEST_COLUMN_1", "PRODUCTS :type/Text :type/Name"],
["TEST_COLUMN_2", "PRODUCTS :type/Text :type/Name"],
["TEST_COLUMN_3", "PRODUCTS :type/Text :type/Name"],
],
}).as("autocomplete");
H.startNewNativeQuestion();
H.NativeEditor.type("e");
// autocomplete_suggestions?prefix=s
cy.wait("@autocomplete");
H.NativeEditor.type("o");
// autocomplete_suggestions?prefix=so
cy.wait("@autocomplete");
});
it("should not continue to request more prefix matches from the server when the limit was not hit (metabase#20625)", () => {
cy.intercept("GET", "/api/database/*/autocomplete_suggestions**", {
statusCode: 200,
body: [
// This result has less than 50 items, which is under the limit
// as set by the backend.
// It will not be necessary to trigger the second autocomplete.
["ORDERS", "Table"],
["PEOPLE", "Table"],
["REVIEWS", "Table"],
["ACTIVE_SUBSCRIPTION", "ACCOUNTS :type/Boolean :type/Category"],
["ADDRESS", "PEOPLE :type/Text"],
],
}).as("autocomplete");
H.startNewNativeQuestion();
H.NativeEditor.type("e");
// autocomplete_suggestions?prefix=s
cy.wait("@autocomplete");
H.NativeEditor.type("o");
cy.get("@autocomplete.all").should("have.length", 1);
});
});
describe("issue 21034", () => {
beforeEach(() => {
H.restore();

View File

@@ -1,9 +1,5 @@
const { H } = cy;
import {
SAMPLE_DB_ID,
USER_GROUPS,
WRITABLE_DB_ID,
} from "e2e/support/cypress_data";
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { THIRD_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
@@ -527,117 +523,6 @@ describe("scenarios > question > native", () => {
});
});
// causes error in cypress 13
describe("no native access", { tags: ["@external", "@skip"] }, () => {
beforeEach(() => {
H.restore("postgres-12");
cy.signInAsAdmin();
cy.intercept("/api/database?saved=true").as("database");
cy.updatePermissionsGraph({
[USER_GROUPS.ALL_USERS_GROUP]: {
[WRITABLE_DB_ID]: {
"view-data": "blocked",
"create-queries": "no",
},
},
[USER_GROUPS.NOSQL_GROUP]: {
[SAMPLE_DB_ID]: {
"view-data": "unrestricted",
"create-queries": "query-builder-and-native",
},
[WRITABLE_DB_ID]: {
"view-data": "unrestricted",
"create-queries": "query-builder",
},
},
});
cy.updateCollectionGraph({
[USER_GROUPS.NOSQL_GROUP]: { root: "write" },
});
H.createNativeQuestion(
{
name: "Secret Orders",
native: {
query: "SELECT * FROM ORDERS",
},
database: WRITABLE_DB_ID,
},
{
wrapId: true,
},
);
cy.signIn("nosql");
});
it("should not display the query when you do not have native access to the data source", () => {
cy.get("@questionId").then((questionId) =>
cy.visit(`/question/${questionId}`),
);
cy.findByTestId("native-query-top-bar").within(() => {
cy.findByText("This question is written in SQL.").should("be.visible");
cy.findByTestId("visibility-toggler").should("not.exist");
});
cy.log("#32387");
cy.findByRole("button", { name: /New/ }).click();
H.popover().findByText("SQL query").click();
cy.wait("@database");
cy.go("back");
cy.findByTestId("native-query-top-bar").within(() => {
cy.findByText("This question is written in SQL.").should("be.visible");
cy.findByTestId("visibility-toggler").should("not.exist");
});
});
it(
"shows format query button only for sql queries",
{ tags: "@mongo" },
() => {
const MONGO_DB_NAME = "QA Mongo";
cy.intercept("POST", "/api/card").as("createQuestion");
cy.intercept("POST", "/api/dataset").as("dataset");
H.restore("mongo-5");
cy.signInAsNormalUser();
H.startNewNativeQuestion();
cy.findByTestId("gui-builder-data").click();
cy.findByLabelText(MONGO_DB_NAME).click();
cy.findByLabelText("Auto-format").should("not.exist");
cy.findByTestId("native-query-top-bar").findByText(MONGO_DB_NAME).click();
// Switch to SQL engine which is supported by the formatter
H.popover().findByText("Sample Database").click();
H.NativeEditor.focus().type("select * from orders", {
parseSpecialCharSequences: false,
});
// It should load the formatter chunk only when used
cy.intercept("GET", "**/sql-formatter**").as("sqlFormatter");
cy.findByLabelText("Auto-format").click();
cy.wait("@sqlFormatter");
H.NativeEditor.get().should("be.visible").get(".ace_line").as("lines");
cy.get("@lines").eq(0).should("have.text", "SELECT");
cy.get("@lines").eq(1).should("have.text", " *");
cy.get("@lines").eq(2).should("have.text", "FROM");
cy.get("@lines").eq(3).should("have.text", " orders");
},
);
});
describe("scenarios > native question > data reference sidebar", () => {
beforeEach(() => {
H.restore();

View File

@@ -1,24 +0,0 @@
const { H } = cy;
describe("scenarios > about Metabase", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
cy.visit("/");
cy.icon("gear").click();
H.popover().findByText("About Metabase").click();
});
it(
"should display correct Metabase version (metabase#15656)",
{ tags: "@skip" },
() => {
H.modal().within(() => {
cy.findByText(/You're on version v[01](\.\d+){2,3}(-[\w\d]+)?/i);
cy.findByText(/Built on \d{4}-\d{2}-\d{2}/);
cy.findByText("Branch: ?").should("not.exist");
cy.findByText("Hash: ?").should("not.exist");
});
},
);
});

View File

@@ -73,18 +73,6 @@ describe("scenarios > reference > databases", () => {
});
});
it(
"should sort data reference database list (metabase#15598)",
{ tags: "@skip" },
() => {
cy.visit("/browse");
checkReferenceDatabasesOrder();
cy.visit("/reference/databases/");
checkReferenceDatabasesOrder();
},
);
it("should sort databases in new UI based question data selection popover", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
@@ -99,14 +87,6 @@ describe("scenarios > reference > databases", () => {
});
});
});
it(
"should sort databases in new native question data selection popover",
{ tags: "@skip" },
() => {
checkQuestionSourceDatabasesOrder("Native query");
},
);
});
describe("x-ray", () => {
@@ -155,25 +135,3 @@ describe("scenarios > reference > databases", () => {
});
});
});
function checkReferenceDatabasesOrder() {
cy.get("[class*=Card]").as("databaseCard").first().should("have.text", "a");
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.get("@databaseCard").last().should("have.text", "Sample Database");
}
function checkQuestionSourceDatabasesOrder() {
// Last item is "Saved Questions" for UI based questions so we have to check for the one before that (-2), and the last one for "Native" (-1)
const lastDatabaseIndex = -1;
const selector = "[data-element-id=list-item]-title";
H.startNewQuestion();
H.popover().within(() => {
cy.findByText("Raw Data").click();
cy.get(selector).as("databaseName").eq(1).should("have.text", "a");
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.get("@databaseName")
.eq(lastDatabaseIndex)
.should("have.text", "Sample Database");
});
}

View File

@@ -1,6 +1,7 @@
const { H } = cy;
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
import type { Dashboard } from "metabase-types/api";
const { ORDERS_ID } = SAMPLE_DATABASE;
@@ -429,6 +430,43 @@ describe("scenarios > organization > entity picker > shared-tenant-collection na
});
});
it("should allow admins to add questions from shared tenant sub-collections", () => {
setupTenantCollections().as("tenantCollections");
cy.get<{ subCollectionId: number }>("@tenantCollections").then(
({ subCollectionId }) =>
H.createQuestion({
name: "Tenant Sub-Collection Orders Question",
collection_id: subCollectionId,
query: { "source-table": ORDERS_ID },
}),
);
H.createDashboard({ name: "Test Dashboard" }).its("body").as("dashboard");
cy.get<Dashboard>("@dashboard").then(({ id }) => {
H.visitDashboard(id);
H.editDashboard();
H.openQuestionsSidebar();
H.sidebar()
.findByTestId("breadcrumbs")
.should("contain", "Collections")
.and("contain", "Our analytics")
.findByText("Collections")
.click();
H.sidebar().findByText(TENANT_ROOT_NAME).should("be.visible").click();
H.sidebar().findByText("Test Tenant Collection").click();
H.sidebar().findByText("Tenant Sub-Collection").click();
H.sidebar().findByText("Tenant Sub-Collection Orders Question").click();
H.getDashboardCards()
.should("have.length", 1)
.and("contain", "Tenant Sub-Collection Orders Question");
});
});
it("should show 'Collections' in breadcrumb when navigating into Our Analytics sub-collections", () => {
setupTenantCollections().then(() => {
H.createCollection({ name: "Our Analytics Sub" }).then(() => {

View File

@@ -365,12 +365,16 @@ describe("scenarios > organization > timelines > question", () => {
H.timelineEventChip("RC2").should("not.exist");
// its timeline, visible but having one hidden event
// should display its checkbox with a "dash" icon
// should display its checkbox in an indeterminate state
cy.findByTestId("sidebar-content")
.findByText("Releases")
.closest("[aria-label='Timeline card header']")
.within(() => {
cy.icon("dash").should("be.visible");
cy.findByRole("checkbox").should(
"have.prop",
"indeterminate",
true,
);
// Hide the timeline then show it again
cy.findByRole("checkbox").click();

View File

@@ -117,6 +117,7 @@ describe("impersonated permission", { tags: "@external" }, () => {
.findByText(/Caching settings/)
.should("be.visible");
H.selectCacheStrategy(/Duration/);
H.fillCacheDuration(24);
H.cacheStrategySidesheet()
.findByRole("button", { name: /Save/ })
.click();

View File

@@ -8,75 +8,11 @@ import {
} from "e2e/support/cypress_sample_instance_data";
const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
const { ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, REVIEWS_ID, PRODUCTS } =
SAMPLE_DATABASE;
const { PRODUCTS_ID, PEOPLE_ID, PRODUCTS } = SAMPLE_DATABASE;
const { nocollection } = USERS;
const PG_DB_ID = 2;
// NOTE: This issue wasn't specifically related to PostgreSQL. We simply needed to add another DB to reproduce it.
describe("issue 13347", { tags: ["@external", "@skip"] }, () => {
beforeEach(() => {
cy.intercept("POST", "/api/dataset").as("dataset");
H.restore("postgres-12");
cy.signInAsAdmin();
cy.updatePermissionsGraph({
[ALL_USERS_GROUP]: {
1: {
"view-data": "unrestricted",
"create-queries": "query-builder-and-native",
},
[PG_DB_ID]: {
"view-data": "unrestricted",
"create-queries": "no",
},
},
});
cy.updateCollectionGraph({
[ALL_USERS_GROUP]: { root: "read" },
});
H.withDatabase(
PG_DB_ID,
({ ORDERS_ID }) =>
H.createQuestion({
name: "Q1",
query: { "source-table": ORDERS_ID },
database: PG_DB_ID,
}),
H.createNativeQuestion({
name: "Q2",
native: { query: "SELECT * FROM ORDERS" },
database: PG_DB_ID,
}),
);
});
["QB", "Native"].forEach((test) => {
it(`${test.toUpperCase()} version:\n should be able to select question (from "Saved Questions") which belongs to the database user doesn't have data-permissions for (metabase#13347)`, () => {
cy.signIn("none");
H.startNewQuestion();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Saved Questions").click();
if (test === "QB") {
cy.findByText("Q1").click();
} else {
cy.findByText("Q2").click();
}
cy.wait("@dataset", { timeout: 5000 });
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.contains("37.65");
});
});
});
describe("postgres > user > query", { tags: "@external" }, () => {
beforeEach(() => {
H.restore("postgres-12");
@@ -152,46 +88,6 @@ describe("postgres > user > query", { tags: "@external" }, () => {
});
});
describe("issue 17777", { tags: "@skip" }, () => {
function hideTables(tables) {
cy.request("PUT", "/api/table", {
ids: tables,
visibility_type: "hidden",
});
}
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
hideTables([ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, REVIEWS_ID]);
});
it("should still be able to set permissions on individual tables, even though they are hidden in data model (metabase#17777)", () => {
cy.visit(`/admin/permissions/data/group/${ALL_USERS_GROUP}`);
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Permissions for the All Users group");
cy.findByTextEnsureVisible("Sample Database").click();
cy.location("pathname").should(
"eq",
`/admin/permissions/data/group/${ALL_USERS_GROUP}/database/${SAMPLE_DB_ID}`,
);
cy.findByTestId("permission-table").within(() => {
cy.findByText("Orders");
cy.findByText("Products");
cy.findByText("Reviews");
cy.findByText("People");
});
cy.findAllByText("No self-service").first().click();
H.popover().contains("Unrestricted");
});
});
describe("issue 19603", () => {
beforeEach(() => {
H.restore();

View File

@@ -709,125 +709,3 @@ describe("issue 19742", () => {
});
});
});
const QUESTION_1 = {
name: "Q1",
query: {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
breakout: [["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }]],
},
};
const QUESTION_2 = {
name: "Q2",
query: {
"source-table": PRODUCTS_ID,
aggregation: [
["sum", ["field", PRODUCTS.PRICE, { "base-type": "type/Float" }]],
],
breakout: [["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }]],
},
};
describe("issue 19893", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it(
"should display correct join source table when joining visited questions (metabase#19893)",
{ tags: "@skip" },
() => {
H.createQuestion(QUESTION_1, {
wrapId: true,
idAlias: "questionId1",
visitQuestion: true,
});
H.createQuestion(QUESTION_2, {
wrapId: true,
idAlias: "questionId2",
visitQuestion: true,
});
cy.then(function () {
const { questionId1, questionId2 } = this;
createQ1PlusQ2Question(questionId1, questionId2).then(
({ body: question }) => {
cy.visit(`/question/${question.id}/notebook`);
},
);
});
assertQ1PlusQ2Joins();
},
);
it(
"should display correct join source table when joining non-visited questions (metabase#19893)",
{ tags: "@skip" },
() => {
H.createQuestion(QUESTION_1, { wrapId: true, idAlias: "questionId1" });
H.createQuestion(QUESTION_2, { wrapId: true, idAlias: "questionId2" });
cy.then(function () {
const { questionId1, questionId2 } = this;
createQ1PlusQ2Question(questionId1, questionId2).then(
({ body: question }) => {
cy.visit(`/question/${question.id}/notebook`);
},
);
});
assertQ1PlusQ2Joins();
},
);
});
const createQ1PlusQ2Question = (questionId1, questionId2) => {
return H.createQuestion({
name: "Q1 + Q2",
query: {
"source-table": `card__${questionId1}`,
joins: [
{
fields: "all",
strategy: "left-join",
alias: "Q2 - Category",
condition: [
"=",
["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
[
"field",
PRODUCTS.CATEGORY,
{ "base-type": "type/Text", "join-alias": "Q2 - Category" },
],
],
"source-table": `card__${questionId2}`,
},
],
},
});
};
const assertQ1PlusQ2Joins = () => {
H.getNotebookStep("join").within(() => {
cy.findAllByTestId("notebook-cell-item").then((items) => {
cy.wrap(items[0]).should("contain", QUESTION_1.name);
cy.wrap(items[1]).should("contain", QUESTION_2.name);
});
cy.findByLabelText("Left column").within(() => {
cy.findByText(QUESTION_1.name).should("exist");
cy.findByText("Category").should("exist");
});
cy.findByLabelText("Right column").within(() => {
cy.findByText(QUESTION_2.name).should("exist");
cy.findByText("Q2 - Category → Category").should("exist");
});
});
};

View File

@@ -681,57 +681,6 @@ describe("issue 40064", () => {
});
});
describe("issue 10493", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.signInAsAdmin();
});
it("should not reset chart axes after adding a new query stage (metabase#10493)", () => {
H.visitQuestionAdhoc({
display: "bar",
dataset_query: {
type: "query",
database: SAMPLE_DB_ID,
query: {
aggregation: [["count"]],
breakout: [
[
"field",
ORDERS.QUANTITY,
{ "base-type": "type/Integer", binning: { strategy: "default" } },
],
],
"source-table": ORDERS_ID,
},
},
});
H.filter();
H.modal().within(() => {
cy.findByText("Summaries").click();
cy.findByTestId("filter-column-Count").within(() => {
cy.findByPlaceholderText("Min").type("0");
cy.findByPlaceholderText("Max").type("30000");
});
cy.button("Apply filters").click();
});
cy.wait("@dataset");
H.echartsContainer().within(() => {
// y axis
cy.findByText("Count").should("exist");
cy.findByText("21,000").should("exist");
cy.findByText("3,000").should("exist");
// x axis
cy.findByText("Quantity").should("exist");
cy.findByText("25").should("exist");
cy.findByText("75").should("exist");
});
});
});
describe("issue 44071", () => {
const questionDetails = {
name: "Test",

View File

@@ -568,41 +568,6 @@ describe("issue 41464", () => {
});
});
describe("issue 45359", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.intercept("/app/fonts/Lato/lato-v16-latin-regular.woff2").as(
"font-regular",
);
cy.intercept("/app/fonts/Lato/lato-v16-latin-700.woff2").as("font-bold");
cy.signInAsAdmin();
});
it("loads app fonts correctly (metabase#45359)", () => {
H.openOrdersTable({ mode: "notebook" });
H.getNotebookStep("data")
.findByText("Orders")
.should("have.css", "font-family", "Lato, Arial, sans-serif");
cy.get("@font-regular.all").should("have.length", 1);
cy.get("@font-regular").should(({ response }) => {
expect(response).to.include({ statusCode: 200 });
});
cy.get("@font-bold.all").should("have.length", 1);
cy.get("@font-bold").should(({ response }) => {
expect(response).to.include({ statusCode: 200 });
});
cy.document()
.then((document) => document.fonts.ready)
.then((fonts) => {
cy.wrap(fonts).invoke("check", "16px Lato").should("be.true");
});
});
});
describe("issue 45452", () => {
beforeEach(() => {
H.restore();

File diff suppressed because it is too large Load Diff

View File

@@ -246,7 +246,6 @@ describe(
{ tags: "@mongo" },
() => {
const MONGO_DB_NAME = "QA Mongo";
const MONGO_DB_ID = 2;
beforeEach(() => {
H.restore("mongo-5");
@@ -315,64 +314,6 @@ describe(
cy.findByTestId("selected-table").should("have.text", "Products");
cy.get("[data-testid=cell-data]").should("contain", "Small Marble Shoes");
});
it(
"should work for a nested GUI question (metabase#40557)",
{ tags: "@skip" },
() => {
H.withDatabase(
MONGO_DB_ID,
({ PRODUCTS_ID }: { PRODUCTS_ID: number }) => {
H.createQuestion({
name: "Mongo Source",
query: {
"source-table": PRODUCTS_ID,
limit: 1,
},
database: MONGO_DB_ID,
}).then(({ body: { id: sourceId } }) => {
H.createQuestion(
{
name: "Mongo Nested",
query: {
"source-table": `card__${sourceId}`,
},
database: MONGO_DB_ID,
},
{ visitQuestion: true },
);
});
},
);
cy.get("[data-testid=cell-data]").should(
"contain",
"Small Marble Shoes",
);
H.openNotebook();
openSidebar("native");
cy.findByTestId("native-query-preview-sidebar").within(() => {
cy.findByText("Native query for this question").should("exist");
H.NativeEditor.get()
.should("be.visible")
.and("contain", "$project")
.and("contain", "$limit")
.and("not.contain", "BsonString")
.and("not.contain", "BsonInt32");
cy.button("Convert this question to a native query").click();
});
cy.log("Database and table should be pre-selected (metabase#40557)");
cy.findByTestId("selected-database").should("have.text", MONGO_DB_NAME);
cy.findByTestId("selected-table").should("have.text", "Products");
cy.get("[data-testid=cell-data]").should(
"contain",
"Small Marble Shoes",
);
},
);
},
);

View File

@@ -700,80 +700,6 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
});
});
it(
"should be possible to sort by metric (metabase#8283,metabase#42392)",
{ tags: "@skip" },
() => {
H.createQuestion(
{
name: "Revenue",
description: "Sum of orders subtotal",
type: "metric",
query: {
"source-table": ORDERS_ID,
aggregation: [["sum", ["field", ORDERS.SUBTOTAL, null]]],
},
},
{
wrapId: true,
idAlias: "metricId",
},
);
cy.get("@metricId").then((metricId) => {
const questionDetails = {
query: {
"source-table": `card__${metricId}`,
aggregation: ["metric", metricId],
},
};
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
cy.findByText("Pick a column to group by").click();
cy.findByText("Created At").click();
cy.findByText("Sort").click();
// Sorts ascending by default
// Revenue appears twice, but it's the only integer column to order by
H.popover().icon("int").click();
// Let's make sure it's possible to sort descending as well
cy.icon("arrow_up").click();
cy.icon("arrow_down").parent().contains("Revenue");
H.visualize();
// Visualization will render line chart by default. Switch to the table.
cy.icon("table2").click();
cy.findAllByRole("grid").as("table");
cy.get("@table")
.first()
.as("tableHeader")
.within(() => {
cy.get("[data-testid=cell-data]")
.eq(1)
.invoke("text")
.should("eq", "Revenue");
});
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.get("@table")
.last()
.as("tableBody")
.within(() => {
cy.get("[data-testid=cell-data]")
.eq(1)
.invoke("text")
.should("eq", "50,072.98");
});
});
},
);
it("should open only one bucketing popover at a time (metabase#45036)", () => {
H.visitQuestionAdhoc(
{

View File

@@ -48,8 +48,6 @@ describe("scenarios > question > offset", () => {
});
describe("custom columns", () => {
// This test contradicts the next 2 skipped ones.
// Remove it once we enable offset() in custom columns.
it("does not suggest or allow using offset()", () => {
const expression = "Offset([Total], -1)";
const prefixLength = 3;
@@ -79,127 +77,6 @@ describe("scenarios > question > offset", () => {
);
});
});
// Skipped because we want to disable offset() in custom columns for now
it("suggests and allows using offset()", { tags: "@skip" }, () => {
const expression = "Offset([Total], -1)";
const prefixLength = 3;
const prefix = expression.substring(0, prefixLength);
const query: StructuredQuery = {
"source-table": ORDERS_ID,
fields: [ORDERS_ID_FIELD_REF, ORDERS_TOTAL_FIELD_REF],
limit: 5,
};
H.createQuestion({ query }, { visitQuestion: true });
H.openNotebook();
cy.button("Custom column").click();
H.enterCustomColumnDetails({ formula: prefix });
cy.log("suggests offset() in custom column expressions");
cy.findByTestId("expression-suggestions-list-item")
.should("exist")
.and("have.text", "Offset");
H.enterCustomColumnDetails({ formula: expression });
cy.realPress("Tab");
H.popover().within(() => {
cy.findByText("OFFSET in a custom expression requires a sort order");
cy.button("Done").should("be.disabled");
cy.button("Cancel").click();
});
cy.button("Sort").click();
H.popover().findByText("ID").click();
H.getNotebookStep("expression").icon("add").click();
H.enterCustomColumnDetails({ formula: expression });
cy.realPress("Tab");
H.popover().within(() => {
cy.button("Done").should("be.disabled");
cy.findByPlaceholderText("Something nice and descriptive")
.type("My expression")
.blur();
cy.button("Done").should("be.enabled").click();
});
cy.log("preview availability");
H.getNotebookStep("data").icon("play").should("be.visible");
H.getNotebookStep("expression").icon("play").should("not.be.visible");
H.getNotebookStep("sort").icon("play").should("be.visible");
H.getNotebookStep("limit").icon("play").should("be.visible");
H.visualize();
verifyTableContent([
["1", "39.72", ""],
["2", "117.03", "39.72"],
["3", "49.21", "117.03"],
]);
});
// Skipped because we want to disable offset() in custom columns for now
it(
"does not allow to use offset-based column in other clauses (metabase#42764)",
{ tags: "@skip" },
() => {
const offsettedColumnName = "xyz";
const expression = `Offset([${offsettedColumnName}], -1)`;
const prefixLength = "Offset([x".length;
const prefix = expression.substring(0, prefixLength);
const query: StructuredQuery = {
"source-table": ORDERS_ID,
expressions: {
[offsettedColumnName]: [
"offset",
createOffsetOptions(offsettedColumnName),
ORDERS_TOTAL_FIELD_REF,
-1,
],
},
"order-by": [["asc", ORDERS_ID_FIELD_REF]],
limit: 5,
};
H.createQuestion({ query }, { visitQuestion: true });
cy.log("custom column drills");
const rowIndex = 1;
const columnIndex = 9;
const columnsCount = 10;
const cellIndex = rowIndex * columnsCount + columnIndex;
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByRole("gridcell").eq(cellIndex).click();
cy.get(H.POPOVER_ELEMENT).should("not.exist");
H.openNotebook();
cy.log("custom column expressions");
H.getNotebookStep("expression").icon("add").click();
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
H.popover().button("Cancel").click();
cy.log("custom filter expressions");
cy.icon("filter").click();
H.popover().findByText("Custom Expression").click();
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
H.popover().button("Cancel").click();
cy.realPress("Escape");
cy.log("custom aggregation expressions");
cy.icon("sum").click();
H.popover().findByText("Custom Expression").click();
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
H.popover().button("Cancel").click();
cy.realPress("Escape");
cy.log("sort clause");
H.getNotebookStep("sort").icon("add").click();
H.popover().should("not.contain", offsettedColumnName);
},
);
});
describe("filters", () => {
@@ -530,22 +407,6 @@ function verifyNoQuestionError() {
});
}
function verifyInvalidColumnName(
columnName: string,
prefix: string,
expression: string,
) {
H.enterCustomColumnDetails({ formula: prefix });
cy.findByTestId("expression-suggestions-list-item").should("not.exist");
H.enterCustomColumnDetails({ formula: expression });
cy.realPress("Tab");
H.popover().within(() => {
cy.findByText(`Unknown Field: ${columnName}`).should("be.visible");
cy.button("Done").should("be.disabled");
});
}
function createOffsetOptions(name = "offset") {
return {
"lib/uuid": uuid(),

View File

@@ -652,35 +652,5 @@ describe(
H.modal().findByText(secondWebhookName).should("be.visible");
});
// There is no api to test individual hooks for new Question Alerts
it("should allow you to test a webhook", { tags: "@skip" }, () => {
cy.intercept("POST", "/api/pulse/test").as("testAlert");
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
cy.findByLabelText("Move, trash, and more…").click();
H.popover().findByText("Create an alert").click();
H.modal().within(() => {
H.getAlertChannel(firstWebhookName).scrollIntoView();
H.getAlertChannel(firstWebhookName)
.findByRole("checkbox")
.click({ force: true });
H.getAlertChannel(firstWebhookName).button("Send a test").click();
});
cy.wait("@testAlert");
cy.request(
`${H.WEBHOOK_TEST_HOST}/api/session/${H.WEBHOOK_TEST_SESSION_ID}/requests`,
).then(({ body }) => {
expect(body).to.have.length(1);
cy.wrap(atob(body[0].content_base64))
.should("have.string", "alert_creator_name")
.and("have.string", "Bobby Tables");
});
});
},
);

View File

@@ -429,128 +429,6 @@ describe("scenarios > question > settings", () => {
cy.findByLabelText("close icon").should("not.exist");
});
});
it(
"should allow hiding and showing aggregated columns with a post-aggregation custom column (metabase#22563)",
{ tags: "@skip" },
() => {
// products joined to orders with breakouts on 3 product columns followed by a custom column
H.createQuestion(
{
name: "repro 22563",
query: {
"source-query": {
"source-table": ORDERS_ID,
joins: [
{
alias: "Products",
condition: [
"=",
["field", ORDERS.PRODUCT_ID, null],
[
"field",
PRODUCTS.ID,
{
"join-alias": "Products",
},
],
],
"source-table": PRODUCTS_ID,
},
],
aggregation: [["count"]],
breakout: [
[
"field",
PRODUCTS.CATEGORY,
{
"base-type": "type/Text",
"join-alias": "Products",
},
],
[
"field",
PRODUCTS.TITLE,
{
"base-type": "type/Text",
"join-alias": "Products",
},
],
[
"field",
PRODUCTS.VENDOR,
{
"base-type": "type/Text",
"join-alias": "Products",
},
],
],
},
expressions: {
two: ["+", 1, 1],
},
},
},
{ visitQuestion: true },
);
const columnNames = [
"Products → Category",
"Products → Title",
"Products → Vendor",
"Count",
"two",
];
H.tableInteractive().within(() => {
columnNames.forEach((text) =>
cy.findByText(text).should("be.visible"),
);
});
H.openVizSettingsSidebar();
cy.findByTestId("chartsettings-sidebar").within(() => {
columnNames.forEach((text) =>
cy.findByText(text).should("be.visible"),
);
cy.findByText("More Columns").should("not.exist");
cy.icon("eye_outline").first().click();
cy.findByText("More columns").should("be.visible");
// disable the first column
cy.findByTestId("disabled-columns")
.findByText("Products → Category")
.should("be.visible");
cy.findByTestId("visible-columns")
.findByText("Products → Category")
.should("not.exist");
});
H.tableInteractive().within(() => {
// the query should not have changed
cy.icon("play").should("not.exist");
cy.findByText("Products → Category").should("not.exist");
});
cy.findByTestId("chartsettings-sidebar").within(() => {
cy.icon("add").click();
// re-enable the first column
cy.findByText("More columns").should("not.exist");
cy.findByTestId("visible-columns")
.findByText("Products → Category")
.should("be.visible");
});
H.tableInteractive().within(() => {
// the query should not have changed
cy.icon("play").should("not.exist");
cy.findByText("Products → Category").should("be.visible");
});
},
);
});
describe("resetting state", () => {

View File

@@ -253,25 +253,6 @@ describe("scenarios > question > summarize sidebar", () => {
cy.findAllByTestId("header-cell").should("have.length", 2);
cy.get("[data-testid=cell-data]").should("contain", 744); // `Count` for year 2025
});
// flaky test (#19454)
it(
"should show an info popover when hovering over summarize dimension options",
{ tags: "@skip" },
() => {
H.openReviewsTable();
H.summarize();
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Group by")
.parent()
.findByText("Title")
.trigger("mouseenter");
H.popover().contains("Title");
H.popover().contains("199 distinct values");
},
);
});
function removeMetricFromSidebar(metricName) {

View File

@@ -5,7 +5,7 @@ import {
ORDERS_QUESTION_ID,
} from "e2e/support/cypress_sample_instance_data";
const { ORDERS_ID, PEOPLE_ID, REVIEWS_ID } = SAMPLE_DATABASE;
const { ORDERS_ID, PEOPLE_ID } = SAMPLE_DATABASE;
const visitEmbeddingWithSearch = (url = "/") => {
H.visitFullAppEmbeddingUrl({
@@ -249,27 +249,6 @@ describe("scenarios > search", () => {
});
});
describe("issue 16785", { tags: "@skip" }, () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
cy.request("PUT", "/api/table", {
ids: [REVIEWS_ID],
visibility_type: "hidden",
});
});
it("should not display hidden tables (metabase#16785)", () => {
cy.visit("/");
cy.findByPlaceholderText("Search…").type("Reviews");
cy.findByTestId("search-results-list").within(() => {
cy.findByText("Reviews").should("not.exist");
});
});
});
describe("issue 28788", () => {
const LONG_STRING = "01234567890ABCDEFGHIJKLMNOPQRSTUVXYZ0123456789";

View File

@@ -167,20 +167,6 @@ describe("scenarios > dashboard > subscriptions", () => {
H.popover().isRenderedWithinViewport();
});
it(
"should not send attachments by default if not explicitly selected (metabase#28673)",
{ tags: "@skip" },
() => {
openDashboardSubscriptions();
assignRecipient();
cy.findByLabelText("Attach results").should("not.be.checked");
H.sendEmailAndAssert(
({ attachments }) => expect(attachments).to.be.empty,
);
},
);
});
describe("with existing subscriptions", () => {

View File

@@ -824,217 +824,6 @@ describe("scenarios > visualizations > bar chart", () => {
H.echartsTriggerBlur();
});
it(
"should allow grouping series into a single 'Other' series",
{ tags: "@skip" },
() => {
const AK_SERIES_COLOR = "#509EE3";
const USER_STATE_FIELD_REF = [
"field",
PEOPLE.STATE,
{ "source-field": ORDERS.USER_ID },
];
const ORDER_CREATED_AT_FIELD_REF = [
"field",
ORDERS.CREATED_AT,
{ "temporal-unit": "month" },
];
function setMaxCategories(value, { viaBreakoutSettings = false } = {}) {
if (viaBreakoutSettings) {
H.leftSidebar().findByTestId("settings-STATE").click();
} else {
H.leftSidebar().findByLabelText("Other series settings").click();
}
H.popover()
.findByTestId("graph-max-categories-input")
.type(`{selectAll}${value}`)
.blur();
cy.wait(500); // wait for viz to re-render
}
function setOtherCategoryAggregationFn(fnName) {
H.leftSidebar().findByLabelText("Other series settings").click();
H.popover()
.findByTestId("graph-other-category-aggregation-fn-picker")
.click();
// eslint-disable-next-line metabase/no-unsafe-element-filtering
H.popover().last().findByText(fnName).click();
}
H.visitQuestionAdhoc({
display: "bar",
dataset_query: {
type: "query",
database: SAMPLE_DB_ID,
query: {
"source-table": ORDERS_ID,
aggregation: [["count"]],
breakout: [USER_STATE_FIELD_REF, ORDER_CREATED_AT_FIELD_REF],
filter: [
"and",
[
"between",
ORDER_CREATED_AT_FIELD_REF,
"2025-09-01T00:00Z",
"2026-02-01T00:00Z",
],
[
"=",
USER_STATE_FIELD_REF,
"AK",
"AL",
"AR",
"AZ",
"CA",
"CO",
"CT",
"DE",
"FL",
"GA",
"IA",
"ID",
"IL",
"KY",
],
],
},
},
});
// Enable 'Other' series
H.openVizSettingsSidebar();
H.leftSidebar().findByTestId("settings-STATE").click();
H.popover().findByLabelText("Enforce maximum number of series").click();
// Test 'Other' series renders
H.otherSeriesChartPaths().should("have.length", 6);
// Test drill-through is disabled for 'Other' series
H.otherSeriesChartPaths().first().click();
cy.findByTestId("click-actions-view").should("not.exist");
// Test drill-through is enabled for regular series
H.chartPathWithFillColor(AK_SERIES_COLOR).first().click();
cy.findByTestId("click-actions-view").should("exist");
// Test legend and series visibility toggling
H.queryBuilderMain()
.findAllByTestId("legend-item")
.should("have.length", 9)
.last()
.as("other-series-legend-item");
cy.get("@other-series-legend-item")
.findByLabelText("Hide series")
.click();
H.otherSeriesChartPaths().should("have.length", 0);
cy.get("@other-series-legend-item")
.findByLabelText("Show series")
.click();
H.otherSeriesChartPaths().should("have.length", 6);
// Test tooltips
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "9" }] });
H.otherSeriesChartPaths().first().realHover();
H.assertEChartsTooltip({
header: "September 2025",
rows: [
{ name: "IA", value: "3" },
{ name: "KY", value: "2" },
{ name: "FL", value: "1" },
{ name: "GA", value: "1" },
{ name: "ID", value: "1" },
{ name: "IL", value: "1" },
{ name: "Total", value: "9" },
],
});
// Test "graph.max_categories" change
setMaxCategories(4);
H.queryBuilderMain().click(); // close popover
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.echartsTooltip().find("tr").should("have.length", 5);
H.queryBuilderMain()
.findAllByTestId("legend-item")
.should("have.length", 5);
// Test can move series in/out of "Other" series
H.getDraggableElements().eq(3).as("AZ");
H.moveDnDKitElementByAlias("@AZ", { vertical: 150 }); // Move AZ into "Other"
H.getDraggableElements().eq(6).as("CT");
H.moveDnDKitElementByAlias("@CT", { vertical: -150 }); // Move CT out of "Other"
H.queryBuilderMain()
.findAllByTestId("legend-item")
.should("have.length", 5);
H.queryBuilderMain()
.findAllByTestId("legend-item")
.contains("AZ")
.should("not.exist");
H.queryBuilderMain()
.findAllByTestId("legend-item")
.contains("CT")
.should("exist");
// Test "graph.max_categories" removes "Other" altogether
setMaxCategories(0);
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.echartsTooltip().find("tr").should("have.length", 14);
H.queryBuilderMain()
.findAllByTestId("legend-item")
.should("have.length", 14);
H.otherSeriesChartPaths().should("not.exist");
setMaxCategories(8, { viaBreakoutSettings: true });
// Test "graph.other_category_aggregation_fn" for native queries
H.openNotebook();
H.queryBuilderHeader().findByLabelText("View SQL").click();
cy.findByTestId("native-query-preview-sidebar")
.button("Convert this question to SQL")
.click();
cy.wait("@dataset");
H.queryBuilderMain().findByTestId("visibility-toggler").click();
H.openVizSettingsSidebar();
setOtherCategoryAggregationFn("Average");
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "1.5" }] });
H.otherSeriesChartPaths().first().realHover();
H.assertEChartsTooltip({
header: "September 2025",
rows: [
{ name: "IA", value: "3" },
{ name: "KY", value: "2" },
{ name: "FL", value: "1" },
{ name: "GA", value: "1" },
{ name: "ID", value: "1" },
{ name: "IL", value: "1" },
{ name: "Average", value: "1.5" },
],
});
setOtherCategoryAggregationFn("Min");
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "1" }] });
H.otherSeriesChartPaths().first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Min", value: "1" }] });
setOtherCategoryAggregationFn("Max");
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "3" }] });
H.otherSeriesChartPaths().first().realHover();
H.assertEChartsTooltip({ rows: [{ name: "Max", value: "3" }] });
},
);
it("should format goal tooltip value as a percent when the Stacking option is 'Stack - 100%'", () => {
H.visitQuestionAdhoc({
...breakoutBarChart,

View File

@@ -611,103 +611,6 @@ describe("scenarios > visualizations > line/bar chart > tooltips", () => {
});
});
// TODO: Fix series have different colors in visualizer because of different
// column names in original dashcard and visualizer ("avg" becomes "COLUMN_2" and the color has is different)
it(
"should show updated column titles in tooltips after editing them via Visualization Options",
{ tags: "@skip" },
() => {
// Checking the second datum since the first circle of one series is covered with a circle from the other series
const circleIndex = 1;
const originalSeriesColors = ["#A989C5", "#88BF4D"];
const addedSeriesColors = ["#509EE3", "#98D9D9"];
const originalAvgSeriesName = "Q1: Average of Total";
const originalCumSumSeriesName = "Q1: Cumulative sum of Quantity";
const updatedOriginalAvgSeriesName = "Q1 Custom 1";
const updatedOriginalCumSumSeriesName = "Q1 Custom 2";
const addedAvgSeriesName = "Q2: Average of Discount";
const addedSumSeriesName = "Q2: Sum of Discount";
const updatedAddedAvgSeriesName = "Q2 Custom 1";
const updatedAddedSumSeriesName = "Q2 Custom 2";
[...originalSeriesColors, ...addedSeriesColors].forEach((color) => {
showTooltipForCircleInSeries(color, circleIndex);
H.assertEChartsTooltip({
header: "2026",
rows: [
{
color: originalSeriesColors[0],
name: originalAvgSeriesName,
value: "56.86",
},
{
color: originalSeriesColors[1],
name: originalCumSumSeriesName,
value: "17,587",
},
{
color: addedSeriesColors[0],
name: addedAvgSeriesName,
value: "5.41",
},
{
color: addedSeriesColors[1],
name: addedSumSeriesName,
value: "1,953.08",
},
],
});
});
H.editDashboard();
H.showDashcardVisualizerModalSettings(0, {
isVisualizerCard: false,
});
updateColumnTitle(originalAvgSeriesName, updatedOriginalAvgSeriesName);
updateColumnTitle(
originalCumSumSeriesName,
updatedOriginalCumSumSeriesName,
);
updateColumnTitle(addedAvgSeriesName, updatedAddedAvgSeriesName);
updateColumnTitle(addedSumSeriesName, updatedAddedSumSeriesName);
H.saveDashcardVisualizerModalSettings();
H.saveDashboard();
[...originalSeriesColors, ...addedSeriesColors].forEach((color) => {
showTooltipForCircleInSeries(color, circleIndex);
H.assertEChartsTooltip({
header: "2026",
rows: [
{
color: originalSeriesColors[0],
name: updatedOriginalAvgSeriesName,
value: "56.86",
},
{
color: originalSeriesColors[1],
name: updatedOriginalCumSumSeriesName,
value: "17,587",
},
{
color: addedSeriesColors[0],
name: updatedAddedAvgSeriesName,
value: "5.41",
},
{
color: addedSeriesColors[1],
name: updatedAddedSumSeriesName,
value: "1,953.08",
},
],
});
});
},
);
it("should show percent change in tooltip for timeseries axis", () => {
testAvgTotalChange(showTooltipForCircleInSeries, "Q1: Average of Total");
testCumSumChange(false, "Q1: Cumulative sum of Quantity");

View File

@@ -635,53 +635,6 @@ describe("issue 21665", () => {
});
});
describe("issue 22527", { tags: "@skip" }, () => {
const questionDetails = {
native: {
query:
"select 1 x, 1 y, 20 size\nunion all select 2 x, 10 y, 10 size\nunion all select 3 x, -9 y, 6 size\nunion all select 4 x, 100 y, 30 size\nunion all select 5 x, -20 y, 70 size",
},
display: "scatter",
visualization_settings: {
"graph.dimensions": ["X"],
"graph.metrics": ["Y"],
},
};
function assertion() {
cy.get("circle").should("have.length", 5).last().realHover();
H.popover().within(() => {
H.testPairedTooltipValues("X", "5");
H.testPairedTooltipValues("Y", "-20");
H.testPairedTooltipValues("SIZE", "70");
});
}
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.createNativeQuestion(questionDetails, { visitQuestion: true });
});
it("should render negative values in a scatter visualziation (metabase#22527)", () => {
assertion();
H.openVizSettingsSidebar();
cy.findByTestId("sidebar-left").within(() => {
cy.findByTextEnsureVisible("Data").click();
});
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
cy.findByText("Bubble size").parent().contains("Select a field").click();
H.popover().contains(/size/i).click();
assertion();
});
});
describe("issue 25156", () => {
const questionDetails = {
name: "25156",

View File

@@ -652,17 +652,27 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
},
});
H.pieSlices()
.filter("[fill^='hsla']")
.then(($slice) => {
const { height } = $slice[0].getBoundingClientRect();
const otherPieSlice = () => H.pieSlices().filter("[fill^='hsla']");
cy.wrap($slice).realClick({
x: 30,
y: height / 2,
scrollBehavior: false,
});
});
H.pieSlices().should("have.length", 3);
otherPieSlice().then(($slice) => {
const { height } = $slice[0].getBoundingClientRect();
cy.wrap($slice).trigger("mousemove", 30, height / 2);
});
H.assertEChartsTooltip({
header: "Category",
rows: [
{ name: "Doohickey", value: "42" },
{ name: "Gizmo", value: "51" },
],
});
otherPieSlice().then(($slice) => {
const { height } = $slice[0].getBoundingClientRect();
cy.wrap($slice).click(30, height / 2, { force: true });
});
H.popover().within(() => {
cy.findByTestId("click-actions-view").within(() => {

View File

@@ -3,6 +3,7 @@
[clojure.spec.alpha :as s]
[metabase-enterprise.advanced-config.file.interface
:as advanced-config.file.i]
[metabase.auth-identity.core :as auth-identity]
[metabase.setup.core :as setup]
[metabase.users.models.user :as user]
[metabase.util :as u]
@@ -37,18 +38,26 @@
(defn- init-from-config-file!
[user]
(if-let [existing-user (select-user (:email user))]
(do
(log/info (u/format-color :blue "Updating User %d" (:id existing-user)))
(let [new-user (update user :login_attributes
#(merge % (:login_attributes existing-user)))]
(t2/update! :model/User (:id existing-user) new-user)))
;; create a new user. If they are the first non-internal User, force them to be an admin.
(let [user (cond-> user
(not (setup/has-user-setup)) (assoc :is_superuser true))]
(log/info (u/colorize :green "Creating the first User for this instance. The first user is always created as an admin."))
(log/info (u/colorize :green "Creating new User"))
(t2/insert! :model/User user))))
;; the profile write and the password write must land together — initialize! runs no transaction of its own
(t2/with-transaction [_]
(let [password (:password user)
;; the password is stored only via set-password!; never hand it to the User model
user (dissoc user :password)
user-id (if-let [existing-user (select-user (:email user))]
(do
(log/info (u/format-color :blue "Updating User %d" (:id existing-user)))
(let [new-user (update user :login_attributes
#(merge % (:login_attributes existing-user)))]
(t2/update! :model/User (:id existing-user) new-user))
(:id existing-user))
;; create a new user. If they are the first non-internal User, force them to be an admin.
(let [user (cond-> user
(not (setup/has-user-setup)) (assoc :is_superuser true))]
(log/info (u/colorize :green "Creating the first User for this instance. The first user is always created as an admin."))
(log/info (u/colorize :green "Creating new User"))
(u/the-id (t2/insert-returning-instance! :model/User user))))]
;; passwords live in the AuthIdentity, not on the User row
(auth-identity/set-password! user-id password))))
(defmethod advanced-config.file.i/initialize-section! :users
[_section-name users]

View File

@@ -22,6 +22,7 @@
[metabase.setup.core :as setup]
[metabase.startup.core :as startup]
[metabase.sync.core :as sync]
[metabase.util.i18n :refer [tru]]
[metabase.util.log :as log]
[toucan2.core :as t2])
(:import
@@ -63,6 +64,11 @@
Returns the created database map."
[user-id]
;; An analytics-dev database is a handle onto the app-db and must not exist outside analytics dev mode; refuse to
;; create one unless the mode is on (mirrors the connection-layer guard in
;; [[metabase.driver.sql-jdbc.connection/db->pooled-connection-spec]]).
(when-not (audit/analytics-dev-mode)
(throw (ex-info (tru "Analytics dev mode is not enabled.") {:status-code 400})))
(let [db-type (mdb/db-type)]
(if-let [existing (find-analytics-dev-database)]
(do

View File

@@ -32,7 +32,6 @@
{:email (sag.settings/support-access-grant-email)
:first_name (sag.settings/support-access-grant-first-name)
:last_name (sag.settings/support-access-grant-last-name)
:password (str (random-uuid))
:is_superuser true})))
(methodical/defmethod t2/batched-hydrate [:model/SupportAccessGrantLog :user_info]

View File

@@ -105,7 +105,5 @@
(:id user) grant-ends-at)
(t2/with-transaction [_]
(t2/update! :model/AuthIdentity (:id auth-identity) (auth-identity/mark-token-consumed auth-identity))
(t2/update! :model/AuthIdentity :user_id (:id user) :provider "password"
{:expires_at grant-ends-at
:credentials {:plaintext_password password}}))))
(auth-identity/set-password! (:id user) password {:expires-at grant-ends-at}))))
result)

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