Compare commits

..

2 Commits

Author SHA1 Message Date
Stas Gavrylov
6ecffd0c9b Regenerate SDK host app lockfiles with npm 11 2026-08-25 08:44:05 -07:00
Stas Gavrylov
0a96d3d9dd Upgrade Node.js to 24 LTS 2026-08-25 07:29:23 -07:00
426 changed files with 11132 additions and 8906 deletions

View File

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

View File

@@ -27,11 +27,12 @@
: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 10
:metabase/validate-deftest 9
: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,10 +18,6 @@ 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
@@ -32,21 +28,18 @@ runs:
echo '```'
grep -h '<testsuite' target/junit/*.xml 2>/dev/null \
| sed -E 's|.*<testsuite name="([^"]*)" time="([^"]*)".*|\2 \1|' \
| sort -rn | awk 'NR<=25' || true
| sort -rn | head -25
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 | awk 'NR<=25' || true
| sort -rn | head -25
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
@@ -56,6 +49,5 @@ 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 "🤖 v${TARGET_VERSION} 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 "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
if [ "$CONFLICTS" -gt 0 ]; then
PR_ARGS+=(--draft)
fi

View File

@@ -955,44 +955,17 @@ jobs:
test-args: >-
:only-tags [:mb/upload-tests]
:fail-fast? true
# `: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
- name: Redshift (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 EE (Excluding Upload Tests) Part 2
- name: Redshift (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"
--expose grepTags="-@mongo+-@python+-@OSS+-@skip"
# 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

@@ -35,7 +35,7 @@ jobs:
sparse-checkout-cone-mode: false
- uses: actions/setup-node@v6.4.0
with:
node-version: lts/Jod # 22.x.x
node-version: lts/Krypton # 24.x.x
- name: Build e2e matrix
uses: actions/github-script@v9
id: matrix

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" \
--expose grepTags="-@mongo+-@python+-@OSS+-@skip" \
--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 || '-@mongo+-@python+-@OSS' }}" \
--expose grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@mongo+-@python+-@OSS+-@skip'}}" \
--spec "$SPEC_PATH"
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true

View File

@@ -1,137 +0,0 @@
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,8 +1,6 @@
name: Update Release Log
run-name: Update Release Log ${{ inputs.version || github.ref_name }}
# some other changes
on:
push:
branches:

View File

@@ -133,7 +133,7 @@ jobs:
id: claude
uses: anthropics/claude-code-base-action@e8132bc5e637a42c27763fc757faa37e1ee43b34 # v0.0.63
env:
NODE_VERSION: "22"
NODE_VERSION: "24"
with:
# TODO: get a dedicated API key, sharing repro bot's key for now
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_BACKPORT_FIX }}

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6.4.0
with:
node-version: 22
node-version: 24
- name: Harvest and write AI context to Crowdin
timeout-minutes: 60
env:

View File

@@ -143,7 +143,7 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
with:
node-version: "${{ needs.get-build-requirements.outputs.node_version || '22' }}"
node-version: "${{ needs.get-build-requirements.outputs.node_version || '24' }}"
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:

View File

@@ -38,7 +38,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 22
node-version: 24
- name: Collect and average timing data
id: collect-timings

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: 75 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.

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

Binary file not shown.

Before

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: 113 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 413 KiB

2
.nvmrc
View File

@@ -1 +1 @@
v22.23.2
v24.19.0

View File

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

View File

@@ -6,13 +6,6 @@ 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:
@@ -25,42 +18,12 @@ 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

@@ -26,7 +26,7 @@ Building a custom viz from scaffolding to adding it to your Metabase looks somet
## Prerequisites
- Node.js 22 or newer.
- Node.js 24 or newer.
- Familiarity with React and TypeScript.
- A Metabase on a [Pro or Enterprise plan](https://www.metabase.com/pricing/) to load your plugin into.

View File

@@ -30,9 +30,6 @@ 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:
@@ -61,14 +58,11 @@ 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.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.
- `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-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.
- 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,7 +122,6 @@ type MetabaseColorKey =
| "background_surface-warning-strong"
| "border-brand"
| "border-filter"
| "border-highlight"
| "border-neutral"
| "border-neutral-strong"
| "border-neutral-strongest"

View File

@@ -133,7 +133,6 @@ 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

@@ -18,6 +18,16 @@
"@types/react-dom": "^18"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"license": "MIT",
@@ -46,6 +56,28 @@
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"cpu": [
@@ -60,6 +92,450 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@next/env": {
"version": "15.5.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",

View File

@@ -19,6 +19,16 @@
"@types/react-dom": "^18"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"license": "MIT",
@@ -47,6 +57,28 @@
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"cpu": [
@@ -61,6 +93,450 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@metabase/embedding-sdk-react": {
"version": "0.58.0",
"resolved": "https://registry.npmjs.org/@metabase/embedding-sdk-react/-/embedding-sdk-react-0.58.0.tgz",

View File

@@ -40,10 +40,6 @@ 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.
@@ -242,11 +238,7 @@ const defaultConfig = {
loader: {
".svg": "text",
},
plugins: [
NodeModulesPolyfillPlugin(),
assetsResolverPlugin,
sideEffectFreeModulesPlugin,
],
plugins: [NodeModulesPolyfillPlugin(), assetsResolverPlugin],
sourcemap: "inline",
}),
);

View File

@@ -3,7 +3,7 @@ export const cacheStrategySidesheet = () =>
export const cacheStrategySelect = () =>
cy
.findByRole("form", { name: "Cache invalidation policy" })
.findByRole("form", { name: "Select the cache invalidation policy" })
.findByTestId("cache-strategy-select");
/** Open the strategy dropdown and pick an option by its title (e.g. /Duration/).
@@ -13,8 +13,3 @@ 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,8 +589,7 @@ function getSegmentEditorNameInput() {
}
function getSegmentEditorDescriptionInput() {
getSegmentEditor().findByLabelText("Give it a description").click();
return getSegmentEditor().findByPlaceholderText("Only if it really needs it");
return getSegmentEditor().findByLabelText("Give it a description");
}
function getSegmentEditorFilterPlaceholder() {
@@ -682,8 +681,7 @@ function getMeasureEditorNameInput() {
}
function getMeasureEditorDescriptionInput() {
getMeasureEditor().findByLabelText("Give it a description").click();
return getMeasureEditor().findByPlaceholderText("Only if it really needs it");
return getMeasureEditor().findByLabelText("Give it a description");
}
function getMeasureEditorAggregationPlaceholder() {
@@ -807,7 +805,7 @@ function verifyTablePreview({
});
if (description != null) {
hoverHeaderCell();
cy.findByTestId("header-cell").realHover();
}
});
@@ -816,26 +814,6 @@ 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,14 +16,8 @@ export const DependencyGraph = {
};
export const DependencyDiagnostics = {
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");
},
visitBrokenDependencies: () =>
cy.visit("/monitor/dependency-diagnostics/broken"),
visitUnreferencedEntities: () => {
cy.intercept("GET", "/api/ee/dependencies/graph/unreferenced*").as(
"unreferencedEntities",
@@ -129,7 +123,7 @@ export function waitForBreakingDependencies(
return cy
.request<ListBreakingGraphNodesResponse>(
"GET",
"/api/ee/dependencies/graph/breaking?include-personal-collections=true",
"/api/ee/dependencies/graph/breaking",
)
.then((response) => {
if (filter(response.body.data)) {

View File

@@ -8,11 +8,12 @@ import {
NORMAL_USER_ID,
} from "e2e/support/cypress_sample_instance_data";
const { normal, admin, nocollection } = USERS;
const { sandboxed, normal, admin, nodata, 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_ID } = SAMPLE_DATABASE;
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
const { COLLECTION_GROUP } = USER_GROUPS;
const TEST_USER = {
first_name: "Testy",
@@ -25,6 +26,8 @@ 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");
@@ -889,6 +892,77 @@ 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,33 +256,21 @@ describe("issue 45890", () => {
cy.visit("/admin/performance/databases");
H.main()
.findByLabelText(/Edit policy for database 'Sample Database'/)
.findByText("No caching")
.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();
// The strategy picker is a dropdown; its options render in a portal.
H.main().findByTestId("cache-strategy-select").click();
cy.findByRole("option", { name: /Schedule/ }).click();
cy.intercept("PUT", "/api/cache").as("putCacheConfig");
cy.findByTestId("cache-policy-panel").button("Save").click();
cy.wait("@putCacheConfig");
H.main().button("Save changes").click();
});
it("should correctly reset caching schedule form when discarding changes", () => {
cy.findByTestId("cache-policy-panel").findByLabelText("Frequency").click();
H.main().findByLabelText("Frequency").click();
H.popover().findByText("weekly").click();
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");
H.main().button("Discard changes").click();
H.main().findByLabelText("Frequency").should("have.value", "hourly");
});
});

View File

@@ -9,8 +9,15 @@ 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, REVIEWS, REVIEWS_ID, PRODUCTS_ID } =
SAMPLE_DATABASE;
const {
ORDERS,
ORDERS_ID,
PEOPLE_ID,
PRODUCTS,
REVIEWS,
REVIEWS_ID,
PRODUCTS_ID,
} = SAMPLE_DATABASE;
const { ALL_USERS_GROUP } = USER_GROUPS;
const { FieldSection, PreviewSection, TablePicker, TableSection } = H.DataModel;
@@ -416,6 +423,54 @@ 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,8 +191,6 @@ 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();
@@ -201,10 +199,8 @@ 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: "Cache invalidation policy" })
cy.findByRole("form", { name: "Select the 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 -> row-label mapping, policy-options counts,
* strategy -> launcher-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,10 +72,6 @@ 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");
});
@@ -99,7 +95,6 @@ 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/,
@@ -114,7 +109,7 @@ describe("scenarios > admin > performance > caching", () => {
cy.button(/Clear cache for this database/).click();
cy.log("Confirm in the dialog");
cy.findByTestId("confirm-modal")
cy.findByRole("dialog")
.button(/Clear cache/)
.click();
cy.wait("@invalidateCacheForSampleDatabase");
@@ -127,8 +122,6 @@ 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();
@@ -223,10 +216,6 @@ 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,6 +16,40 @@ 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,6 +164,32 @@ 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,6 +6,42 @@ 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";
@@ -45,6 +81,42 @@ 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";
@@ -414,10 +486,7 @@ describe("issue 20229", () => {
}
function unselectColumn(column) {
cy.findByText(column)
.siblings()
.find('input[type="checkbox"]')
.click({ force: true });
cy.findByText(column).siblings().find(".Icon-check").click({ force: true });
}
beforeEach(() => {
@@ -602,6 +671,78 @@ 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";
@@ -649,6 +790,140 @@ describe("issue 24922", () => {
});
});
});
// 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(() => {
@@ -760,6 +1035,23 @@ 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,6 +473,78 @@ 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();
@@ -513,6 +585,27 @@ 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,6 +431,14 @@ 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");
@@ -470,6 +478,37 @@ 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,6 +302,49 @@ 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,6 +1934,102 @@ 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) => {
@@ -2150,9 +2246,7 @@ describe("issue 64138", () => {
})
.within(() => {
cy.findByLabelText("Zoom in").should("not.exist");
// 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.findByText("Set as default view").should("be.visible").click();
});
cy.log("hovering marker icons should not open their tooltips");

View File

@@ -1,99 +0,0 @@
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

@@ -1,159 +0,0 @@
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,6 +1535,70 @@ 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 }) => {
// 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" });
H.visitDashboard(dashboard.id);
H.waitForDashcardsToLoad({ count: 4 });
// 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.getDashboardCard(0)
.findByRole("link", { name: "Line chart" })
@@ -519,58 +519,35 @@ describe("scenarios > dashboard > title drill", () => {
.findByRole("link", { name: "Funnel chart" })
.as("funnel-chart-title");
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", "#");
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`,
});
cy.log("focusing or hovering a title turns it into a question anchor");
titles.forEach(assertTitleBecomesQuestionAnchor);
});
});
function focusTitle(element) {
element.focus();
function assertTitleHrefOnFocus({ elementAlias, href }) {
cy.get(elementAlias).should("have.attr", "href", "#");
cy.get(elementAlias).focus();
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);
});
function assertTitleHrefOnHover({ elementAlias, href }) {
cy.get(elementAlias).should("have.attr", "href", "#");
cy.get(elementAlias).realHover();
cy.get(elementAlias).should("have.attr", "href", href);
}
});

View File

@@ -0,0 +1,78 @@
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,20 +328,8 @@ describe("bulk table operations", { viewportWidth: 1600 }, () => {
cy.log(
"Expand the rows up front - we'll need them later for the assertion",
);
// 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();
TablePicker.getDatabase("Writable Postgres12").should("be.visible").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,7 +407,6 @@ 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 { DependencyNode, TransformId } from "metabase-types/api";
import type { TransformId } from "metabase-types/api";
const { H } = cy;
@@ -505,24 +505,11 @@ function createModelContent() {
}
function waitForBreakingDependencies() {
// 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),
),
H.waitForBreakingDependencies(
(nodes) => nodes.length >= BROKEN_DEPENDENCIES.length,
);
}
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,6 +6,178 @@ 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",
@@ -831,6 +1003,45 @@ 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,6 +106,30 @@ 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();
@@ -169,6 +193,38 @@ 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,21 +10,6 @@ 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();
@@ -175,7 +160,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
H.createQuestionAndDashboard({
questionDetails: {
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
query: { "source-table": ORDERS_ID, limit: 5 },
},
}).then(({ body: { dashboard_id } }) => {
H.visitCustomHtmlPage(`
@@ -184,10 +169,11 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
<metabase-dashboard dashboard-id="${dashboard_id}" drills />
`);
cy.wait("@getDashCardQuery");
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
H.getSimpleEmbedIframeContent()
.findAllByText("37.65")
.first()
.should("be.visible")
.click();
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)
.should("be.visible");
@@ -198,7 +184,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
H.createQuestionAndDashboard({
questionDetails: {
name: "Limited Orders",
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
query: { "source-table": ORDERS_ID, limit: 5 },
},
}).then(({ body: { dashboard_id } }) => {
H.visitCustomHtmlPage(`
@@ -209,8 +195,11 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getDashCardQuery");
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("not.exist");
@@ -305,7 +294,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: LIMITED_ORDERS_ROW_COUNT },
query: { "source-table": ORDERS_ID, limit: 5 },
}).then(({ body: { id: questionId } }) => {
H.visitCustomHtmlPage(`
${H.getNewEmbedScriptTag()}
@@ -315,8 +304,17 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getCardQuery");
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
// 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()
.findAllByText("37.65")
.first()
.should("be.visible")
.click({ force: true });
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)
.should("be.visible");
@@ -326,7 +324,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: LIMITED_ORDERS_ROW_COUNT },
query: { "source-table": ORDERS_ID, limit: 5 },
}).then(({ body: { id: questionId } }) => {
H.visitCustomHtmlPage(`
${H.getNewEmbedScriptTag()}
@@ -336,7 +334,16 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
cy.wait("@getCardQuery");
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
// 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 });
H.getSimpleEmbedIframeContent()
.findByText(/Filter by this value/)

View File

@@ -335,19 +335,20 @@ describe("scenarios > embedding > sdk iframe embed options passthrough", () => {
frame.within(() => {
cy.findByText("Pick your starting data").should("be.visible");
H.popover().findByText("Orders").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();
H.popover().within(() => {
cy.findByText("Orders").click();
});
cy.findByRole("button", { name: "Visualize" }).click();
cy.log("1. clicking on the filter should drill down");
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("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.findAllByText("29.8").first().should("be.visible");
cy.findByTestId("interactive-question-result-toolbar").should(
"be.visible",

View File

@@ -414,6 +414,67 @@ 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,6 +1266,63 @@ 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,6 +357,57 @@ 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,6 +466,35 @@ 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,7 +5,10 @@ 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 { turnIntoModel } from "./helpers/e2e-models-helpers";
import {
openDetailsSidebar,
turnIntoModel,
} from "./helpers/e2e-models-helpers";
const { ORDERS_ID, ORDERS, PRODUCTS_ID } = SAMPLE_DATABASE;
@@ -100,6 +103,110 @@ 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,11 +1,13 @@
const { H } = cy;
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DB_ID, SAMPLE_DB_SCHEMA_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", () => {
@@ -107,6 +109,48 @@ 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,6 +201,72 @@ 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,6 +218,26 @@ 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,6 +294,114 @@ 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,5 +1,9 @@
const { H } = cy;
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import {
SAMPLE_DB_ID,
USER_GROUPS,
WRITABLE_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";
@@ -523,6 +527,117 @@ 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

@@ -0,0 +1,24 @@
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,6 +73,18 @@ 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();
@@ -87,6 +99,14 @@ describe("scenarios > reference > databases", () => {
});
});
});
it(
"should sort databases in new native question data selection popover",
{ tags: "@skip" },
() => {
checkQuestionSourceDatabasesOrder("Native query");
},
);
});
describe("x-ray", () => {
@@ -135,3 +155,25 @@ 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,7 +1,6 @@
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;
@@ -430,43 +429,6 @@ 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,16 +365,12 @@ describe("scenarios > organization > timelines > question", () => {
H.timelineEventChip("RC2").should("not.exist");
// its timeline, visible but having one hidden event
// should display its checkbox in an indeterminate state
// should display its checkbox with a "dash" icon
cy.findByTestId("sidebar-content")
.findByText("Releases")
.closest("[aria-label='Timeline card header']")
.within(() => {
cy.findByRole("checkbox").should(
"have.prop",
"indeterminate",
true,
);
cy.icon("dash").should("be.visible");
// Hide the timeline then show it again
cy.findByRole("checkbox").click();

View File

@@ -117,7 +117,6 @@ 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,11 +8,75 @@ import {
} from "e2e/support/cypress_sample_instance_data";
const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
const { PRODUCTS_ID, PEOPLE_ID, PRODUCTS } = SAMPLE_DATABASE;
const { ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, REVIEWS_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");
@@ -88,6 +152,46 @@ 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,3 +709,125 @@ 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,6 +681,57 @@ 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,6 +568,41 @@ 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,6 +246,7 @@ describe(
{ tags: "@mongo" },
() => {
const MONGO_DB_NAME = "QA Mongo";
const MONGO_DB_ID = 2;
beforeEach(() => {
H.restore("mongo-5");
@@ -314,6 +315,64 @@ 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,6 +700,80 @@ 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,6 +48,8 @@ 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;
@@ -77,6 +79,127 @@ 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", () => {
@@ -407,6 +530,22 @@ 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,5 +652,35 @@ 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,6 +429,128 @@ 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,6 +253,25 @@ 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 } = SAMPLE_DATABASE;
const { ORDERS_ID, PEOPLE_ID, REVIEWS_ID } = SAMPLE_DATABASE;
const visitEmbeddingWithSearch = (url = "/") => {
H.visitFullAppEmbeddingUrl({
@@ -249,6 +249,27 @@ 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,6 +167,20 @@ 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", () => {

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