Compare commits
59 Commits
nodejs
...
stack-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75df201252 | ||
|
|
2779f14fd8 | ||
|
|
b78ac4affd | ||
|
|
acaaa73106 | ||
|
|
2e58c450be | ||
|
|
039a969544 | ||
|
|
f5ac3e3c64 | ||
|
|
44b811bdcc | ||
|
|
0421c77905 | ||
|
|
bd6f636e35 | ||
|
|
15ef5d5b97 | ||
|
|
b0525d2dc4 | ||
|
|
c2f5f3bd7a | ||
|
|
a9f25e8645 | ||
|
|
70a3c288ed | ||
|
|
3f2ad8bd17 | ||
|
|
fd13d19cad | ||
|
|
02a1dc223b | ||
|
|
da72c9e970 | ||
|
|
e83b3f6abe | ||
|
|
28f667591b | ||
|
|
5a315ad0b4 | ||
|
|
a04aaf3714 | ||
|
|
24a9ec6182 | ||
|
|
49716266bf | ||
|
|
9cd6391ce9 | ||
|
|
f11ceeac6e | ||
|
|
9b3874e114 | ||
|
|
4480309da3 | ||
|
|
c7690123cf | ||
|
|
3fe2998633 | ||
|
|
7c8f7b450b | ||
|
|
4a7f73029a | ||
|
|
88432c1055 | ||
|
|
b3bcce1e6b | ||
|
|
5fced6e7da | ||
|
|
d90b61d26d | ||
|
|
6cd29dedb0 | ||
|
|
9c2ee7c41d | ||
|
|
bed8a61829 | ||
|
|
5a65c04cda | ||
|
|
11e969a619 | ||
|
|
0312bafbc9 | ||
|
|
3eabd5847f | ||
|
|
b3cddb3504 | ||
|
|
b1d98f6538 | ||
|
|
adc97eaff2 | ||
|
|
0a3659ea3d | ||
|
|
4539b47457 | ||
|
|
6be03b850b | ||
|
|
38346d6268 | ||
|
|
6059647f51 | ||
|
|
87841b3fe6 | ||
|
|
4e60ba6fad | ||
|
|
36fd11b0f7 | ||
|
|
6ca347e9e2 | ||
|
|
e58b5a46fb | ||
|
|
27bfc0d005 | ||
|
|
b7322989e3 |
@@ -451,8 +451,7 @@
|
||||
|
||||
bug-reporting
|
||||
{:team "UX West"
|
||||
:api #{metabase.bug-reporting.api
|
||||
metabase.bug-reporting.init}
|
||||
:api #{metabase.bug-reporting.api metabase.bug-reporting.init metabase.bug-reporting.settings}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
@@ -505,6 +504,7 @@
|
||||
api
|
||||
app-db
|
||||
appearance
|
||||
bug-reporting
|
||||
collections
|
||||
config
|
||||
dashboards
|
||||
@@ -1962,6 +1962,7 @@
|
||||
metabase.query-processor.pivot
|
||||
metabase.query-processor.preprocess
|
||||
metabase.query-processor.schema
|
||||
metabase.query-processor.setup
|
||||
metabase.query-processor.store
|
||||
metabase.query-processor.streaming
|
||||
metabase.query-processor.streaming.common
|
||||
@@ -2264,7 +2265,7 @@
|
||||
setup
|
||||
system
|
||||
util}
|
||||
:model-imports #{:model/AuthIdentity :model/User}}
|
||||
:model-imports #{:model/User}}
|
||||
|
||||
slackbot
|
||||
{:team "Metabot"
|
||||
@@ -2523,6 +2524,7 @@
|
||||
premium-features
|
||||
query-processor
|
||||
remote-sync
|
||||
request
|
||||
revisions
|
||||
run-tracking
|
||||
search
|
||||
@@ -2696,6 +2698,7 @@
|
||||
metabase.users.settings}
|
||||
:uses #{analytics
|
||||
api
|
||||
auth-identity
|
||||
batch-processing
|
||||
channel
|
||||
config
|
||||
@@ -2711,11 +2714,9 @@
|
||||
util
|
||||
enterprise/advanced-permissions}
|
||||
:model-exports #{:model/User :model/UserParameterValue}
|
||||
:model-imports #{:model/AuthIdentity
|
||||
:model/Dashboard
|
||||
:model-imports #{:model/Dashboard
|
||||
:model/Database
|
||||
:model/PermissionsGroupMembership
|
||||
:model/Session
|
||||
:model/Tenant}}
|
||||
|
||||
users-rest
|
||||
@@ -2737,8 +2738,7 @@
|
||||
users
|
||||
util
|
||||
enterprise/advanced-permissions}
|
||||
:model-imports #{:model/AuthIdentity
|
||||
:model/Card
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Dashboard
|
||||
:model/LoginHistory
|
||||
@@ -2972,6 +2972,7 @@
|
||||
:uses #{api
|
||||
api-keys
|
||||
app-db
|
||||
auth-identity
|
||||
driver
|
||||
lib
|
||||
permissions
|
||||
|
||||
@@ -27,12 +27,11 @@
|
||||
:metabase/validate-defendpoint-has-response-schema 440
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 50
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case 44
|
||||
:metabase/validate-deftest 9
|
||||
:metabase/validate-deftest 10
|
||||
:missing-docstring 10
|
||||
:missing-protocol-method 3
|
||||
:non-arg-vec-return-type-hint 1
|
||||
:reduce-without-init 3
|
||||
:redundant-fn-wrapper 1
|
||||
:syntax 2
|
||||
:uninitialized-var 1
|
||||
:unquote-not-syntax-quoted 1
|
||||
|
||||
12
.github/actions/collect-profile/action.yml
vendored
@@ -18,6 +18,10 @@ runs:
|
||||
steps:
|
||||
# Unlike a JUnit failure report, these run on a green build too — a suite that passes slowly is
|
||||
# exactly what profiling is for.
|
||||
#
|
||||
# `awk 'NR<=25'` rather than `head -25`: the runner ignores SIGPIPE, so `head` exiting early
|
||||
# leaves `sort` with an EPIPE write error and `pipefail` fails the step. `|| true` covers `grep`
|
||||
# finding no junit files.
|
||||
- name: Summarize test wall times
|
||||
if: ${{ inputs.profiled == 'true' }}
|
||||
continue-on-error: true
|
||||
@@ -28,18 +32,21 @@ runs:
|
||||
echo '```'
|
||||
grep -h '<testsuite' target/junit/*.xml 2>/dev/null \
|
||||
| sed -E 's|.*<testsuite name="([^"]*)" time="([^"]*)".*|\2 \1|' \
|
||||
| sort -rn | head -25
|
||||
| sort -rn | awk 'NR<=25' || true
|
||||
echo '```'
|
||||
echo "### Slowest tests (seconds)"
|
||||
echo '```'
|
||||
grep -h '<testcase' target/junit/*.xml 2>/dev/null \
|
||||
| sed -E 's|.*classname="([^"]*)" name="([^"]*)" time="([^"]*)".*|\3 \1/\2|' \
|
||||
| sort -rn | head -25
|
||||
| sort -rn | awk 'NR<=25' || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Only the rendered flamegraph is kept: the raw collapsed stacks it was built from run to
|
||||
# hundreds of MB on a long suite, and are reproducible locally from the same test selection.
|
||||
#
|
||||
# The info-level test log rides along on a green profiled run too. The failure-only log artifact
|
||||
# the test jobs upload is not written when nothing failed, which is exactly the run being profiled.
|
||||
- name: Upload profile
|
||||
if: ${{ inputs.profiled == 'true' }}
|
||||
continue-on-error: true
|
||||
@@ -49,5 +56,6 @@ runs:
|
||||
path: |
|
||||
target/clj-async-profiler/results/*flamegraph.html
|
||||
target/junit/*.xml
|
||||
logs/test-log.json
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
2
.github/actions/create-backport/action.yml
vendored
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
git push -u origin ${BACKPORT_BRANCH}
|
||||
|
||||
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 v${TARGET_VERSION} backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
if [ "$CONFLICTS" -gt 0 ]; then
|
||||
PR_ARGS+=(--draft)
|
||||
fi
|
||||
|
||||
31
.github/workflows/drivers.yml
vendored
@@ -955,17 +955,44 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/upload-tests]
|
||||
:fail-fast? true
|
||||
- name: Redshift (Excluding Upload Tests) Part 1
|
||||
# `:partition/*` balances on test-var count, so it cannot break up a block of namespaces that is
|
||||
# cheap in vars and expensive in seconds. The `metabase-enterprise.*` driver tests are exactly that:
|
||||
# ~10% of the vars and ~40% of the wall clock, and they sort ahead of `metabase.*`, so a plain 2-way
|
||||
# split hands one job the whole block and times out. Splitting on the namespace pattern first puts
|
||||
# them in their own pair of jobs, and each pair then partitions by count within a comparable cost.
|
||||
# The two patterns are exhaustive over the default `:namespace-pattern` -- every namespace lands in
|
||||
# exactly one of them -- so nothing stops running when a namespace is added.
|
||||
- name: Redshift EE (Excluding Upload Tests) Part 1
|
||||
skip: false
|
||||
test-args: >-
|
||||
:namespace-pattern '"^metabase-enterprise[.].*"'
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: Redshift (Excluding Upload Tests) Part 2
|
||||
- name: Redshift EE (Excluding Upload Tests) Part 2
|
||||
skip: false
|
||||
test-args: >-
|
||||
:namespace-pattern '"^metabase-enterprise[.].*"'
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
- name: Redshift OSS (Excluding Upload Tests) Part 1
|
||||
skip: false
|
||||
test-args: >-
|
||||
:namespace-pattern '"^(?:metabase[.]|hooks[.]).*"'
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: Redshift OSS (Excluding Upload Tests) Part 2
|
||||
skip: false
|
||||
test-args: >-
|
||||
:namespace-pattern '"^(?:metabase[.]|hooks[.]).*"'
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
|
||||
2
.github/workflows/e2e-coverage-shard.yml
vendored
@@ -97,7 +97,7 @@ jobs:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--expose grepTags="-@mongo+-@python+-@OSS+-@skip"
|
||||
--expose grepTags="-@mongo+-@python+-@OSS"
|
||||
# Don't let a single failing/flaky spec abort the shard — we still want
|
||||
# coverage from the specs that did run.
|
||||
continue-on-error: true
|
||||
|
||||
4
.github/workflows/e2e-test.yml
vendored
@@ -181,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
echo "Running tests with specs: $SPEC_PATH"
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--expose grepTags="-@mongo+-@python+-@OSS+-@skip" \
|
||||
--expose grepTags="-@mongo+-@python+-@OSS" \
|
||||
--spec "$SPEC_PATH"
|
||||
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
|
||||
continue-on-error: true
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
run: |
|
||||
echo "Running tests with specs: $SPEC_PATH"
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--expose grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@mongo+-@python+-@OSS+-@skip'}}" \
|
||||
--expose grepTags="${{ inputs.tags || '-@mongo+-@python+-@OSS' }}" \
|
||||
--spec "$SPEC_PATH"
|
||||
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
|
||||
continue-on-error: true
|
||||
|
||||
137
.github/workflows/notify.rerun-limit.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: Notify On Re-run Limit
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Run tests"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Send Slack Notification If Workflow Run Attempts Exceed Limit
|
||||
if: ${{ github.event.workflow_run.run_attempt > 5 && github.repository == 'metabase/metabase' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Send Slack Notification
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
RUN_NAME: ${{ github.event.workflow_run.name }}
|
||||
RUN_URL: ${{ github.event.workflow_run.html_url }}
|
||||
TRIGGERING_ACTOR_LOGIN: ${{ github.event.workflow_run.triggering_actor.login }}
|
||||
with:
|
||||
script: |
|
||||
const { findSlackMessage, mentionUserByGithubLogin, sendSlackMessage, sendSlackReply } = require(`${{ github.workspace }}/release/dist/index.cjs`);
|
||||
|
||||
const CHANNEL_NAME = 'engineering-ci';
|
||||
|
||||
// made a few changes...
|
||||
|
||||
// even more changes
|
||||
|
||||
const ATTEMPT = Number(process.env.ATTEMPT);
|
||||
const COMMIT = process.env.COMMIT;
|
||||
const SHORT_COMMIT = COMMIT.slice(0, 7);
|
||||
const RUN_ID = process.env.RUN_ID;
|
||||
const RUN_NAME = process.env.RUN_NAME;
|
||||
const RUN_URL = process.env.RUN_URL;
|
||||
const TRIGGERING_ACTOR_LOGIN = process.env.TRIGGERING_ACTOR_LOGIN;
|
||||
|
||||
// Every pull request in a stack re-runs as the stack is rebased, so re-runs shouldn't trigger notifications.
|
||||
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: COMMIT,
|
||||
}).catch(error => {
|
||||
core.warning(`Could not fetch pull requests for ${SHORT_COMMIT}: ${error.message}`);
|
||||
return { data: [] };
|
||||
});
|
||||
|
||||
const stackedPullRequest = pullRequests.find(pullRequest => pullRequest.stack);
|
||||
if (stackedPullRequest) {
|
||||
const { number, stack } = stackedPullRequest;
|
||||
core.info(`Skipping notification: PR #${number} is ${stack.position} of ${stack.size} in stack #${stack.number}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up PR title to avoid breaking the link to the PR in Slack
|
||||
const pullRequest = pullRequests.find(pullRequest => pullRequest.state === 'open') ?? pullRequests[0];
|
||||
const pullRequestLink = pullRequest
|
||||
? ` in <${pullRequest.html_url}|#${pullRequest.number} ${pullRequest.title.replace(/[<>|]/g, '')}>`
|
||||
: '';
|
||||
|
||||
const messageText = `Workflow Retry Limit Exceeded for run ${RUN_ID} of ${RUN_NAME}`;
|
||||
const existingMessage = await findSlackMessage({
|
||||
channelName: CHANNEL_NAME,
|
||||
text: messageText,
|
||||
limit: 200,
|
||||
}).catch(error => {
|
||||
core.warning(`Could not search #${CHANNEL_NAME} for an existing message: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (existingMessage) {
|
||||
await sendSlackReply({
|
||||
channelName: CHANNEL_NAME,
|
||||
messageId: existingMessage.id,
|
||||
message: `:repeat: <${RUN_URL}|${RUN_NAME}> ran again: that is ${ATTEMPT - 1} re-runs for this workflow run.`,
|
||||
}).catch(error => {
|
||||
core.warning(`Could not post a reply to #${CHANNEL_NAME}: ${error.message}`);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await sendSlackMessage({
|
||||
channelName: CHANNEL_NAME,
|
||||
message: messageText,
|
||||
blocks: [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": `:rotating_light: Workflow Rerun Limit Exceeded :rotating_light:`,
|
||||
"emoji": true,
|
||||
}
|
||||
},
|
||||
],
|
||||
attachments: [{
|
||||
color: "#ff4500",
|
||||
blocks: [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": `<${RUN_URL}|${RUN_NAME}> has been re-run ${ATTEMPT - 1} times by ${mentionUserByGithubLogin(TRIGGERING_ACTOR_LOGIN)} for <https://github.com/metabase/metabase/commit/${COMMIT}|${SHORT_COMMIT}> commit${pullRequestLink}.`
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Repeatedly re-running CI workflows wastes CI resources and may cause more issues. If re-running a workflow a handful of times does not resolve an issue, reach out to the github-admins Slack group to force merge the PR."
|
||||
}
|
||||
}
|
||||
],
|
||||
}],
|
||||
}).catch(error => {
|
||||
core.warning(`Could not post a message to #${CHANNEL_NAME}: ${error.message}`);
|
||||
});
|
||||
2
.github/workflows/release-log.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Update Release Log
|
||||
run-name: Update Release Log ${{ inputs.version || github.ref_name }}
|
||||
|
||||
# some other changes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 418 KiB |
@@ -6,6 +6,9 @@ const webpack = require("webpack");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const { CSS_CONFIG } = require("../frontend/build/shared/rspack/css-config");
|
||||
const {
|
||||
SIDE_EFFECT_FREE_RULE,
|
||||
} = require("../frontend/build/shared/rspack/side-effect-free-modules");
|
||||
|
||||
const mainAppStories = [
|
||||
"../frontend/**/*.mdx",
|
||||
@@ -80,6 +83,7 @@ const config: StorybookConfig = {
|
||||
module: {
|
||||
...config.module,
|
||||
rules: [
|
||||
SIDE_EFFECT_FREE_RULE,
|
||||
...(config.module?.rules ?? []).filter(
|
||||
(rule) => !isCSSRule(rule) && !isSvgRule(rule),
|
||||
),
|
||||
|
||||
@@ -57,4 +57,4 @@ clojure -M:generate-automagic-dashboards-pot
|
||||
# merge all pots #
|
||||
##################
|
||||
|
||||
msgcat --add-location=file "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME"
|
||||
msgcat --add-location=file -F "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME"
|
||||
|
||||
263
docs/api.json
generated
@@ -16623,6 +16623,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/agent" ]
|
||||
}
|
||||
},
|
||||
@@ -17728,6 +17738,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/apps" ]
|
||||
}
|
||||
},
|
||||
@@ -21890,6 +21910,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/comment" ]
|
||||
},
|
||||
"post" : {
|
||||
@@ -21975,6 +22005,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/comment" ]
|
||||
}
|
||||
},
|
||||
@@ -23383,6 +23423,18 @@
|
||||
"pattern" : "^[A-Za-z0-9_\\-]{21}$"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "dashboard_load_id",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"oneOf" : [ {
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}, {
|
||||
"type" : "null"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -24132,6 +24184,18 @@
|
||||
"pattern" : "^[A-Za-z0-9_\\-]{21}$"
|
||||
} ]
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "dashboard_load_id",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"oneOf" : [ {
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}, {
|
||||
"type" : "null"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -25204,6 +25268,18 @@
|
||||
"minimum" : 1
|
||||
},
|
||||
"description" : "value must be an integer greater than zero."
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "include_editable_data_model",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"default" : false,
|
||||
"oneOf" : [ {
|
||||
"type" : "boolean"
|
||||
}, {
|
||||
"type" : "null"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -25755,7 +25831,14 @@
|
||||
"operationId" : "get-api-database-virtual-db-datasets-schema",
|
||||
"summary" : "GET /api/database/{virtual-db}/datasets/{schema}",
|
||||
"description" : "Returns a list of Tables for the datasets virtual database.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "path",
|
||||
"name" : "schema",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
@@ -25795,7 +25878,14 @@
|
||||
"operationId" : "get-api-database-virtual-db-schema-schema",
|
||||
"summary" : "GET /api/database/{virtual-db}/schema/{schema}",
|
||||
"description" : "Returns a list of Tables for the saved questions virtual database.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "path",
|
||||
"name" : "schema",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
@@ -28166,6 +28256,17 @@
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "locale",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"oneOf" : [ {
|
||||
"type" : "string"
|
||||
}, {
|
||||
"type" : "null"
|
||||
} ]
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -28709,6 +28810,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/custom-viz-plugin" ]
|
||||
}
|
||||
},
|
||||
@@ -28919,6 +29030,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/custom-viz-plugin" ]
|
||||
}
|
||||
},
|
||||
@@ -29094,6 +29215,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/custom-viz-plugin" ]
|
||||
}
|
||||
},
|
||||
@@ -29390,6 +29521,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-complexity-score" ]
|
||||
}
|
||||
},
|
||||
@@ -30570,6 +30711,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/library" ]
|
||||
},
|
||||
"get" : {
|
||||
@@ -30605,6 +30756,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/library" ]
|
||||
}
|
||||
},
|
||||
@@ -31929,6 +32090,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/permission_debug" ]
|
||||
}
|
||||
},
|
||||
@@ -32564,6 +32735,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/remote-sync" ]
|
||||
}
|
||||
},
|
||||
@@ -33397,7 +33578,15 @@
|
||||
"operationId" : "put-api-ee-scim-v2-Groups-id",
|
||||
"summary" : "PUT /api/ee/scim/v2/Groups/{id}",
|
||||
"description" : "Update a group.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "path",
|
||||
"name" : "id",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
@@ -33680,7 +33869,15 @@
|
||||
"operationId" : "put-api-ee-scim-v2-Users-id",
|
||||
"summary" : "PUT /api/ee/scim/v2/Users/{id}",
|
||||
"description" : "Update a user.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "path",
|
||||
"name" : "id",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
@@ -34854,6 +35051,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/tenant" ]
|
||||
}
|
||||
},
|
||||
@@ -38162,6 +38369,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/geojson" ]
|
||||
}
|
||||
},
|
||||
@@ -38190,6 +38407,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/geojson" ]
|
||||
}
|
||||
},
|
||||
@@ -38384,7 +38611,16 @@
|
||||
"operationId" : "get-api-health-inspector",
|
||||
"summary" : "GET /api/health-inspector",
|
||||
"description" : "Get a list of recent health check runs.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "query",
|
||||
"name" : "limit",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"description" : "value must be an integer greater than zero."
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response",
|
||||
@@ -45806,6 +46042,13 @@
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "value",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -49214,6 +49457,16 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/session" ]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,13 @@ title: API changelog
|
||||
|
||||
## Metabase 0.64.0
|
||||
|
||||
- `POST /api/slack/bug-report` now requires bug reporting to be enabled (`MB_BUG_REPORTING_ENABLED`).
|
||||
`diagnosticInfo.reporter` is now a boolean: `true` attributes the report to the authenticated user, `false` (or
|
||||
omitting it) submits the report anonymously. The previous `{ "name": ..., "email": ... }` object is still accepted
|
||||
and treated as `true`; the name and email in it are ignored. The request body is validated against a fixed set of
|
||||
keys; undeclared keys are dropped. This change is
|
||||
backported to 0.58 and later; see the per-version entries below.
|
||||
|
||||
- Self-hosted environments must now explicitly enable transforms before beginning to use them via the API. Admins can enable transforms in Data Studio or by setting the MB_TRANSFORMS_ENABLED environment variable to true.
|
||||
|
||||
- The endpoints that fetch prefill values for action forms have been converted from GET to POST so that parameter values are sent in the JSON request body instead of the URL query string. `parameters` is now a JSON object in the request body rather than a JSON-encoded query-string parameter:
|
||||
@@ -18,12 +25,42 @@ title: API changelog
|
||||
|
||||
The GET variants have been removed without a deprecation period.
|
||||
|
||||
## Metabase 0.63.15
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.62.18
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.61.20
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.61.0
|
||||
|
||||
- `POST /api/metabot/describe/card` and `POST /api/metabot/describe/dashboard/:id` have been removed. These endpoints
|
||||
provided LLM-powered autodescription of cards and dashboards using the legacy OpenAI client. This functionality has
|
||||
been superseded by the Metabot agent.
|
||||
|
||||
## Metabase 0.60.26
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.59.30
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.58.32
|
||||
|
||||
- `POST /api/slack/bug-report`: `diagnosticInfo.reporter` is now a boolean and bug reporting must be enabled. See the
|
||||
0.64.0 entry.
|
||||
|
||||
## Metabase 0.57.0
|
||||
|
||||
- MBQL queries (in Cards and elsewhere) are now serialized as MBQL 5 as opposed to MBQL 4 (aka legacy MBQL) in the
|
||||
|
||||
@@ -30,6 +30,9 @@ title: Driver interface changelog
|
||||
form for the pivot-grouping bitmask. The default emits `GROUPING(exprs...)` (the Postgres/Oracle/Snowflake
|
||||
multi-arg extension); drivers whose SQL dialect uses a different function or shape override this method.
|
||||
|
||||
- `:native-pivot-tables` is now enabled for `:hive-like` drivers.
|
||||
Hive-family dialects synthesise the pivot-grouping bitmask from single-arg `GROUPING(x)` calls.
|
||||
|
||||
- Index Manager: drivers can now read and create table indexes, in the broad sense (secondary indexes, sort keys,
|
||||
distribution keys, clustering, etc.). New driver feature flags:
|
||||
|
||||
@@ -58,11 +61,14 @@ title: Driver interface changelog
|
||||
- `metabase.driver/compile-create-index` `[driver schema table structured]` -- compiles a `:standalone` index into
|
||||
the DDL statement(s) that create it.
|
||||
|
||||
- `metabase.driver-api.core/aggregation-name` is now deprecated, as it operates on legacy MBQL; use
|
||||
`metabase.driver-api.core/mbql-5-aggregation-name` going forward.
|
||||
- `metabase.driver.sql-jdbc.sync.interface/db-tables` is now a multimethod for retrieving JDBC metadata
|
||||
tables. SQL JDBC drivers can override this method to customize which database objects are discovered during sync.
|
||||
|
||||
- The method signature for `metabase.driver/substitute-native-parameters-in-stage-method`, introduced in 63, has
|
||||
changed; please update your implementations accordingly.
|
||||
- `metabase.driver-api.core/aggregation-name` is now deprecated, as it operates on legacy MBQL; use
|
||||
`metabase.driver-api.core/mbql-5-aggregation-name` going forward.
|
||||
|
||||
- The method signature for `metabase.driver/substitute-native-parameters-in-stage-method`, introduced in 63, has
|
||||
changed; please update your implementations accordingly.
|
||||
|
||||
## Metabase 0.63.0
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ type MetabaseColorKey =
|
||||
| "background_surface-warning-strong"
|
||||
| "border-brand"
|
||||
| "border-filter"
|
||||
| "border-highlight"
|
||||
| "border-neutral"
|
||||
| "border-neutral-strong"
|
||||
| "border-neutral-strongest"
|
||||
|
||||
@@ -133,6 +133,7 @@ type MetabaseEmbeddingColorKeyV2 = Exclude<
|
||||
| "background_surface-warning-strong"
|
||||
| "border-brand"
|
||||
| "border-filter"
|
||||
| "border-highlight"
|
||||
| "border-neutral"
|
||||
| "border-neutral-strong"
|
||||
| "border-neutral-strongest"
|
||||
|
||||
@@ -40,6 +40,10 @@ const {
|
||||
} = require("@esbuild-plugins/node-modules-polyfill");
|
||||
const cypressSplit = require("cypress-split");
|
||||
|
||||
const {
|
||||
sideEffectFreeModulesPlugin,
|
||||
} = require("../../frontend/build/shared/esbuild/side-effect-free-modules-plugin");
|
||||
|
||||
const isInstrumented = process.env.INSTRUMENT_COVERAGE === "true";
|
||||
// The Cypress config process runs with cwd = this file's directory
|
||||
// (e2e/support), so @cypress/code-coverage writes .nyc_output/out.json here.
|
||||
@@ -238,7 +242,11 @@ const defaultConfig = {
|
||||
loader: {
|
||||
".svg": "text",
|
||||
},
|
||||
plugins: [NodeModulesPolyfillPlugin(), assetsResolverPlugin],
|
||||
plugins: [
|
||||
NodeModulesPolyfillPlugin(),
|
||||
assetsResolverPlugin,
|
||||
sideEffectFreeModulesPlugin,
|
||||
],
|
||||
sourcemap: "inline",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ export const cacheStrategySidesheet = () =>
|
||||
|
||||
export const cacheStrategySelect = () =>
|
||||
cy
|
||||
.findByRole("form", { name: "Select the cache invalidation policy" })
|
||||
.findByRole("form", { name: "Cache invalidation policy" })
|
||||
.findByTestId("cache-strategy-select");
|
||||
|
||||
/** Open the strategy dropdown and pick an option by its title (e.g. /Duration/).
|
||||
@@ -13,3 +13,8 @@ export const selectCacheStrategy = (name: RegExp) => {
|
||||
cacheStrategySelect().click();
|
||||
cy.findByRole("option", { name }).click();
|
||||
};
|
||||
|
||||
/** Selecting the Duration strategy leaves the duration field empty, and the
|
||||
* form cannot be saved until it is filled. */
|
||||
export const fillCacheDuration = (value: number) =>
|
||||
cy.findByRole("spinbutton", { name: /Cache duration/ }).type(String(value));
|
||||
|
||||
@@ -589,7 +589,8 @@ function getSegmentEditorNameInput() {
|
||||
}
|
||||
|
||||
function getSegmentEditorDescriptionInput() {
|
||||
return getSegmentEditor().findByLabelText("Give it a description");
|
||||
getSegmentEditor().findByLabelText("Give it a description").click();
|
||||
return getSegmentEditor().findByPlaceholderText("Only if it really needs it");
|
||||
}
|
||||
|
||||
function getSegmentEditorFilterPlaceholder() {
|
||||
@@ -681,7 +682,8 @@ function getMeasureEditorNameInput() {
|
||||
}
|
||||
|
||||
function getMeasureEditorDescriptionInput() {
|
||||
return getMeasureEditor().findByLabelText("Give it a description");
|
||||
getMeasureEditor().findByLabelText("Give it a description").click();
|
||||
return getMeasureEditor().findByPlaceholderText("Only if it really needs it");
|
||||
}
|
||||
|
||||
function getMeasureEditorAggregationPlaceholder() {
|
||||
@@ -805,7 +807,7 @@ function verifyTablePreview({
|
||||
});
|
||||
|
||||
if (description != null) {
|
||||
cy.findByTestId("header-cell").realHover();
|
||||
hoverHeaderCell();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -814,6 +816,26 @@ function verifyTablePreview({
|
||||
}
|
||||
}
|
||||
|
||||
// Open the preview's column-description hovercard. The hovercard opens on a
|
||||
// delayed `openDelay` timer, and a single one-shot `realHover()` can miss it:
|
||||
// after `cy.wait("@dataset")` the table re-renders on a microtask (fetch
|
||||
// resolution) and Chrome v133+ headless hit-tests CDP mouse events
|
||||
// differently, either of which drops the hover and cancels the open timer,
|
||||
// leaving the hovercard permanently absent for that attempt. Re-query the cell
|
||||
// for each dispatch so the events land on the post-render DOM node, and fire
|
||||
// both `mouseover` (bubbles → React 18 synthetic onMouseEnter for any wrapper)
|
||||
// and `mouseenter` (for native useEventListener handlers Mantine attaches
|
||||
// directly to the cell).
|
||||
function hoverHeaderCell() {
|
||||
const headerCell = () =>
|
||||
cy
|
||||
.findByTestId("header-cell")
|
||||
.findByTestId("cell-data")
|
||||
.should("be.visible");
|
||||
headerCell().trigger("mouseenter", { force: true });
|
||||
headerCell().trigger("mouseover", { force: true });
|
||||
}
|
||||
|
||||
function verifyObjectDetailPreview({
|
||||
rowNumber,
|
||||
row,
|
||||
|
||||
@@ -16,8 +16,14 @@ export const DependencyGraph = {
|
||||
};
|
||||
|
||||
export const DependencyDiagnostics = {
|
||||
visitBrokenDependencies: () =>
|
||||
cy.visit("/monitor/dependency-diagnostics/broken"),
|
||||
visitBrokenDependencies: () => {
|
||||
cy.intercept("GET", "/api/ee/dependencies/graph/breaking*").as(
|
||||
"breakingEntities",
|
||||
);
|
||||
cy.visit("/monitor/dependency-diagnostics/broken");
|
||||
cy.wait("@breakingEntities");
|
||||
DependencyDiagnostics.list().should("be.visible");
|
||||
},
|
||||
visitUnreferencedEntities: () => {
|
||||
cy.intercept("GET", "/api/ee/dependencies/graph/unreferenced*").as(
|
||||
"unreferencedEntities",
|
||||
@@ -123,7 +129,7 @@ export function waitForBreakingDependencies(
|
||||
return cy
|
||||
.request<ListBreakingGraphNodesResponse>(
|
||||
"GET",
|
||||
"/api/ee/dependencies/graph/breaking",
|
||||
"/api/ee/dependencies/graph/breaking?include-personal-collections=true",
|
||||
)
|
||||
.then((response) => {
|
||||
if (filter(response.body.data)) {
|
||||
|
||||
@@ -8,12 +8,11 @@ import {
|
||||
NORMAL_USER_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const { sandboxed, normal, admin, nodata, nocollection } = USERS;
|
||||
const { normal, admin, nocollection } = USERS;
|
||||
const { ALL_USERS_GROUP, DATA_GROUP } = USER_GROUPS;
|
||||
const TOTAL_USERS = Object.entries(USERS).length;
|
||||
const TOTAL_GROUPS = Object.entries(USER_GROUPS).length;
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
const { COLLECTION_GROUP } = USER_GROUPS;
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const TEST_USER = {
|
||||
first_name: "Testy",
|
||||
@@ -26,8 +25,6 @@ const adminUserName = H.getFullName(admin);
|
||||
const noCollectionUserName = H.getFullName(nocollection);
|
||||
const normalUserName = H.getFullName(normal);
|
||||
|
||||
const totalUsers = Object.keys(USERS).length;
|
||||
|
||||
describe("scenarios > admin > people", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("GET", "/api/permissions/group").as("getGroups");
|
||||
@@ -892,77 +889,6 @@ describe("scenarios > admin > people > group managers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: remove skip when this issue gets fixed. Initial attempt: https://github.com/metabase/metabase/pull/23825,
|
||||
// reverted in https://github.com/metabase/metabase/pull/24760 to fix a security vulnerability.
|
||||
describe("issue 23689", { tags: "@skip" }, () => {
|
||||
function findUserByFullName(user) {
|
||||
const { first_name, last_name } = user;
|
||||
return cy.findByText(`${first_name} ${last_name}`);
|
||||
}
|
||||
|
||||
function visitGroupPermissionsPage(groupId) {
|
||||
cy.visit(`/admin/people/groups/${groupId}`);
|
||||
cy.wait("@membership");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("GET", "/api/permissions/membership").as("membership");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
visitGroupPermissionsPage(COLLECTION_GROUP);
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("3 members");
|
||||
|
||||
findUserByFullName(normal);
|
||||
findUserByFullName(nodata);
|
||||
|
||||
// Make sandboxed user a group manager
|
||||
findUserByFullName(sandboxed)
|
||||
.closest("tr")
|
||||
.findByTestId("user-type-toggle")
|
||||
.click({ force: true });
|
||||
|
||||
// Sanity check instead of waiting for the PUT request
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Manager");
|
||||
|
||||
cy.sandboxTable({
|
||||
table_id: ORDERS_ID,
|
||||
attribute_remappings: {
|
||||
attr_uid: ["dimension", ["field", ORDERS.USER_ID, null]],
|
||||
},
|
||||
});
|
||||
|
||||
cy.signOut();
|
||||
cy.signInAsSandboxedUser();
|
||||
});
|
||||
|
||||
it("sandboxed group manager should see all other members (metabase#23689)", () => {
|
||||
visitGroupPermissionsPage(COLLECTION_GROUP);
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("3 members");
|
||||
|
||||
findUserByFullName(sandboxed);
|
||||
findUserByFullName(normal);
|
||||
findUserByFullName(nodata);
|
||||
|
||||
cy.visit("/admin/people");
|
||||
cy.wait("@membership");
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(`${totalUsers} people found`);
|
||||
findUserByFullName(sandboxed);
|
||||
findUserByFullName(normal);
|
||||
findUserByFullName(nodata);
|
||||
findUserByFullName(nocollection);
|
||||
});
|
||||
});
|
||||
|
||||
function showUserOptions(full_name) {
|
||||
cy.findByText(full_name)
|
||||
.closest("tr")
|
||||
|
||||
@@ -256,21 +256,33 @@ describe("issue 45890", () => {
|
||||
cy.visit("/admin/performance/databases");
|
||||
H.main()
|
||||
.findByLabelText(/Edit policy for database 'Sample Database'/)
|
||||
.findByText("No caching")
|
||||
.click();
|
||||
|
||||
// The strategy picker is a dropdown; its options render in a portal.
|
||||
H.main().findByTestId("cache-strategy-select").click();
|
||||
// The strategy form opens in a side panel; the picker options render in a portal.
|
||||
cy.findByTestId("cache-policy-panel")
|
||||
.findByTestId("cache-strategy-select")
|
||||
.click();
|
||||
cy.findByRole("option", { name: /Schedule/ }).click();
|
||||
|
||||
H.main().button("Save changes").click();
|
||||
cy.intercept("PUT", "/api/cache").as("putCacheConfig");
|
||||
cy.findByTestId("cache-policy-panel").button("Save").click();
|
||||
cy.wait("@putCacheConfig");
|
||||
});
|
||||
|
||||
it("should correctly reset caching schedule form when discarding changes", () => {
|
||||
H.main().findByLabelText("Frequency").click();
|
||||
cy.findByTestId("cache-policy-panel").findByLabelText("Frequency").click();
|
||||
H.popover().findByText("weekly").click();
|
||||
|
||||
H.main().button("Discard changes").click();
|
||||
H.main().findByLabelText("Frequency").should("have.value", "hourly");
|
||||
cy.log("Cancel the dirty form and confirm discarding the changes");
|
||||
cy.findByTestId("cache-policy-panel").button("Cancel").click();
|
||||
cy.findByTestId("confirm-modal").button("Discard").click();
|
||||
|
||||
cy.log("Reopening the form shows the saved schedule, not the draft");
|
||||
H.main()
|
||||
.findByLabelText(/Edit policy for database 'Sample Database'/)
|
||||
.click();
|
||||
cy.findByTestId("cache-policy-panel")
|
||||
.findByLabelText("Frequency")
|
||||
.should("have.value", "hourly");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,15 +9,8 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import type { TableId } from "metabase-types/api";
|
||||
|
||||
const { H } = cy;
|
||||
const {
|
||||
ORDERS,
|
||||
ORDERS_ID,
|
||||
PEOPLE_ID,
|
||||
PRODUCTS,
|
||||
REVIEWS,
|
||||
REVIEWS_ID,
|
||||
PRODUCTS_ID,
|
||||
} = SAMPLE_DATABASE;
|
||||
const { ORDERS, ORDERS_ID, PEOPLE_ID, REVIEWS, REVIEWS_ID, PRODUCTS_ID } =
|
||||
SAMPLE_DATABASE;
|
||||
const { ALL_USERS_GROUP } = USER_GROUPS;
|
||||
const { FieldSection, PreviewSection, TablePicker, TableSection } = H.DataModel;
|
||||
|
||||
@@ -423,54 +416,6 @@ describe("scenarios > admin > datamodel", () => {
|
||||
|
||||
H.queryBuilderHeader().findByText("View-only").should("be.visible");
|
||||
});
|
||||
|
||||
it("question with joins (metabase#15947-2)", { tags: "@skip" }, () => {
|
||||
H.createQuestion({
|
||||
name: "15947",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
joins: [
|
||||
{
|
||||
fields: "all",
|
||||
"source-table": PRODUCTS_ID,
|
||||
condition: [
|
||||
"=",
|
||||
["field", ORDERS.PRODUCT_ID, null],
|
||||
["field", PRODUCTS.ID, { "join-alias": "Products" }],
|
||||
],
|
||||
alias: "Products",
|
||||
},
|
||||
],
|
||||
filter: [
|
||||
"and",
|
||||
["=", ["field", ORDERS.QUANTITY, null], 1],
|
||||
[
|
||||
">",
|
||||
["field", PRODUCTS.RATING, { "join-alias": "Products" }],
|
||||
3,
|
||||
],
|
||||
],
|
||||
aggregation: [
|
||||
["sum", ["field", ORDERS.TOTAL, null]],
|
||||
[
|
||||
"sum",
|
||||
["field", PRODUCTS.RATING, { "join-alias": "Products" }],
|
||||
],
|
||||
],
|
||||
breakout: [
|
||||
["field", ORDERS.CREATED_AT, { "temporal-unit": "year" }],
|
||||
["field", PRODUCTS.CATEGORY, { "join-alias": "Products" }],
|
||||
],
|
||||
},
|
||||
}).then(({ body: { id: QUESTION_ID } }) => {
|
||||
turnTableVisibilityOff(PRODUCTS_ID);
|
||||
cy.visit(`/question/${QUESTION_ID}/notebook`);
|
||||
cy.findByText("Products");
|
||||
cy.findByText("Quantity is equal to 1");
|
||||
cy.findByText("Rating is greater than 3");
|
||||
H.queryBuilderHeader().findByText("View-only").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +191,8 @@ describe("issue 15542", () => {
|
||||
|
||||
describe("issue 52411", { tags: "@external" }, () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("GET", "/api/database/*/schema/*").as("getSchema");
|
||||
cy.intercept("GET", "/api/database").as("getDatabases");
|
||||
H.restore("postgres-writable");
|
||||
H.resetTestTable({ type: "postgres", table: "multi_schema" });
|
||||
cy.signInAsAdmin();
|
||||
@@ -199,8 +201,10 @@ describe("issue 52411", { tags: "@external" }, () => {
|
||||
|
||||
it("should be able to select a table in a database with multiple schemas on segments list page when there are multiple databases and there is a saved question (metabase#52411)", () => {
|
||||
cy.visit("/admin/datamodel/segments");
|
||||
cy.wait(["@getDatabases", "@getSchema"]);
|
||||
cy.findByTestId("segment-list-table").findByText("Filter by table").click();
|
||||
H.popover().within(() => {
|
||||
cy.icon("chevronleft").click(); // go back
|
||||
cy.findByText("Writable Postgres12").click();
|
||||
cy.findByText("Wild").click();
|
||||
cy.findByText("Birds").click();
|
||||
|
||||
@@ -10,7 +10,7 @@ const preemptiveCachingSwitch = () =>
|
||||
|
||||
const saveCacheStrategyForm = () => {
|
||||
cy.intercept("PUT", "/api/cache").as("putCacheConfig");
|
||||
cy.findByRole("form", { name: "Select the cache invalidation policy" })
|
||||
cy.findByRole("form", { name: "Cache invalidation policy" })
|
||||
.button(/Save/)
|
||||
.click();
|
||||
cy.wait("@putCacheConfig");
|
||||
@@ -57,7 +57,7 @@ const cancelConfirmationModal = () =>
|
||||
* frontend/.../Schedule/{cron,Schedule}.unit.spec.{ts,tsx}.
|
||||
* - Strategy form input binding (typing into Min Duration / Multiplier),
|
||||
* strategy-switch flows (Adaptive <-> Duration <-> No caching),
|
||||
* strategy -> launcher-label mapping, policy-options counts,
|
||||
* strategy -> row-label mapping, policy-options counts,
|
||||
* preemptive caching switch availability for root/database targets:
|
||||
* frontend/.../StrategyEditorForDatabases.unit.spec.tsx and
|
||||
* enterprise/.../caching/components/StrategyEditorForDatabases.unit.spec.tsx.
|
||||
@@ -72,6 +72,10 @@ describe("scenarios > admin > performance > caching", () => {
|
||||
it("saves the default-policy strategy and reflects the saved state", () => {
|
||||
cy.visit("/admin/performance");
|
||||
H.selectCacheStrategy(/Adaptive/);
|
||||
cy.findByRole("spinbutton", { name: /minimum query duration/i }).type(
|
||||
"1",
|
||||
);
|
||||
cy.findByRole("spinbutton", { name: /multiplier/i }).type("10");
|
||||
saveCacheStrategyForm();
|
||||
H.cacheStrategySelect().should("have.value", "Adaptive");
|
||||
});
|
||||
@@ -95,6 +99,7 @@ describe("scenarios > admin > performance > caching", () => {
|
||||
|
||||
cy.log("Set Sample Database to Duration and save");
|
||||
H.selectCacheStrategy(/Duration/);
|
||||
H.fillCacheDuration(24);
|
||||
saveCacheStrategyForm();
|
||||
cy.findByTestId("admin-layout-content").findByLabelText(
|
||||
/Edit.*Sample Database.*currently.*Duration/,
|
||||
@@ -109,7 +114,7 @@ describe("scenarios > admin > performance > caching", () => {
|
||||
cy.button(/Clear cache for this database/).click();
|
||||
|
||||
cy.log("Confirm in the dialog");
|
||||
cy.findByRole("dialog")
|
||||
cy.findByTestId("confirm-modal")
|
||||
.button(/Clear cache/)
|
||||
.click();
|
||||
cy.wait("@invalidateCacheForSampleDatabase");
|
||||
@@ -122,6 +127,8 @@ describe("scenarios > admin > performance > caching", () => {
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
openSidebarCacheStrategyForm("question");
|
||||
H.selectCacheStrategy(/Duration/);
|
||||
// 24 keeps the "Duration: 24h" admin-tab assertion below true
|
||||
H.fillCacheDuration(24);
|
||||
preemptiveCachingSwitch().within(() => {
|
||||
cy.findByRole("switch").should("not.be.checked");
|
||||
cy.findByRole("switch").parent("label").click();
|
||||
@@ -216,6 +223,10 @@ describe("scenarios > admin > performance > caching", () => {
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
openSidebarCacheStrategyForm("question");
|
||||
H.selectCacheStrategy(/Adaptive/);
|
||||
cy.findByRole("spinbutton", { name: /minimum query duration/i }).type(
|
||||
"1",
|
||||
);
|
||||
cy.findByRole("spinbutton", { name: /multiplier/i }).type("10");
|
||||
saveCacheStrategyForm();
|
||||
|
||||
cy.log("Both entries are visible on the admin tab");
|
||||
|
||||
@@ -16,40 +16,6 @@ describe("personal collections", () => {
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
/**
|
||||
* This reproduction is here only as a placeholder until a proper backend tests is added.
|
||||
*
|
||||
* Not entirely sure how this issue will be resolved!
|
||||
* Thus, test might not work as expected by that point.
|
||||
*
|
||||
* For example:
|
||||
* 1. FE might decide not to fetch the full collection tree on the home page or
|
||||
* 2. BE might alter this endpoint
|
||||
*
|
||||
* TODO:
|
||||
* - When the solution for this problem is ready, either adjust the test or completely remove it!
|
||||
*/
|
||||
|
||||
it(
|
||||
"shouldn't get API response containing all other personal collections when visiting the home page (metabase#24330)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
cy.intercept("GET", "/api/collection/tree*").as("getCollections");
|
||||
|
||||
cy.visit("/");
|
||||
|
||||
cy.wait("@getCollections").then(({ response: { body } }) => {
|
||||
const personalCollections = body.filter(({ personal_owner_id }) => {
|
||||
return personal_owner_id !== null;
|
||||
});
|
||||
|
||||
// Admin can only see their own personal collection, so this list should return only that
|
||||
// Loading all other users' personal collections can lead to performance issues!
|
||||
expect(personalCollections).to.have.lengthOf(1);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should see a link to other users' personal collections only if there are other users", () => {
|
||||
cy.intercept("GET", "/api/session/properties", (req) => {
|
||||
req.continue((res) => {
|
||||
|
||||
@@ -164,32 +164,6 @@ describe("scenarios > question > custom column", () => {
|
||||
H.getNotebookStep("summarize").findByText("UserLAT").should("be.visible");
|
||||
});
|
||||
|
||||
// flaky test (#19454)
|
||||
it(
|
||||
"should show info popovers when hovering over custom column dimensions in the summarize sidebar",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.openOrdersTable({ mode: "notebook" });
|
||||
cy.findByLabelText("Custom column").click();
|
||||
|
||||
H.enterCustomColumnDetails({ formula: "1 + 1", name: "Math" });
|
||||
cy.button("Done").click();
|
||||
|
||||
H.visualize();
|
||||
|
||||
H.summarize();
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Group by")
|
||||
.parent()
|
||||
.findByText("Math")
|
||||
.trigger("mouseenter");
|
||||
|
||||
H.popover().contains("Math");
|
||||
H.popover().contains("No description");
|
||||
},
|
||||
);
|
||||
|
||||
it("can create a custom column with an existing column name", () => {
|
||||
const customFormulas = [
|
||||
{
|
||||
|
||||
@@ -6,42 +6,6 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
|
||||
const { PRODUCTS, PRODUCTS_ID, ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
describe("issue 12445", { tags: ["@external", "@skip"] }, () => {
|
||||
const CC_NAME = "Abbr";
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore("mysql-8");
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should correctly apply substring for a custom column (metabase#12445)", () => {
|
||||
H.withDatabase(2, ({ PEOPLE, PEOPLE_ID }) => {
|
||||
cy.log("Create a question with `Source` column and abbreviated CC");
|
||||
H.createQuestion(
|
||||
{
|
||||
name: "12445",
|
||||
query: {
|
||||
"source-table": PEOPLE_ID,
|
||||
breakout: [["expression", CC_NAME]],
|
||||
expressions: {
|
||||
[CC_NAME]: [
|
||||
"substring",
|
||||
["field", PEOPLE.SOURCE, null],
|
||||
1,
|
||||
4, // we want 4 letter abbreviation
|
||||
],
|
||||
},
|
||||
},
|
||||
database: 2,
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
cy.findByText(CC_NAME);
|
||||
cy.findByText("Goog");
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("issue 13751", { tags: "@external" }, () => {
|
||||
const CC_NAME = "C-States";
|
||||
const PG_DB_NAME = "QA Postgres12";
|
||||
@@ -81,42 +45,6 @@ describe("issue 13751", { tags: "@external" }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
"postgres > question > custom columns",
|
||||
{ tags: ["@external", "@skip"] },
|
||||
() => {
|
||||
const PG_DB_NAME = "QA Postgres12";
|
||||
|
||||
// Ironically, both Prettier and Cypress remove escape characters from our code as well
|
||||
// We're testing for the literal sting `(?<=\/\/)[^\/]*`, but we need to escape the escape characters to make it work
|
||||
const ESCAPED_REGEX = "(?<=\\/\\/)[^\\/]*";
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore("postgres-12");
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.startNewQuestion();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(PG_DB_NAME).should("be.visible").click();
|
||||
cy.findByTextEnsureVisible("People").click();
|
||||
});
|
||||
|
||||
it("should not remove regex escape characters (metabase#14517)", () => {
|
||||
cy.log("Create custom column using `regexExtract()`");
|
||||
cy.findByLabelText("Custom Column").click();
|
||||
H.popover().within(() => {
|
||||
cy.get("[contenteditable='true']")
|
||||
.type(`regexExtract([State], "${ESCAPED_REGEX}")`)
|
||||
.blur();
|
||||
|
||||
// It removes escaped characters already on blur
|
||||
cy.log("Reported failing on v0.36.4");
|
||||
cy.contains(ESCAPED_REGEX);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("issue 14843", () => {
|
||||
const { PEOPLE, PEOPLE_ID } = SAMPLE_DATABASE;
|
||||
const CC_NAME = "City Length";
|
||||
@@ -486,7 +414,10 @@ describe("issue 20229", () => {
|
||||
}
|
||||
|
||||
function unselectColumn(column) {
|
||||
cy.findByText(column).siblings().find(".Icon-check").click({ force: true });
|
||||
cy.findByText(column)
|
||||
.siblings()
|
||||
.find('input[type="checkbox"]')
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -671,78 +602,6 @@ describe("issue 24922", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 25189", { tags: "@skip" }, () => {
|
||||
const ccTable = "Custom Created";
|
||||
const ccFunction = "Custom Total";
|
||||
|
||||
const questionDetails = {
|
||||
name: "25189",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
limit: 5,
|
||||
expressions: {
|
||||
[ccTable]: ["field", ORDERS.CREATED_AT, null],
|
||||
[ccFunction]: [
|
||||
"case",
|
||||
[[[">", ["field", ORDERS.TOTAL, null], 100], "Yay"]],
|
||||
{
|
||||
default: "Nay",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createQuestion(questionDetails).then(
|
||||
({ body: { id: baseQuestionId } }) => {
|
||||
H.createQuestion(
|
||||
{
|
||||
name: "Nested 25189",
|
||||
query: { "source-table": `card__${baseQuestionId}` },
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("custom column referencing only a single column should not be dropped in a nested question (metabase#25189)", () => {
|
||||
// 1. Column should not be dropped
|
||||
cy.findAllByTestId("header-cell")
|
||||
.should("contain", ccFunction)
|
||||
.and("contain", ccTable);
|
||||
|
||||
// 2. We shouldn't see duplication in the bulk filter modal
|
||||
H.filter();
|
||||
H.modal().within(() => {
|
||||
// Implicit assertion - will fail if more than one element is found
|
||||
cy.findByText(ccFunction);
|
||||
cy.findByText(ccTable);
|
||||
|
||||
cy.findByText("Today").click();
|
||||
cy.button("Apply Filters").click();
|
||||
});
|
||||
|
||||
cy.wait("@dataset");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("No results");
|
||||
|
||||
// 3. We shouldn't see duplication in the breakout fields
|
||||
H.summarize();
|
||||
cy.findByTestId("sidebar-content").within(() => {
|
||||
// Another implicit assertion
|
||||
cy.findByText(ccFunction);
|
||||
cy.findByText(ccTable);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
["postgres" /*, "mysql" */].forEach((dialect) => {
|
||||
describe(`issue 27745 (${dialect})`, { tags: "@external" }, () => {
|
||||
const tableName = "colors27745";
|
||||
@@ -790,140 +649,6 @@ describe("issue 25189", { tags: "@skip" }, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
// broken. see https://github.com/metabase/metabase/issues/55673
|
||||
describe("issue 42949", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should correctly show available shortcuts for date and number columns (metabase#42949)", () => {
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
native: {
|
||||
query: `
|
||||
SELECT DATE '2024-05-21' AS created_at, null as v
|
||||
UNION ALL SELECT DATE '2024-05-20', 1
|
||||
UNION ALL SELECT DATE '2024-05-19', 2
|
||||
ORDER BY created_at
|
||||
`,
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
cy.findByTestId("qb-header").findByText("Explore results").click();
|
||||
|
||||
cy.log("Verify header drills - CREATED_AT");
|
||||
H.tableHeaderClick("CREATED_AT");
|
||||
H.popover().findByText("Extract day, month…").should("be.visible");
|
||||
H.popover().findByText("Combine columns").should("not.exist");
|
||||
cy.realPress("Escape");
|
||||
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
|
||||
|
||||
cy.log("Verify header drills - V");
|
||||
H.tableHeaderClick("V");
|
||||
H.popover().findByText("Extract part of column").should("not.exist");
|
||||
H.popover().findByText("Combine columns").should("not.exist");
|
||||
cy.realPress("Escape");
|
||||
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
|
||||
|
||||
cy.log("Verify plus button - extract column");
|
||||
cy.button("Add column").click();
|
||||
H.popover().findByText("Extract part of column").click();
|
||||
H.popover().findByText("CREATED_AT").click();
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Day of month").should("be.visible");
|
||||
cy.findByText("Day of week").should("be.visible");
|
||||
cy.findByText("Month of year").should("be.visible");
|
||||
cy.findByText("Quarter of year").should("be.visible");
|
||||
cy.findByText("Year").should("be.visible").click();
|
||||
});
|
||||
cy.findAllByTestId("header-cell").eq(2).should("have.text", "Year");
|
||||
|
||||
cy.log("Verify plus button - combine columns");
|
||||
cy.button("Add column").click();
|
||||
H.popover().findByText("Combine columns").click();
|
||||
H.popover().findAllByTestId("column-input").eq(0).click();
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
H.popover()
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.findByText("CREATED_AT").should("be.visible");
|
||||
cy.findByText("V").should("be.visible");
|
||||
cy.findByText("Year").should("be.visible").click();
|
||||
});
|
||||
H.popover().button("Done").click();
|
||||
|
||||
cy.findAllByTestId("header-cell")
|
||||
.eq(3)
|
||||
.should("have.text", "Combined Year, V");
|
||||
|
||||
cy.findAllByTestId("cell-data").eq(6).should("have.text", "2,024");
|
||||
cy.findAllByTestId("cell-data").eq(7).should("have.text", "2024 2");
|
||||
cy.findAllByTestId("cell-data").eq(10).should("have.text", "2,024");
|
||||
cy.findAllByTestId("cell-data").eq(11).should("have.text", "2024 1");
|
||||
cy.findAllByTestId("cell-data").eq(13).should("have.text", "2,024");
|
||||
cy.findAllByTestId("cell-data").eq(14).should("have.text", "2024 ");
|
||||
});
|
||||
|
||||
it("should correctly show available shortcuts for a number column (metabase#42949)", () => {
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
native: {
|
||||
query: "select 1 as n",
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
cy.findByTestId("qb-header").findByText("Explore results").click();
|
||||
cy.findByLabelText("Switch to data").click();
|
||||
|
||||
cy.log("Verify header drills");
|
||||
H.tableHeaderClick("N");
|
||||
H.popover().findByText("Extract part of column").should("not.exist");
|
||||
H.popover().findByText("Combine columns").should("not.exist");
|
||||
cy.realPress("Escape");
|
||||
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
|
||||
|
||||
cy.log("Verify plus button");
|
||||
cy.button("Add column").click();
|
||||
H.popover().findByText("Extract part of column").should("not.exist");
|
||||
H.popover().findByText("Combine columns").click();
|
||||
H.popover().findAllByTestId("column-input").eq(0).click();
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
H.popover().last().findByText("N").should("be.visible");
|
||||
});
|
||||
|
||||
it("should correctly show available shortcuts for a string column (metabase#42949)", () => {
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
native: {
|
||||
query: "select 'abc'",
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
cy.findByTestId("qb-header").findByText("Explore results").click();
|
||||
cy.findByLabelText("Switch to data").click();
|
||||
|
||||
cy.log("Verify header drills");
|
||||
H.tableHeaderClick("'abc'");
|
||||
H.popover().findByText("Extract part of column").should("not.exist");
|
||||
H.popover().findByText("Combine columns").should("be.visible");
|
||||
cy.realPress("Escape");
|
||||
H.popover({ skipVisibilityCheck: true }).should("not.be.visible");
|
||||
|
||||
cy.log("Verify plus button");
|
||||
cy.button("Add column").click();
|
||||
H.popover().findByText("Extract part of column").should("not.exist");
|
||||
H.popover().findByText("Combine columns").click();
|
||||
H.popover().findAllByTestId("column-input").eq(0).click();
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
H.popover().last().findByText("'abc'").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 49342", () => {
|
||||
beforeEach(() => {
|
||||
@@ -1035,23 +760,6 @@ describe("issue 49882", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: we no longer have wrapped lines (for now)
|
||||
it(
|
||||
"should allow moving cursor between wrapped lines with arrow up and arrow down keys (metabase#49882-3)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.enterCustomColumnDetails({
|
||||
formula:
|
||||
'case([Tax] > 1, case([Total] > 200, [Total], "Nothing"), [Tax]){leftarrow}{leftarrow}{uparrow}x{downarrow}y',
|
||||
});
|
||||
|
||||
H.CustomExpressionEditor.value().should(
|
||||
"equal",
|
||||
'case([Tax] > 1, xcase([Total] > 200, [Total], "Nothing"), [Tax]y)',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("should update currently selected suggestion when suggestions list is updated (metabase#49882-4)", () => {
|
||||
const selectProductRating =
|
||||
"{downarrow}{downarrow}{downarrow}{downarrow}{downarrow}";
|
||||
|
||||
@@ -473,78 +473,6 @@ describe("issue 55687", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 58371", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
cy.request("PUT", `/api/field/${ORDERS.PRODUCT_ID}`, {
|
||||
display_name: null,
|
||||
});
|
||||
|
||||
const baseQuestion = {
|
||||
name: "Base Question",
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
aggregation: [
|
||||
[
|
||||
"aggregation-options",
|
||||
["count-where", ["=", ["field", PRODUCTS.TITLE, null], "OK"]],
|
||||
{ "display-name": "Aggregation with Dash-in-name" },
|
||||
],
|
||||
],
|
||||
breakout: [["field", PRODUCTS.ID, null]],
|
||||
},
|
||||
};
|
||||
|
||||
H.createQuestion(baseQuestion, { wrapId: true }).then((questionId) => {
|
||||
const questionDetails = {
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
joins: [
|
||||
{
|
||||
fields: "all",
|
||||
"source-table": `card__${questionId}`,
|
||||
alias: "Other Question",
|
||||
condition: [
|
||||
"=",
|
||||
["field", ORDERS.PRODUCT_ID, null],
|
||||
["field", PRODUCTS.ID, { "join-alias": "Other Question" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
expressions: {
|
||||
Foo: [
|
||||
"+",
|
||||
0,
|
||||
[
|
||||
"field",
|
||||
"count_where",
|
||||
{
|
||||
"base-type": "type/Float",
|
||||
"join-alias": "Other Question",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
H.createQuestion(questionDetails, { visitQuestion: true });
|
||||
});
|
||||
|
||||
H.openNotebook();
|
||||
});
|
||||
|
||||
it("should allow using names with a dash in them from joined tables (metabase#58371)", () => {
|
||||
H.getNotebookStep("expression").findByText("Foo").click();
|
||||
H.CustomExpressionEditor.value().should(
|
||||
"eq",
|
||||
"0 + [Other Question → Aggregation with Dash-in-name]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Issue 58230", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -585,27 +513,6 @@ describe("issue 57674", () => {
|
||||
H.openOrdersTable({ mode: "notebook" });
|
||||
});
|
||||
|
||||
// TODO: re-enable this test once we have a fix for metabase#61264
|
||||
it(
|
||||
"should show an error when using a case or if expression with mismatched types (metabase#57674)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.getNotebookStep("data").button("Custom column").click();
|
||||
|
||||
H.CustomExpressionEditor.clear();
|
||||
H.popover().findByText("Types are incompatible.").should("not.exist");
|
||||
|
||||
H.CustomExpressionEditor.type(
|
||||
'case([Total] > 100, [Created At], "foo")',
|
||||
{
|
||||
allowFastSet: true,
|
||||
},
|
||||
).blur();
|
||||
|
||||
H.popover().findByText("Types are incompatible.").should("be.visible");
|
||||
},
|
||||
);
|
||||
|
||||
it("should not show an error when using a case or if expression with compatible types (metabase#57674)", () => {
|
||||
H.getNotebookStep("data").button("Custom column").click();
|
||||
|
||||
|
||||
@@ -431,14 +431,6 @@ describe("issue 17160", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function visitPublicSourceDashboard() {
|
||||
cy.get("@sourceDashboardUUID").then((uuid) => {
|
||||
cy.visit(`/public/dashboard/${uuid}`);
|
||||
|
||||
cy.findByTextEnsureVisible("Enormous Wool Car");
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
|
||||
@@ -478,37 +470,6 @@ describe("issue 17160", () => {
|
||||
|
||||
assertMultipleValuesFilterState();
|
||||
});
|
||||
|
||||
it(
|
||||
"should pass multiple filter values to public questions and dashboards (metabase#17160-2)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
// FIXME: setup public dashboards
|
||||
setup();
|
||||
|
||||
// 1. Check click behavior connected to a public question
|
||||
visitPublicSourceDashboard();
|
||||
|
||||
cy.findAllByText("click-behavior-question-label").eq(0).click();
|
||||
|
||||
cy.url().should("include", "/public/question");
|
||||
|
||||
assertMultipleValuesFilterState();
|
||||
|
||||
// 2. Check click behavior connected to a publicdashboard
|
||||
visitPublicSourceDashboard();
|
||||
|
||||
cy.findAllByText("click-behavior-dashboard-label").eq(0).click();
|
||||
|
||||
cy.url().should("include", "/public/dashboard");
|
||||
cy.location("search").should("eq", "?category=Doohickey&category=Gadget");
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(TARGET_DASHBOARD_NAME);
|
||||
|
||||
assertMultipleValuesFilterState();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("issue 18454", () => {
|
||||
|
||||
@@ -302,49 +302,6 @@ describe("scenarios > dashboard > dashboard drill", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// This was flaking. Example: https://dashboard.cypress.io/projects/a394u1/runs/2109/test-results/91a15b66-4b80-40bf-b569-de28abe21f42
|
||||
it("should handle cross-filter on a table", { tags: "@skip" }, () => {
|
||||
createDashboardWithQuestion({}, (dashboardId) =>
|
||||
H.visitDashboard(dashboardId),
|
||||
);
|
||||
cy.icon("pencil").click();
|
||||
H.showDashboardCardActions();
|
||||
cy.findByTestId("dashboardcard-actions-panel").within(() => {
|
||||
cy.icon("click").click();
|
||||
});
|
||||
|
||||
// configure clicks on "MY_NUMBER to update the param
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("On-click behavior for each column")
|
||||
.parent()
|
||||
.parent()
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
.within(() => cy.findByText("MY_NUMBER").click());
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Update a dashboard filter").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Pick one or more filters to update")
|
||||
.parent()
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
.within(() => cy.findByText("My Param").click());
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
H.selectDropdown().within(() => cy.findByText("MY_STRING").click());
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Save").click();
|
||||
|
||||
// click on table value
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("111").click();
|
||||
|
||||
// check that param was set to "foo"
|
||||
cy.location("search").should("eq", "?my_param=foo");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("My Param")
|
||||
.parent()
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
.within(() => cy.findByText("foo"));
|
||||
});
|
||||
|
||||
describe("should pass multiple filters for numeric column on drill-through (metabase#13062)", () => {
|
||||
const questionDetails = {
|
||||
name: "13062Q",
|
||||
|
||||
@@ -1934,102 +1934,6 @@ describe("issue 56716", () => {
|
||||
H.getDashboardCard().findByText("200 rows").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Issue 46337", () => {
|
||||
const MODEL_NAME = "Model 46337";
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.createQuestion({
|
||||
type: "model",
|
||||
name: MODEL_NAME,
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
fields: [
|
||||
[
|
||||
"field",
|
||||
ORDERS.ID,
|
||||
{
|
||||
"base-type": "type/BigInteger",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.TAX,
|
||||
{
|
||||
"base-type": "type/Float",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.TOTAL,
|
||||
{
|
||||
"base-type": "type/Float",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.DISCOUNT,
|
||||
{
|
||||
"base-type": "type/Float",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.QUANTITY,
|
||||
{
|
||||
"base-type": "type/Integer",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.CREATED_AT,
|
||||
{
|
||||
"base-type": "type/DateTime",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
ORDERS.PRODUCT_ID,
|
||||
{
|
||||
"base-type": "type/Integer",
|
||||
},
|
||||
],
|
||||
],
|
||||
joins: [
|
||||
{
|
||||
fields: "all",
|
||||
alias: "Products",
|
||||
"source-table": PEOPLE_ID,
|
||||
strategy: "left-join",
|
||||
condition: [
|
||||
"=",
|
||||
["field", ORDERS.USER_ID, {}],
|
||||
["field", PEOPLE.ID, { "join-alias": "Products" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}).then(({ body: model }) => {
|
||||
cy.visit(`/auto/dashboard/model/${model.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: unskip when metabase#46337 is fixed
|
||||
// See: https://github.com/metabase/metabase/issues/46337
|
||||
it("should (metabase#46337)", { tags: "@skip" }, () => {
|
||||
cy.log("ensure the dashcards render data not errors");
|
||||
|
||||
cy.findByTestId("dashboard-grid").within(() => {
|
||||
cy.findByText("There was a problem displaying this chart.").should(
|
||||
"not.exist",
|
||||
);
|
||||
cy.findByText(`Total ${MODEL_NAME}`).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function slowDownCardQuery() {
|
||||
return cy.intercept("POST", "/api/card/*/query", (req) => {
|
||||
req.on("response", (res) => {
|
||||
@@ -2246,7 +2150,9 @@ describe("issue 64138", () => {
|
||||
})
|
||||
.within(() => {
|
||||
cy.findByLabelText("Zoom in").should("not.exist");
|
||||
cy.findByText("Set as default view").should("be.visible").click();
|
||||
// the update button is disabled until the map is panned, so only
|
||||
// assert that it is shown while the zoom controls are hidden
|
||||
cy.findByText("Set as default view").should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("hovering marker icons should not open their tooltips");
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const { H } = cy;
|
||||
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import type { Collection, Dashboard, Tenant } from "metabase-types/api";
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
describe("scenarios > dashboard > admins using tenant-specific question picker", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.updateSetting("use-tenants", true);
|
||||
|
||||
cy.request("POST", "/api/ee/tenant", {
|
||||
name: "Acme",
|
||||
slug: "acme",
|
||||
})
|
||||
.its("body")
|
||||
.as("tenant");
|
||||
|
||||
cy.get<Tenant>("@tenant")
|
||||
.then(({ tenant_collection_id }) =>
|
||||
H.createCollection({
|
||||
name: "Tenant questions",
|
||||
parent_id: tenant_collection_id,
|
||||
}),
|
||||
)
|
||||
.its("body")
|
||||
.as("tenantCollection");
|
||||
|
||||
cy.get<Collection>("@tenantCollection").then((tenantCollection) =>
|
||||
H.createQuestion({
|
||||
name: "Tenant orders",
|
||||
collection_id: tenantCollection.id,
|
||||
query: { "source-table": ORDERS_ID },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows adding questions from the dashboard's tenant-specific collection (EMB-2312)", () => {
|
||||
cy.get<Tenant>("@tenant")
|
||||
.then(({ tenant_collection_id }) =>
|
||||
H.createDashboard({
|
||||
name: "Tenant dashboard",
|
||||
collection_id: tenant_collection_id,
|
||||
}),
|
||||
)
|
||||
.its("body")
|
||||
.as("dashboard");
|
||||
|
||||
cy.get<Dashboard>("@dashboard").then(({ id }) => {
|
||||
H.visitDashboard(id);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.should("contain", "Acme")
|
||||
.and("not.contain", "Unknown");
|
||||
|
||||
H.sidebar().findByText("Tenant questions").click();
|
||||
H.sidebar().findByText("Tenant orders").click();
|
||||
|
||||
H.getDashboardCards()
|
||||
.should("have.length", 1)
|
||||
.and("contain", "Tenant orders");
|
||||
});
|
||||
});
|
||||
|
||||
// Verify that Tenant collections and Our analytics are separate top-level entries.
|
||||
it("allows browsing into a tenant-specific collection from Our Analytics (EMB-2312)", () => {
|
||||
H.createDashboard({ name: "Our analytics dashboard" })
|
||||
.its("body")
|
||||
.as("dashboard");
|
||||
|
||||
cy.get<Dashboard>("@dashboard").then(({ id }) => {
|
||||
H.visitDashboard(id);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.should("contain", "Collections")
|
||||
.and("contain", "Our analytics");
|
||||
|
||||
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
|
||||
|
||||
H.sidebar().findByText("Tenant collections").click();
|
||||
H.sidebar().findByText("Acme").click();
|
||||
H.sidebar().findByText("Tenant questions").click();
|
||||
H.sidebar().findByText("Tenant orders").click();
|
||||
|
||||
H.getDashboardCards()
|
||||
.should("have.length", 1)
|
||||
.and("contain", "Tenant orders");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
const { H } = cy;
|
||||
|
||||
import { JWT_SHARED_SECRET } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
import type { Collection, Dashboard, Tenant, User } from "metabase-types/api";
|
||||
|
||||
const TENANT_USER = {
|
||||
first_name: "acme",
|
||||
last_name: "user",
|
||||
email: "acme.user@email.com",
|
||||
"@tenant": "acme",
|
||||
};
|
||||
|
||||
const loginAsTenantUser = (returnTo: string) => {
|
||||
cy.task<string>("signJwt", {
|
||||
payload: TENANT_USER,
|
||||
secret: JWT_SHARED_SECRET,
|
||||
}).then((key) =>
|
||||
cy.visit(`/auth/sso?return_to=${encodeURIComponent(returnTo)}&jwt=${key}`),
|
||||
);
|
||||
};
|
||||
|
||||
describe("scenarios > dashboard > tenant users using question picker", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"jwt-attribute-email": "email",
|
||||
"jwt-attribute-firstname": "first_name",
|
||||
"jwt-attribute-lastname": "last_name",
|
||||
"jwt-enabled": true,
|
||||
"jwt-identity-provider-uri": "localhost:4000",
|
||||
"jwt-shared-secret": JWT_SHARED_SECRET,
|
||||
"jwt-user-provisioning-enabled?": true,
|
||||
"use-tenants": true,
|
||||
});
|
||||
|
||||
cy.request("POST", "/api/ee/tenant", {
|
||||
name: "Acme",
|
||||
slug: "acme",
|
||||
})
|
||||
.its("body")
|
||||
.as("tenant");
|
||||
|
||||
H.createSharedTenantCollection("Finance")
|
||||
.its("body")
|
||||
.as("financeCollection");
|
||||
|
||||
H.createSharedTenantCollection("Marketing")
|
||||
.its("body")
|
||||
.as("marketingCollection");
|
||||
|
||||
cy.get<Tenant>("@tenant")
|
||||
.then(({ tenant_collection_id }) =>
|
||||
H.createCollection({
|
||||
name: "Tenant questions",
|
||||
parent_id: tenant_collection_id,
|
||||
}),
|
||||
)
|
||||
.its("body")
|
||||
.as("tenantCollection");
|
||||
});
|
||||
|
||||
// Tenant users do not know what is a 'Tenant-specific collection' or 'Shared collection'.
|
||||
// Only show a flattened structure for them, with "Our data" being their tenant collection.
|
||||
it("shows flattened tenant collections without grouping by namespaces (EMB-2312)", () => {
|
||||
cy.get<Collection>("@tenantCollection")
|
||||
.then(({ id }) =>
|
||||
H.createDashboard({
|
||||
name: "Tenant dashboard",
|
||||
collection_id: id,
|
||||
}),
|
||||
)
|
||||
.its("body")
|
||||
.as("dashboard");
|
||||
|
||||
cy.get<Dashboard>("@dashboard").then(({ id }) => {
|
||||
loginAsTenantUser(`/dashboard/${id}`);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.should("contain", "Collections")
|
||||
.and("contain", "Our data");
|
||||
|
||||
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
|
||||
|
||||
H.sidebar().findByText("Our data").should("be.visible");
|
||||
H.sidebar().findByText("Finance").should("be.visible");
|
||||
H.sidebar().findByText("Marketing").should("be.visible");
|
||||
H.sidebar().findByText("Our analytics").should("not.exist");
|
||||
H.sidebar().findByText("Shared collections").should("not.exist");
|
||||
H.sidebar().findByText("Tenant collections").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
// The API omits 'Our analytics' when tenant users cannot read it, but it still
|
||||
// returns their personal collection. Show that collection directly under the
|
||||
// synthetic collections level instead of adding another 'collections' folder.
|
||||
it("shows the personal collection under top-level collections (EMB-2312)", () => {
|
||||
loginAsTenantUser("/");
|
||||
|
||||
cy.request<User>("GET", "/api/user/current").its("body").as("tenantUser");
|
||||
|
||||
cy.get<User>("@tenantUser")
|
||||
.then(({ personal_collection_id }) => {
|
||||
if (personal_collection_id == null) {
|
||||
throw new Error("Tenant user has no personal collection");
|
||||
}
|
||||
|
||||
return H.createDashboard({
|
||||
name: "Personal dashboard",
|
||||
collection_id: personal_collection_id,
|
||||
});
|
||||
})
|
||||
.its("body")
|
||||
.as("personalDashboard");
|
||||
|
||||
cy.get<Dashboard>("@personalDashboard").then(({ id }) => {
|
||||
cy.intercept({
|
||||
method: "GET",
|
||||
pathname: "/api/collection/tree",
|
||||
query: {
|
||||
namespace: "shared-tenant-collection",
|
||||
"exclude-archived": "true",
|
||||
},
|
||||
}).as("sharedTenantCollections");
|
||||
|
||||
H.visitDashboard(id);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
cy.wait("@sharedTenantCollections");
|
||||
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.should("contain", "Collections")
|
||||
.and("contain", "My personal collection");
|
||||
|
||||
H.sidebar().findByTestId("breadcrumbs").findByText("Collections").click();
|
||||
|
||||
H.sidebar()
|
||||
.findByRole("menuitem", { name: "My personal collection" })
|
||||
.should("be.visible");
|
||||
|
||||
// There should not be a second nested "Collections" folder.
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.findAllByText("Collections")
|
||||
.should("have.length", 1);
|
||||
|
||||
H.sidebar()
|
||||
.findByRole("menuitem", { name: "Collections" })
|
||||
.should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1535,70 +1535,6 @@ function assertScrollBarExists() {
|
||||
});
|
||||
}
|
||||
|
||||
describe("LOCAL TESTING ONLY > dashboard", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
/**
|
||||
* WARNING:
|
||||
* https://github.com/metabase/metabase/issues/15656
|
||||
* - We are currently not able to test translations in CI
|
||||
* - DO NOT unskip this test even after the issue is fixed
|
||||
* - To be used for local testing only
|
||||
* - Make sure you have translation resources built first.
|
||||
* - Run `./bin/i18n/build-translation-resources`
|
||||
* - Then start the server and Cypress tests
|
||||
*/
|
||||
|
||||
it(
|
||||
"dashboard filter should not show placeholder for translated languages (metabase#15694)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
cy.request("GET", "/api/user/current").then(
|
||||
({ body: { id: USER_ID } }) => {
|
||||
cy.request("PUT", `/api/user/${USER_ID}`, { locale: "fr" });
|
||||
},
|
||||
);
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "15694",
|
||||
query: { "source-table": PEOPLE_ID },
|
||||
},
|
||||
dashboardDetails: {
|
||||
parameters: [
|
||||
{
|
||||
name: "Location",
|
||||
slug: "location",
|
||||
id: "5aefc725",
|
||||
type: "string/=",
|
||||
sectionId: "location",
|
||||
},
|
||||
],
|
||||
},
|
||||
}).then(({ body: { card_id, dashboard_id } }) => {
|
||||
H.addOrUpdateDashboardCard({
|
||||
card_id,
|
||||
dashboard_id,
|
||||
card: {
|
||||
parameter_mappings: [
|
||||
{
|
||||
parameter_id: "5aefc725",
|
||||
card_id,
|
||||
target: ["dimension", ["field", PEOPLE.STATE, null]],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.visit(`/dashboard/${dashboard_id}?location=AK&location=CA`);
|
||||
H.filterWidget().contains(/\{0\}/).should("not.exist");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("scenarios > dashboard > permissions", () => {
|
||||
let dashboardId;
|
||||
|
||||
|
||||
@@ -499,12 +499,12 @@ describe("scenarios > dashboard > title drill", () => {
|
||||
{ row: 6, col: 6, size_x: 6, size_y: 6 },
|
||||
],
|
||||
}).then(({ dashboard, questions }) => {
|
||||
H.visitDashboard(dashboard.id);
|
||||
// Park the cursor off the grid before the dashcards render: a pointer
|
||||
// resting over a title computes that title's href before it is asserted.
|
||||
cy.get("body").realHover({ position: "topLeft" });
|
||||
|
||||
// make cursor start from a place where subsequent realHover() calls
|
||||
// won't make the cursor move over the other cards during test
|
||||
// (which would interfere with assertions)
|
||||
cy.findByTestId("sidebar-toggle").realHover();
|
||||
H.visitDashboard(dashboard.id);
|
||||
H.waitForDashcardsToLoad({ count: 4 });
|
||||
|
||||
H.getDashboardCard(0)
|
||||
.findByRole("link", { name: "Line chart" })
|
||||
@@ -519,35 +519,58 @@ describe("scenarios > dashboard > title drill", () => {
|
||||
.findByRole("link", { name: "Funnel chart" })
|
||||
.as("funnel-chart-title");
|
||||
|
||||
assertTitleHrefOnFocus({
|
||||
elementAlias: "@line-chart-title",
|
||||
href: `/question/${questions[0].id}-line-chart`,
|
||||
});
|
||||
assertTitleHrefOnFocus({
|
||||
elementAlias: "@row-chart-title",
|
||||
href: `/question/${questions[1].id}-row-chart`,
|
||||
});
|
||||
assertTitleHrefOnHover({
|
||||
elementAlias: "@map-chart-title",
|
||||
href: `/question/${questions[2].id}-map-chart`,
|
||||
});
|
||||
assertTitleHrefOnHover({
|
||||
elementAlias: "@funnel-chart-title",
|
||||
href: `/question/${questions[3].id}-funnel-chart`,
|
||||
const titles = [
|
||||
{
|
||||
elementAlias: "@line-chart-title",
|
||||
href: `/question/${questions[0].id}-line-chart`,
|
||||
trigger: focusTitle,
|
||||
},
|
||||
{
|
||||
elementAlias: "@row-chart-title",
|
||||
href: `/question/${questions[1].id}-row-chart`,
|
||||
trigger: focusTitle,
|
||||
},
|
||||
{
|
||||
elementAlias: "@map-chart-title",
|
||||
href: `/question/${questions[2].id}-map-chart`,
|
||||
trigger: hoverTitle,
|
||||
},
|
||||
{
|
||||
elementAlias: "@funnel-chart-title",
|
||||
href: `/question/${questions[3].id}-funnel-chart`,
|
||||
trigger: hoverTitle,
|
||||
},
|
||||
];
|
||||
|
||||
cy.log("every title is an inert placeholder before focus and hover");
|
||||
titles.forEach(({ elementAlias }) => {
|
||||
cy.get(elementAlias).should("have.attr", "href", "#");
|
||||
});
|
||||
|
||||
cy.log("focusing or hovering a title turns it into a question anchor");
|
||||
titles.forEach(assertTitleBecomesQuestionAnchor);
|
||||
});
|
||||
});
|
||||
|
||||
function assertTitleHrefOnFocus({ elementAlias, href }) {
|
||||
cy.get(elementAlias).should("have.attr", "href", "#");
|
||||
cy.get(elementAlias).focus();
|
||||
cy.get(elementAlias).should("have.attr", "href", href);
|
||||
function focusTitle(element) {
|
||||
element.focus();
|
||||
}
|
||||
|
||||
function assertTitleHrefOnHover({ elementAlias, href }) {
|
||||
cy.get(elementAlias).should("have.attr", "href", "#");
|
||||
cy.get(elementAlias).realHover();
|
||||
cy.get(elementAlias).should("have.attr", "href", href);
|
||||
function hoverTitle(element) {
|
||||
const { MouseEvent } = element.ownerDocument.defaultView;
|
||||
element.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* The question href is computed lazily on focus/mouseenter, and the anchor
|
||||
* is replaced when it resolves. Re-evaluating the trigger on each retry
|
||||
* keeps the assertion anchored to the node currently in the DOM.
|
||||
*/
|
||||
function assertTitleBecomesQuestionAnchor({ elementAlias, href, trigger }) {
|
||||
cy.get(elementAlias).should(($el) => {
|
||||
trigger($el[0]);
|
||||
expect($el).to.have.attr("href", href);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
const { H } = cy;
|
||||
|
||||
import {
|
||||
PRODUCTS_AVERAGE_BY_CATEGORY,
|
||||
PRODUCTS_COUNT_BY_CATEGORY,
|
||||
} from "e2e/support/test-visualizer-data";
|
||||
|
||||
describe("scenarios > dashboard > visualizer > filters", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
|
||||
"dashcardQuery",
|
||||
);
|
||||
|
||||
cy.signInAsNormalUser();
|
||||
|
||||
H.createQuestion(PRODUCTS_COUNT_BY_CATEGORY, {
|
||||
idAlias: "productsCountByCategoryQuestionId",
|
||||
wrapId: true,
|
||||
});
|
||||
|
||||
H.createQuestion(PRODUCTS_AVERAGE_BY_CATEGORY, {
|
||||
idAlias: "productsAvgByCreatedAtQuestionId",
|
||||
wrapId: true,
|
||||
});
|
||||
});
|
||||
|
||||
// TODO those two datasets should be compatible with each other
|
||||
it(
|
||||
"should create and update a dashcard with 'Visualize another way' button",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.createDashboard().then(({ body: { id: dashboardId } }) => {
|
||||
H.visitDashboard(dashboardId);
|
||||
});
|
||||
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
H.clickVisualizeAnotherWay(PRODUCTS_COUNT_BY_CATEGORY.name);
|
||||
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
H.selectDataset(PRODUCTS_AVERAGE_BY_CATEGORY.name);
|
||||
|
||||
H.assertWellItemsCount({
|
||||
vertical: 2,
|
||||
});
|
||||
});
|
||||
|
||||
H.saveDashcardVisualizerModal({ mode: "create" });
|
||||
|
||||
H.setFilter("Text or Category", "Is");
|
||||
|
||||
// Doing it twice to populate the two filters
|
||||
H.selectDashboardFilter(H.getDashboardCard(0), "Category");
|
||||
H.selectDashboardFilter(H.getDashboardCard(0), "Category");
|
||||
|
||||
H.saveDashboard();
|
||||
|
||||
H.getDashboardCard(0).within(() => {
|
||||
cy.findByText("Doohickey").should("exist");
|
||||
});
|
||||
|
||||
H.filterWidget().contains("Text").click();
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Gadget").click();
|
||||
cy.button("Add filter").click();
|
||||
});
|
||||
|
||||
H.getDashboardCard(0).within(() => {
|
||||
cy.findByText("Doohickey").should("not.exist");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -328,8 +328,20 @@ describe("bulk table operations", { viewportWidth: 1600 }, () => {
|
||||
cy.log(
|
||||
"Expand the rows up front - we'll need them later for the assertion",
|
||||
);
|
||||
TablePicker.getDatabase("Writable Postgres12").should("be.visible").click();
|
||||
// The picker tree keeps mounting after the databases request resolves, so
|
||||
// clicking the database row before its expand handler is wired drops the click
|
||||
// and the schema fetch that populates the tables never fires. Wait for the
|
||||
// expand toggle to render collapsed, click it, then confirm it expanded so the
|
||||
// schema request reliably occurs before we select the database.
|
||||
TablePicker.getDatabaseToggle("Writable Postgres12")
|
||||
.should("have.attr", "aria-expanded", "false")
|
||||
.click();
|
||||
cy.wait("@getSchema");
|
||||
TablePicker.getDatabaseToggle("Writable Postgres12").should(
|
||||
"have.attr",
|
||||
"aria-expanded",
|
||||
"true",
|
||||
);
|
||||
|
||||
TablePicker.getDatabase("Writable Postgres12")
|
||||
.find('input[type="checkbox"]')
|
||||
|
||||
@@ -407,6 +407,7 @@ describe("scenarios > data studio > library > metrics", () => {
|
||||
H.selectDropdown()
|
||||
.findByRole("option", { name: /Duration/ })
|
||||
.click();
|
||||
H.fillCacheDuration(24);
|
||||
H.modal().findByTestId("strategy-form-submit-button").click();
|
||||
|
||||
cy.wait("@updateCacheConfig");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import { ADMIN_PERSONAL_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import type { TransformId } from "metabase-types/api";
|
||||
import type { DependencyNode, TransformId } from "metabase-types/api";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
@@ -505,11 +505,24 @@ function createModelContent() {
|
||||
}
|
||||
|
||||
function waitForBreakingDependencies() {
|
||||
H.waitForBreakingDependencies(
|
||||
(nodes) => nodes.length >= BROKEN_DEPENDENCIES.length,
|
||||
// The instance already has unrelated (Usage Analytics) breaking dependencies,
|
||||
// so a bare count check can pass before the test's own entities are analyzed.
|
||||
// Wait until every expected entity is present by name instead.
|
||||
H.waitForBreakingDependencies((nodes) =>
|
||||
BROKEN_DEPENDENCIES.every((name) =>
|
||||
nodes.some((node) => getNodeName(node) === name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeName(node: DependencyNode): string | undefined {
|
||||
const { data } = node;
|
||||
if ("display_name" in data && data.display_name) {
|
||||
return data.display_name;
|
||||
}
|
||||
return "name" in data ? data.name : undefined;
|
||||
}
|
||||
|
||||
function checkList({
|
||||
visibleEntities = [],
|
||||
hiddenEntities = [],
|
||||
|
||||
@@ -6,178 +6,6 @@ import { defer } from "metabase/utils/promise";
|
||||
const { PRODUCTS, PRODUCTS_ID, ORDERS, ORDERS_ID, FEEDBACK, FEEDBACK_ID } =
|
||||
SAMPLE_DATABASE;
|
||||
|
||||
describe("issue 15860", { tags: "@skip" }, () => {
|
||||
const q1IdFilter = {
|
||||
name: "Q1 ID",
|
||||
slug: "q1_id",
|
||||
id: "fde6db8b",
|
||||
type: "id",
|
||||
sectionId: "id",
|
||||
default: [1],
|
||||
};
|
||||
|
||||
const q1CategoryFilter = {
|
||||
name: "Q1 Category",
|
||||
slug: "q1_category",
|
||||
id: "e8ff3175",
|
||||
type: "string/=",
|
||||
sectionId: "string",
|
||||
filteringParameters: [q1IdFilter.id],
|
||||
};
|
||||
|
||||
const q2IdFilter = {
|
||||
name: "Q2 ID",
|
||||
slug: "q2_id",
|
||||
id: "t3e6hb7b",
|
||||
type: "id",
|
||||
sectionId: "id",
|
||||
default: [3],
|
||||
};
|
||||
|
||||
const q2CategoryFilter = {
|
||||
name: "Q2 Category",
|
||||
slug: "q2_category",
|
||||
id: "ca1n357o",
|
||||
type: "string/=",
|
||||
sectionId: "string",
|
||||
filteringParameters: [q2IdFilter.id],
|
||||
};
|
||||
|
||||
function setDefaultValueForLockedFilter(filter, value) {
|
||||
cy.findByText("Previewing locked parameters")
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.findByText(filter).click({ force: true });
|
||||
});
|
||||
|
||||
cy.findByPlaceholderText("Enter an ID").type(`${value}{enter}`);
|
||||
cy.button("Add filter").click();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Q1",
|
||||
query: { "source-table": PRODUCTS_ID },
|
||||
},
|
||||
dashboardDetails: {
|
||||
embedding_params: {
|
||||
q1_id: "locked",
|
||||
q1_category: "enabled",
|
||||
q2_id: "locked",
|
||||
q2_category: "enabled",
|
||||
},
|
||||
enable_embedding: true,
|
||||
parameters: [
|
||||
q1IdFilter,
|
||||
q1CategoryFilter,
|
||||
q2IdFilter,
|
||||
q2CategoryFilter,
|
||||
],
|
||||
},
|
||||
cardDetails: {
|
||||
size_x: 11,
|
||||
size_y: 6,
|
||||
},
|
||||
}).then(({ body: { card_id: q1, dashboard_id } }) => {
|
||||
// Create a second question with the same source table
|
||||
H.createQuestion({
|
||||
name: "Q2",
|
||||
query: { "source-table": PRODUCTS_ID },
|
||||
}).then(({ body: { id: q2 } }) => {
|
||||
H.updateDashboardCards({
|
||||
dashboard_id,
|
||||
cards: [
|
||||
// Add card for second question with parameter mappings
|
||||
{
|
||||
card_id: q2,
|
||||
row: 0,
|
||||
col: 8,
|
||||
size_x: 13,
|
||||
size_y: 6,
|
||||
parameter_mappings: [
|
||||
{
|
||||
parameter_id: q2IdFilter.id,
|
||||
card_id: q2,
|
||||
target: ["dimension", ["field", PRODUCTS.ID, null]],
|
||||
},
|
||||
{
|
||||
parameter_id: q2CategoryFilter.id,
|
||||
card_id: q2,
|
||||
target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Add parameter mappings to first question's card
|
||||
{
|
||||
card_id: q1,
|
||||
parameter_mappings: [
|
||||
{
|
||||
parameter_id: q1IdFilter.id,
|
||||
card_id: q1,
|
||||
target: ["dimension", ["field", PRODUCTS.ID, null]],
|
||||
},
|
||||
{
|
||||
parameter_id: q1CategoryFilter.id,
|
||||
card_id: q1,
|
||||
target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
|
||||
embedding_params: {
|
||||
q1_id: "locked",
|
||||
q1_category: "enabled",
|
||||
q2_id: "locked",
|
||||
q2_category: "enabled",
|
||||
},
|
||||
enable_embedding: true,
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should work for locked linked filters connected to different cards with the same source table (metabase#15860)", () => {
|
||||
setDefaultValueForLockedFilter("Q1 ID", 1);
|
||||
setDefaultValueForLockedFilter("Q2 ID", 3);
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Q1 Category").click();
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByRole("listitem")
|
||||
.should("have.length", 1)
|
||||
.and("contain", "Gizmo");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Q2 Category").click();
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByRole("listitem")
|
||||
.should("have.length", 1)
|
||||
.and("contain", "Doohickey");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 20438", () => {
|
||||
const questionDetails = {
|
||||
name: "20438",
|
||||
@@ -1003,45 +831,6 @@ describe("issue 40660", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Skipped since it does not make sense when CSP is disabled
|
||||
describe("issue 49142", { tags: "@skip" }, () => {
|
||||
const questionDetails = {
|
||||
name: "Products",
|
||||
query: { "source-table": PRODUCTS_ID, limit: 2 },
|
||||
};
|
||||
|
||||
const dashboardDetails = {
|
||||
name: "Embeddable dashboard",
|
||||
enable_embedding: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("embedding preview should be always working", () => {
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails,
|
||||
dashboardDetails,
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitDashboard(dashboard_id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "lookAndFeel",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
cy.findByTestId("embed-preview-iframe")
|
||||
.its("0.contentDocument.body")
|
||||
.should("be.visible")
|
||||
.and("contain", "Embeddable dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 8490", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -106,30 +106,6 @@ describe(suiteTitle, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows exploration template when selected", { tags: "@skip" }, () => {
|
||||
visitNewEmbedPage();
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Metabase account (SSO)").click();
|
||||
|
||||
cy.findByText("Exploration").click();
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_experience_completed",
|
||||
event_detail:
|
||||
"authType=sso,experience=exploration,isDefaultExperience=false",
|
||||
});
|
||||
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.log("data picker is visible");
|
||||
cy.findByText("Pick your starting data").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows browser template when selected", () => {
|
||||
visitNewEmbedPage();
|
||||
|
||||
@@ -193,38 +169,6 @@ describe(suiteTitle, () => {
|
||||
cy.findByText("Person detail").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"shows question of id=1 when activity log is empty and chart is selected",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
visitNewEmbedPage();
|
||||
cy.wait("@emptyRecentItems");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Guest").click();
|
||||
});
|
||||
embedModalEnableEmbedding();
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Chart").click();
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_experience_completed",
|
||||
event_detail:
|
||||
"authType=guest-embed,experience=chart,isDefaultExperience=false",
|
||||
});
|
||||
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.log("question title of id=1 is visible");
|
||||
cy.findByText("Query log").should("be.visible");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should show a fake loading indicator in embed preview", () => {
|
||||
|
||||
@@ -10,6 +10,21 @@ const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const LIMITED_ORDERS_ROW_COUNT = 5;
|
||||
|
||||
const clickEmbeddedTableCell = (value: string, rowsCount: number) => {
|
||||
// Scoped to the table on purpose: column auto-sizing renders a hidden copy of
|
||||
// the cells on the iframe's body and drops it once measured, so a body-wide
|
||||
// query can latch onto a copy that is about to detach.
|
||||
const tableRoot = () =>
|
||||
H.getSimpleEmbedIframeContent().findByTestId("table-root");
|
||||
|
||||
// Anchor on the fully rendered table before interacting with it.
|
||||
tableRoot().should("have.attr", "data-rows-count", String(rowsCount));
|
||||
|
||||
tableRoot().findByText(value).should("be.visible").click({ force: true });
|
||||
};
|
||||
|
||||
describe("scenarios > embedding > sdk iframe embedding > custom elements api", () => {
|
||||
beforeEach(() => {
|
||||
cy.signInAsAdmin();
|
||||
@@ -160,7 +175,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Limited Orders",
|
||||
query: { "source-table": ORDERS_ID, limit: 5 },
|
||||
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
|
||||
},
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitCustomHtmlPage(`
|
||||
@@ -169,11 +184,10 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
<metabase-dashboard dashboard-id="${dashboard_id}" drills />
|
||||
`);
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findAllByText("37.65")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.wait("@getDashCardQuery");
|
||||
|
||||
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByText(/Filter by this value/)
|
||||
.should("be.visible");
|
||||
@@ -184,7 +198,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Limited Orders",
|
||||
query: { "source-table": ORDERS_ID, limit: 5 },
|
||||
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
|
||||
},
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitCustomHtmlPage(`
|
||||
@@ -195,11 +209,8 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
|
||||
cy.wait("@getDashCardQuery");
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findAllByText("37.65")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click({ force: true });
|
||||
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByText(/Filter by this value/)
|
||||
.should("not.exist");
|
||||
@@ -294,7 +305,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
it("should enable drill-through when drills is true", () => {
|
||||
H.createQuestion({
|
||||
name: "Limited Orders",
|
||||
query: { "source-table": ORDERS_ID, limit: 5 },
|
||||
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
|
||||
}).then(({ body: { id: questionId } }) => {
|
||||
H.visitCustomHtmlPage(`
|
||||
${H.getNewEmbedScriptTag()}
|
||||
@@ -304,17 +315,8 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
|
||||
cy.wait("@getCardQuery");
|
||||
|
||||
// Wait for the table to finish rendering before interacting,
|
||||
// as column auto-sizing can cause re-renders that detach elements.
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByTestId("table-root")
|
||||
.should("have.attr", "data-rows-count", "5");
|
||||
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findAllByText("37.65")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.click({ force: true });
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByText(/Filter by this value/)
|
||||
.should("be.visible");
|
||||
@@ -324,7 +326,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
it("should disable drill-through when drills is false", () => {
|
||||
H.createQuestion({
|
||||
name: "Limited Orders",
|
||||
query: { "source-table": ORDERS_ID, limit: 5 },
|
||||
query: { "source-table": ORDERS_ID, limit: LIMITED_ORDERS_ROW_COUNT },
|
||||
}).then(({ body: { id: questionId } }) => {
|
||||
H.visitCustomHtmlPage(`
|
||||
${H.getNewEmbedScriptTag()}
|
||||
@@ -334,16 +336,7 @@ describe("scenarios > embedding > sdk iframe embedding > custom elements api", (
|
||||
|
||||
cy.wait("@getCardQuery");
|
||||
|
||||
// Wait for the table to finish rendering before interacting,
|
||||
// as column auto-sizing can cause re-renders that detach elements.
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByTestId("table-root")
|
||||
.should("have.attr", "data-rows-count", "5");
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByText("37.65")
|
||||
.should("be.visible")
|
||||
.click({ force: true });
|
||||
clickEmbeddedTableCell("37.65", LIMITED_ORDERS_ROW_COUNT);
|
||||
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByText(/Filter by this value/)
|
||||
|
||||
@@ -335,20 +335,19 @@ describe("scenarios > embedding > sdk iframe embed options passthrough", () => {
|
||||
|
||||
frame.within(() => {
|
||||
cy.findByText("Pick your starting data").should("be.visible");
|
||||
H.popover().findByText("Orders").click();
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Visualize" }).click();
|
||||
// if we don't limit this query, layout shifts can push the first row out of the viewport
|
||||
cy.icon("list").click();
|
||||
cy.findByPlaceholderText("Enter a limit").type("5").blur({ force: true }); // blur hack
|
||||
H.visualize();
|
||||
|
||||
cy.log("1. clicking on the filter should drill down");
|
||||
cy.findAllByText("37.65").first().should("be.visible").click();
|
||||
cy.findByText(/Filter by this value/).should("be.visible");
|
||||
cy.findByTestId("click-actions-filter-section")
|
||||
.find("button")
|
||||
.first()
|
||||
.click();
|
||||
cy.findByText("37.65").should("be.visible").click();
|
||||
H.popover().within(() => {
|
||||
cy.findByText(/Filter by this value/).should("be.visible");
|
||||
cy.button("<").click();
|
||||
});
|
||||
cy.findAllByText("29.8").first().should("be.visible");
|
||||
cy.findByTestId("interactive-question-result-toolbar").should(
|
||||
"be.visible",
|
||||
|
||||
@@ -414,67 +414,6 @@ describe("issue 12985 > dashboard filter dropdown/search", () => {
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Ergonomic Silk Coat");
|
||||
});
|
||||
|
||||
it(
|
||||
"should work for aggregated questions (metabase#12985-2)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
const questionDetails = {
|
||||
name: "12985-v2",
|
||||
query: {
|
||||
"source-query": {
|
||||
"source-table": PRODUCTS_ID,
|
||||
aggregation: [["count"]],
|
||||
breakout: [["field", PRODUCTS.CATEGORY, null]],
|
||||
},
|
||||
filter: [">", ["field", "count", { "base-type": "type/Integer" }], 1],
|
||||
},
|
||||
};
|
||||
|
||||
H.createQuestionAndDashboard({ questionDetails, dashboardDetails }).then(
|
||||
({ body: { id, card_id, dashboard_id } }) => {
|
||||
cy.log("Connect dashboard filter to the aggregated card");
|
||||
|
||||
cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
|
||||
dashcards: [
|
||||
{
|
||||
id,
|
||||
card_id,
|
||||
row: 0,
|
||||
col: 0,
|
||||
size_x: 11,
|
||||
size_y: 6,
|
||||
series: [],
|
||||
visualization_settings: {},
|
||||
// Connect filter to the card
|
||||
parameter_mappings: [
|
||||
{
|
||||
parameter_id: categoryFilter.id,
|
||||
card_id,
|
||||
target: [
|
||||
"dimension",
|
||||
["field", "CATEGORY", { "base-type": "type/Text" }],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
},
|
||||
);
|
||||
|
||||
H.filterWidget().contains("Category").click();
|
||||
// It will fail at this point until the issue is fixed because popover never appears
|
||||
H.popover().contains("Gadget").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Add filter").click();
|
||||
cy.url().should("contain", "?category=Gadget");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Ergonomic Silk Coat");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("issues 15119 and 16112", () => {
|
||||
|
||||
@@ -1266,63 +1266,6 @@ describe("issue 17061", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO ranquild unskip after v54 release
|
||||
describe("issue 48824", { tags: "@skip" }, () => {
|
||||
const dateParameter = {
|
||||
id: "abc",
|
||||
name: "Date filter",
|
||||
slug: "filter-date",
|
||||
type: "date/all-options",
|
||||
default: "past30days-from-7days",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
});
|
||||
|
||||
it("should correctly translate relative filters in dashboards (metabase#48824)", () => {
|
||||
cy.log("set locale");
|
||||
cy.request("GET", "/api/user/current").then(({ body: user }) => {
|
||||
cy.request("PUT", `/api/user/${user.id}`, { locale: "en-ZZ" });
|
||||
});
|
||||
|
||||
cy.log("add a date parameter with a relative default value to a dashboard");
|
||||
cy.request("PUT", `/api/dashboard/${ORDERS_DASHBOARD_ID}`, {
|
||||
parameters: [dateParameter],
|
||||
});
|
||||
H.updateDashboardCards({
|
||||
dashboard_id: ORDERS_DASHBOARD_ID,
|
||||
cards: [
|
||||
{
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
parameter_mappings: [
|
||||
{
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
parameter_id: dateParameter.id,
|
||||
target: ["dimension", ["field", ORDERS.CREATED_AT, null]],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.log("check translations");
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID, {
|
||||
params: { [dateParameter.slug]: "past30days" },
|
||||
});
|
||||
|
||||
cy.log("Previous 30 days");
|
||||
H.filterWidget().findByText("[zz] Previous 30 days").should("be.visible");
|
||||
H.filterWidget().icon("revert").click();
|
||||
|
||||
cy.log("Previous 30 days, starting 7 days ago");
|
||||
H.filterWidget()
|
||||
.findByText("[zz] Previous 30 days, [zz] starting 7 days ago")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 62627", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -357,57 +357,6 @@ describe("issue 45410", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("issue 26861", { tags: "@skip" }, () => {
|
||||
const filter = {
|
||||
id: "a3b95feb-b6d2-33b6-660b-bb656f59b1d7",
|
||||
name: "filter",
|
||||
"display-name": "Filter",
|
||||
type: "dimension",
|
||||
dimension: ["field", ORDERS.CREATED_AT, null],
|
||||
"widget-type": "date/all-options",
|
||||
default: null,
|
||||
};
|
||||
|
||||
const nativeQuery = {
|
||||
name: "26861",
|
||||
native: {
|
||||
query: "select * from orders where {{filter}} limit 2",
|
||||
"template-tags": {
|
||||
filter,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createNativeQuestion(nativeQuery, { visitQuestion: true });
|
||||
});
|
||||
|
||||
it("exclude filter shouldn't break native questions with field filters (metabase#26861)", () => {
|
||||
H.filterWidget().click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Exclude…").click();
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Days of the week…").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Tuesday").click();
|
||||
|
||||
cy.button("Update filter").click();
|
||||
// In all other places in application, POST /api/dataset fires immediately after "Update filter"
|
||||
// A part of this bug is that we have to manually run the query so the next step will fail
|
||||
cy.wait("@dataset");
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("CREATED_AT excludes Tuesday");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("117.03").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 27123", () => {
|
||||
const questionDetails = {
|
||||
|
||||
@@ -466,35 +466,6 @@ describe("scenarios > question > filter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"column filters should work for metrics (metabase#15333)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.visitQuestionAdhoc({
|
||||
dataset_query: {
|
||||
type: "query",
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
aggregation: [["count"]],
|
||||
breakout: [["field-id", PRODUCTS.CATEGORY]],
|
||||
},
|
||||
database: SAMPLE_DB_ID,
|
||||
},
|
||||
display: "table",
|
||||
});
|
||||
|
||||
cy.get("[data-testid=cell-data]").contains("Count").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Filter by this column").click();
|
||||
cy.findByPlaceholderText("Enter a number").type("42");
|
||||
cy.button("Update filter").should("not.be.disabled").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Doohickey");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Gizmo").should("not.exist");
|
||||
},
|
||||
);
|
||||
|
||||
it("custom expression filter should reference fields by their name, not by their id (metabase#15748)", () => {
|
||||
H.openOrdersTable({ mode: "notebook" });
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import type { StructuredQuestionDetails } from "e2e/support/helpers";
|
||||
import type { NativeQuestionDetails } from "e2e/support/helpers/api/createQuestion";
|
||||
|
||||
import {
|
||||
openDetailsSidebar,
|
||||
turnIntoModel,
|
||||
} from "./helpers/e2e-models-helpers";
|
||||
import { turnIntoModel } from "./helpers/e2e-models-helpers";
|
||||
|
||||
const { ORDERS_ID, ORDERS, PRODUCTS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
@@ -103,110 +100,6 @@ describe("issue 20045", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 20624", { tags: "@skip" }, () => {
|
||||
const renamedColumn = "TITLE renamed";
|
||||
|
||||
const questionDetails: NativeQuestionDetails = {
|
||||
name: "20624",
|
||||
type: "model",
|
||||
native: { query: "select * from PRODUCTS limit 2" },
|
||||
visualization_settings: {
|
||||
column_settings: { '["name","TITLE"]': { column_title: renamedColumn } },
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("PUT", "/api/card/*").as("updateCard");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createNativeQuestion(questionDetails, { visitQuestion: true });
|
||||
});
|
||||
|
||||
it("models metadata should override previously defined column settings (metabase#20624)", () => {
|
||||
openDetailsSidebar();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Customize metadata").click();
|
||||
|
||||
// Open settings for this column
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(renamedColumn).click();
|
||||
// Let's set a new name for it
|
||||
cy.findByDisplayValue(renamedColumn).clear().type("Foo").blur();
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.get("[data-testid=cell-data]").should("contain", "Foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 22517", () => {
|
||||
function renameColumn(column: string, newName: string) {
|
||||
cy.findByDisplayValue(column).clear().type(newName).blur();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
cy.intercept("PUT", "/api/card/*").as("updateMetadata");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: "22517",
|
||||
native: { query: "select * from orders" },
|
||||
type: "model",
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
H.openQuestionActions();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Edit metadata").click();
|
||||
|
||||
renameColumn("ID", "Foo");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.wait("@updateMetadata");
|
||||
});
|
||||
|
||||
it(
|
||||
"adding or removing a column should not drop previously edited metadata (metabase#22517)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.openQuestionActions();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Edit query definition").click();
|
||||
|
||||
// Make sure previous metadata changes are reflected in the UI
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Foo");
|
||||
|
||||
// This will edit the original query and add the `SIZE` column
|
||||
// Updated query: `select *, case when quantity > 4 then 'large' else 'small' end size from orders`
|
||||
H.NativeEditor.focus().type(
|
||||
"{leftarrow}".repeat(" from orders".length) +
|
||||
", case when quantity > 4 then 'large' else 'small' end size ",
|
||||
);
|
||||
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Foo");
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Save changes").click();
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Foo");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("issue 22518", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
const { H } = cy;
|
||||
import { SAMPLE_DB_ID, SAMPLE_DB_SCHEMA_ID } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
ORDERS_DASHBOARD_ID,
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
import { turnIntoModel } from "./helpers/e2e-models-helpers";
|
||||
|
||||
const { ORDERS_ID, REVIEWS, REVIEWS_ID, PRODUCTS } = SAMPLE_DATABASE;
|
||||
|
||||
describe("issue 19737", () => {
|
||||
@@ -109,48 +107,6 @@ describe("issue 19737", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 22519", { tags: "@skip" }, () => {
|
||||
const questionDetails = {
|
||||
query: {
|
||||
"source-table": REVIEWS_ID,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("PUT", "/api/field/*").as("updateField");
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.DataModel.visit({
|
||||
databaseId: SAMPLE_DB_ID,
|
||||
schemaName: SAMPLE_DB_SCHEMA_ID,
|
||||
tableId: REVIEWS_ID,
|
||||
fieldId: REVIEWS.REVIEWS,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Don't cast").click();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("UNIX seconds → Datetime").click();
|
||||
cy.wait("@updateField");
|
||||
});
|
||||
|
||||
it("model query should not fail when data model is using casting (metabase#22519)", () => {
|
||||
H.createQuestion(questionDetails, { visitQuestion: true });
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("xavier");
|
||||
|
||||
turnIntoModel();
|
||||
|
||||
cy.wait("@dataset");
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("xavier");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 23024", () => {
|
||||
function addModelToDashboardAndVisit() {
|
||||
H.createDashboard().then(({ body: { id } }) => {
|
||||
|
||||
@@ -201,72 +201,6 @@ describe("issue 12581", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 13961", { tags: "@skip" }, () => {
|
||||
const categoryFilter = {
|
||||
id: "00315d5e-4a41-99da-1a41-e5254dacff9d",
|
||||
name: "category",
|
||||
"display-name": "Category",
|
||||
type: "dimension",
|
||||
default: "Doohickey",
|
||||
dimension: ["field", PRODUCTS.CATEGORY, null],
|
||||
"widget-type": "category",
|
||||
};
|
||||
|
||||
const productIdFilter = {
|
||||
id: "4775bccc-e82a-4069-fc6b-2acc90aadb8b",
|
||||
name: "prodid",
|
||||
"display-name": "ProdId",
|
||||
type: "number",
|
||||
default: null,
|
||||
};
|
||||
|
||||
const nativeQuery = {
|
||||
name: "13961",
|
||||
native: {
|
||||
query:
|
||||
"SELECT * FROM PRODUCTS WHERE 1=1 AND {{category}} [[AND ID={{prodid}}]]",
|
||||
"template-tags": {
|
||||
category: categoryFilter,
|
||||
prodid: productIdFilter,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createNativeQuestion(nativeQuery, { visitQuestion: true });
|
||||
});
|
||||
|
||||
it("should clear default filter value in native questions (metabase#13961)", () => {
|
||||
cy.findAllByText("Small Marble Shoes"); // Product ID 2, Doohickey
|
||||
|
||||
cy.location("search").should("eq", "?category=Doohickey");
|
||||
|
||||
// Remove default filter (category)
|
||||
H.filterWidget().findByRole("button").click();
|
||||
|
||||
cy.icon("play").first().should("be.visible").as("rerunQuestion").click();
|
||||
cy.wait("@cardQuery");
|
||||
|
||||
cy.url().should("not.include", "?category=Doohickey");
|
||||
|
||||
// Add value `1` to the ID filter
|
||||
cy.findByPlaceholderText(productIdFilter["display-name"]).type("1");
|
||||
|
||||
cy.get("@rerunQuestion").click();
|
||||
cy.wait("@cardQuery");
|
||||
|
||||
cy.log("Reported tested and failing on v0.34.3 through v0.37.3");
|
||||
cy.log("URL is correct at this point, but there are no results");
|
||||
|
||||
cy.location("search").should("eq", `?${productIdFilter.name}=1`);
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Rustic Paper Wallet"); // Product ID 1, Gizmo
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 14302", () => {
|
||||
const priceFilter = {
|
||||
id: "39b51ccd-47a7-9df6-a1c5-371918352c79",
|
||||
|
||||
@@ -218,26 +218,6 @@ describe("scenarios > filters > sql filters > basic filter types", () => {
|
||||
.findByPlaceholderText("Testingparamvisbility77")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
// flaky test (#19454)
|
||||
it(
|
||||
"should show an info popover when hovering over fields in the field filter field picker",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{cat}}");
|
||||
|
||||
SQLFilter.openTypePickerFromDefaultFilterType();
|
||||
SQLFilter.chooseType("Field Filter");
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText("People").click();
|
||||
cy.findByText("City").trigger("mouseenter");
|
||||
});
|
||||
|
||||
H.popover().contains("City");
|
||||
H.popover().contains("1,966 distinct values");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("scenarios > filters > sql filters > multiple values", () => {
|
||||
|
||||
@@ -294,114 +294,6 @@ describe("issue 20044", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 20625", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.updateSetting("native-query-autocomplete-match-style", "prefix");
|
||||
cy.signInAsNormalUser();
|
||||
});
|
||||
|
||||
// realpress messes with cypress 13
|
||||
it("should continue to request more prefix matches from the server when the limit was hit (metabase#20625)", () => {
|
||||
cy.intercept("GET", "/api/database/*/autocomplete_suggestions**", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
// This result has 50 items, which is the limit
|
||||
// as set by the backend.
|
||||
// This is needed to trigger the second autocomplete.
|
||||
["ORDERS", "Table"],
|
||||
["PEOPLE", "Table"],
|
||||
["REVIEWS", "Table"],
|
||||
["ACTIVE_SUBSCRIPTION", "ACCOUNTS :type/Boolean :type/Category"],
|
||||
["ADDRESS", "PEOPLE :type/Text"],
|
||||
["BIRTH_DATE", "PEOPLE :type/Date"],
|
||||
["BUTTON_LABEL", "ANALYTIC_EVENTS :type/Text :type/Category"],
|
||||
["CANCELED_AT", "ACCOUNTS :type/DateTime :type/CancelationTimestamp"],
|
||||
["CATEGORY", "PRODUCTS :type/Text :type/Category"],
|
||||
["CREATED_AT", "ACCOUNTS :type/DateTime :type/CreationTimestamp"],
|
||||
["CREATED_AT", "ORDERS :type/DateTime :type/CreationTimestamp"],
|
||||
["CREATED_AT", "PEOPLE :type/DateTime :type/CreationTimestamp"],
|
||||
["CREATED_AT", "PRODUCTS :type/DateTime :type/CreationTimestamp"],
|
||||
["CREATED_AT", "REVIEWS :type/DateTime :type/CreationTimestamp"],
|
||||
["DATE_RECEIVED", "FEEDBACK :type/DateTime"],
|
||||
["DATE_RECEIVED", "INVOICES :type/DateTime"],
|
||||
["EAN", "PRODUCTS :type/Text"],
|
||||
["EMAIL", "ACCOUNTS :type/Text :type/Email"],
|
||||
["EMAIL", "FEEDBACK :type/Text :type/Email"],
|
||||
["EMAIL", "PEOPLE :type/Text :type/Email"],
|
||||
["EVENT", "ANALYTIC_EVENTS :type/Text :type/Category"],
|
||||
["EXPECTED_INVOICE", "INVOICES :type/Boolean :type/Category"],
|
||||
["FIRST_NAME", "ACCOUNTS :type/Text :type/Name"],
|
||||
["LAST_NAME", "ACCOUNTS :type/Text :type/Name"],
|
||||
["LATITUDE", "ACCOUNTS :type/Float :type/Latitude"],
|
||||
["LATITUDE", "PEOPLE :type/Float :type/Latitude"],
|
||||
["LEGACY_PLAN", "ACCOUNTS :type/Boolean :type/Category"],
|
||||
["LONGITUDE", "ACCOUNTS :type/Float :type/Longitude"],
|
||||
["LONGITUDE", "PEOPLE :type/Float :type/Longitude"],
|
||||
["NAME", "PEOPLE :type/Text :type/Name"],
|
||||
["PAGE_URL", "ANALYTIC_EVENTS :type/Text :type/URL"],
|
||||
["PAYMENT", "INVOICES :type/Float"],
|
||||
["PRICE", "PRODUCTS :type/Float"],
|
||||
["RATING_MAPPED", "FEEDBACK :type/Text :type/Category"],
|
||||
["REVIEWER", "REVIEWS :type/Text"],
|
||||
["SEATS", "ACCOUNTS :type/Integer"],
|
||||
["SOURCE", "ACCOUNTS :type/Text :type/Source"],
|
||||
["SOURCE", "PEOPLE :type/Text :type/Source"],
|
||||
["STATE", "PEOPLE :type/Text :type/State"],
|
||||
["TIMESTAMP", "ANALYTIC_EVENTS :type/DateTime"],
|
||||
["TITLE", "PRODUCTS :type/Text :type/Title"],
|
||||
["TRIAL_CONVERTED", "ACCOUNTS :type/Boolean :type/Category"],
|
||||
["TRIAL_ENDS_AT", "ACCOUNTS :type/DateTime"],
|
||||
["USER_ID", "ORDERS :type/Integer :type/FK"],
|
||||
["VENDOR", "PRODUCTS :type/Text :type/Company"],
|
||||
["VENDOR_ID", "PRODUCTS :type/Integer :type/FK"],
|
||||
["USER_NAME", "PRODUCTS :type/Text :type/Name"],
|
||||
["TEST_COLUMN_1", "PRODUCTS :type/Text :type/Name"],
|
||||
["TEST_COLUMN_2", "PRODUCTS :type/Text :type/Name"],
|
||||
["TEST_COLUMN_3", "PRODUCTS :type/Text :type/Name"],
|
||||
],
|
||||
}).as("autocomplete");
|
||||
|
||||
H.startNewNativeQuestion();
|
||||
H.NativeEditor.type("e");
|
||||
|
||||
// autocomplete_suggestions?prefix=s
|
||||
cy.wait("@autocomplete");
|
||||
|
||||
H.NativeEditor.type("o");
|
||||
|
||||
// autocomplete_suggestions?prefix=so
|
||||
cy.wait("@autocomplete");
|
||||
});
|
||||
|
||||
it("should not continue to request more prefix matches from the server when the limit was not hit (metabase#20625)", () => {
|
||||
cy.intercept("GET", "/api/database/*/autocomplete_suggestions**", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
// This result has less than 50 items, which is under the limit
|
||||
// as set by the backend.
|
||||
// It will not be necessary to trigger the second autocomplete.
|
||||
["ORDERS", "Table"],
|
||||
["PEOPLE", "Table"],
|
||||
["REVIEWS", "Table"],
|
||||
["ACTIVE_SUBSCRIPTION", "ACCOUNTS :type/Boolean :type/Category"],
|
||||
["ADDRESS", "PEOPLE :type/Text"],
|
||||
],
|
||||
}).as("autocomplete");
|
||||
|
||||
H.startNewNativeQuestion();
|
||||
H.NativeEditor.type("e");
|
||||
|
||||
// autocomplete_suggestions?prefix=s
|
||||
cy.wait("@autocomplete");
|
||||
|
||||
H.NativeEditor.type("o");
|
||||
|
||||
cy.get("@autocomplete.all").should("have.length", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 21034", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
const { H } = cy;
|
||||
import {
|
||||
SAMPLE_DB_ID,
|
||||
USER_GROUPS,
|
||||
WRITABLE_DB_ID,
|
||||
} from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { THIRD_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
@@ -527,117 +523,6 @@ describe("scenarios > question > native", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// causes error in cypress 13
|
||||
describe("no native access", { tags: ["@external", "@skip"] }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore("postgres-12");
|
||||
cy.signInAsAdmin();
|
||||
cy.intercept("/api/database?saved=true").as("database");
|
||||
cy.updatePermissionsGraph({
|
||||
[USER_GROUPS.ALL_USERS_GROUP]: {
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "blocked",
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
[USER_GROUPS.NOSQL_GROUP]: {
|
||||
[SAMPLE_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder-and-native",
|
||||
},
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cy.updateCollectionGraph({
|
||||
[USER_GROUPS.NOSQL_GROUP]: { root: "write" },
|
||||
});
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: "Secret Orders",
|
||||
native: {
|
||||
query: "SELECT * FROM ORDERS",
|
||||
},
|
||||
database: WRITABLE_DB_ID,
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
},
|
||||
);
|
||||
|
||||
cy.signIn("nosql");
|
||||
});
|
||||
|
||||
it("should not display the query when you do not have native access to the data source", () => {
|
||||
cy.get("@questionId").then((questionId) =>
|
||||
cy.visit(`/question/${questionId}`),
|
||||
);
|
||||
|
||||
cy.findByTestId("native-query-top-bar").within(() => {
|
||||
cy.findByText("This question is written in SQL.").should("be.visible");
|
||||
cy.findByTestId("visibility-toggler").should("not.exist");
|
||||
});
|
||||
|
||||
cy.log("#32387");
|
||||
cy.findByRole("button", { name: /New/ }).click();
|
||||
H.popover().findByText("SQL query").click();
|
||||
|
||||
cy.wait("@database");
|
||||
cy.go("back");
|
||||
|
||||
cy.findByTestId("native-query-top-bar").within(() => {
|
||||
cy.findByText("This question is written in SQL.").should("be.visible");
|
||||
cy.findByTestId("visibility-toggler").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"shows format query button only for sql queries",
|
||||
{ tags: "@mongo" },
|
||||
() => {
|
||||
const MONGO_DB_NAME = "QA Mongo";
|
||||
|
||||
cy.intercept("POST", "/api/card").as("createQuestion");
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
H.restore("mongo-5");
|
||||
cy.signInAsNormalUser();
|
||||
|
||||
H.startNewNativeQuestion();
|
||||
cy.findByTestId("gui-builder-data").click();
|
||||
cy.findByLabelText(MONGO_DB_NAME).click();
|
||||
cy.findByLabelText("Auto-format").should("not.exist");
|
||||
|
||||
cy.findByTestId("native-query-top-bar").findByText(MONGO_DB_NAME).click();
|
||||
|
||||
// Switch to SQL engine which is supported by the formatter
|
||||
H.popover().findByText("Sample Database").click();
|
||||
|
||||
H.NativeEditor.focus().type("select * from orders", {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
|
||||
// It should load the formatter chunk only when used
|
||||
cy.intercept("GET", "**/sql-formatter**").as("sqlFormatter");
|
||||
|
||||
cy.findByLabelText("Auto-format").click();
|
||||
|
||||
cy.wait("@sqlFormatter");
|
||||
|
||||
H.NativeEditor.get().should("be.visible").get(".ace_line").as("lines");
|
||||
|
||||
cy.get("@lines").eq(0).should("have.text", "SELECT");
|
||||
cy.get("@lines").eq(1).should("have.text", " *");
|
||||
cy.get("@lines").eq(2).should("have.text", "FROM");
|
||||
cy.get("@lines").eq(3).should("have.text", " orders");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("scenarios > native question > data reference sidebar", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > about Metabase", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
cy.visit("/");
|
||||
cy.icon("gear").click();
|
||||
H.popover().findByText("About Metabase").click();
|
||||
});
|
||||
|
||||
it(
|
||||
"should display correct Metabase version (metabase#15656)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.modal().within(() => {
|
||||
cy.findByText(/You're on version v[01](\.\d+){2,3}(-[\w\d]+)?/i);
|
||||
cy.findByText(/Built on \d{4}-\d{2}-\d{2}/);
|
||||
cy.findByText("Branch: ?").should("not.exist");
|
||||
cy.findByText("Hash: ?").should("not.exist");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -73,18 +73,6 @@ describe("scenarios > reference > databases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should sort data reference database list (metabase#15598)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
cy.visit("/browse");
|
||||
checkReferenceDatabasesOrder();
|
||||
|
||||
cy.visit("/reference/databases/");
|
||||
checkReferenceDatabasesOrder();
|
||||
},
|
||||
);
|
||||
|
||||
it("should sort databases in new UI based question data selection popover", () => {
|
||||
H.startNewQuestion();
|
||||
H.miniPickerBrowseAll().click();
|
||||
@@ -99,14 +87,6 @@ describe("scenarios > reference > databases", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should sort databases in new native question data selection popover",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
checkQuestionSourceDatabasesOrder("Native query");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("x-ray", () => {
|
||||
@@ -155,25 +135,3 @@ describe("scenarios > reference > databases", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkReferenceDatabasesOrder() {
|
||||
cy.get("[class*=Card]").as("databaseCard").first().should("have.text", "a");
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
cy.get("@databaseCard").last().should("have.text", "Sample Database");
|
||||
}
|
||||
|
||||
function checkQuestionSourceDatabasesOrder() {
|
||||
// Last item is "Saved Questions" for UI based questions so we have to check for the one before that (-2), and the last one for "Native" (-1)
|
||||
const lastDatabaseIndex = -1;
|
||||
const selector = "[data-element-id=list-item]-title";
|
||||
|
||||
H.startNewQuestion();
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Raw Data").click();
|
||||
cy.get(selector).as("databaseName").eq(1).should("have.text", "a");
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
cy.get("@databaseName")
|
||||
.eq(lastDatabaseIndex)
|
||||
.should("have.text", "Sample Database");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { H } = cy;
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import type { Dashboard } from "metabase-types/api";
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
@@ -429,6 +430,43 @@ describe("scenarios > organization > entity picker > shared-tenant-collection na
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow admins to add questions from shared tenant sub-collections", () => {
|
||||
setupTenantCollections().as("tenantCollections");
|
||||
|
||||
cy.get<{ subCollectionId: number }>("@tenantCollections").then(
|
||||
({ subCollectionId }) =>
|
||||
H.createQuestion({
|
||||
name: "Tenant Sub-Collection Orders Question",
|
||||
collection_id: subCollectionId,
|
||||
query: { "source-table": ORDERS_ID },
|
||||
}),
|
||||
);
|
||||
|
||||
H.createDashboard({ name: "Test Dashboard" }).its("body").as("dashboard");
|
||||
|
||||
cy.get<Dashboard>("@dashboard").then(({ id }) => {
|
||||
H.visitDashboard(id);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
H.sidebar()
|
||||
.findByTestId("breadcrumbs")
|
||||
.should("contain", "Collections")
|
||||
.and("contain", "Our analytics")
|
||||
.findByText("Collections")
|
||||
.click();
|
||||
|
||||
H.sidebar().findByText(TENANT_ROOT_NAME).should("be.visible").click();
|
||||
H.sidebar().findByText("Test Tenant Collection").click();
|
||||
H.sidebar().findByText("Tenant Sub-Collection").click();
|
||||
H.sidebar().findByText("Tenant Sub-Collection Orders Question").click();
|
||||
|
||||
H.getDashboardCards()
|
||||
.should("have.length", 1)
|
||||
.and("contain", "Tenant Sub-Collection Orders Question");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show 'Collections' in breadcrumb when navigating into Our Analytics sub-collections", () => {
|
||||
setupTenantCollections().then(() => {
|
||||
H.createCollection({ name: "Our Analytics Sub" }).then(() => {
|
||||
|
||||
@@ -365,12 +365,16 @@ describe("scenarios > organization > timelines > question", () => {
|
||||
H.timelineEventChip("RC2").should("not.exist");
|
||||
|
||||
// its timeline, visible but having one hidden event
|
||||
// should display its checkbox with a "dash" icon
|
||||
// should display its checkbox in an indeterminate state
|
||||
cy.findByTestId("sidebar-content")
|
||||
.findByText("Releases")
|
||||
.closest("[aria-label='Timeline card header']")
|
||||
.within(() => {
|
||||
cy.icon("dash").should("be.visible");
|
||||
cy.findByRole("checkbox").should(
|
||||
"have.prop",
|
||||
"indeterminate",
|
||||
true,
|
||||
);
|
||||
|
||||
// Hide the timeline then show it again
|
||||
cy.findByRole("checkbox").click();
|
||||
|
||||
@@ -117,6 +117,7 @@ describe("impersonated permission", { tags: "@external" }, () => {
|
||||
.findByText(/Caching settings/)
|
||||
.should("be.visible");
|
||||
H.selectCacheStrategy(/Duration/);
|
||||
H.fillCacheDuration(24);
|
||||
H.cacheStrategySidesheet()
|
||||
.findByRole("button", { name: /Save/ })
|
||||
.click();
|
||||
|
||||
@@ -8,75 +8,11 @@ import {
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
|
||||
const { ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, REVIEWS_ID, PRODUCTS } =
|
||||
SAMPLE_DATABASE;
|
||||
const { PRODUCTS_ID, PEOPLE_ID, PRODUCTS } = SAMPLE_DATABASE;
|
||||
const { nocollection } = USERS;
|
||||
|
||||
const PG_DB_ID = 2;
|
||||
|
||||
// NOTE: This issue wasn't specifically related to PostgreSQL. We simply needed to add another DB to reproduce it.
|
||||
describe("issue 13347", { tags: ["@external", "@skip"] }, () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
H.restore("postgres-12");
|
||||
cy.signInAsAdmin();
|
||||
|
||||
cy.updatePermissionsGraph({
|
||||
[ALL_USERS_GROUP]: {
|
||||
1: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "query-builder-and-native",
|
||||
},
|
||||
[PG_DB_ID]: {
|
||||
"view-data": "unrestricted",
|
||||
"create-queries": "no",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cy.updateCollectionGraph({
|
||||
[ALL_USERS_GROUP]: { root: "read" },
|
||||
});
|
||||
|
||||
H.withDatabase(
|
||||
PG_DB_ID,
|
||||
({ ORDERS_ID }) =>
|
||||
H.createQuestion({
|
||||
name: "Q1",
|
||||
query: { "source-table": ORDERS_ID },
|
||||
database: PG_DB_ID,
|
||||
}),
|
||||
|
||||
H.createNativeQuestion({
|
||||
name: "Q2",
|
||||
native: { query: "SELECT * FROM ORDERS" },
|
||||
database: PG_DB_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
["QB", "Native"].forEach((test) => {
|
||||
it(`${test.toUpperCase()} version:\n should be able to select question (from "Saved Questions") which belongs to the database user doesn't have data-permissions for (metabase#13347)`, () => {
|
||||
cy.signIn("none");
|
||||
|
||||
H.startNewQuestion();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Saved Questions").click();
|
||||
|
||||
if (test === "QB") {
|
||||
cy.findByText("Q1").click();
|
||||
} else {
|
||||
cy.findByText("Q2").click();
|
||||
}
|
||||
|
||||
cy.wait("@dataset", { timeout: 5000 });
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.contains("37.65");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("postgres > user > query", { tags: "@external" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore("postgres-12");
|
||||
@@ -152,46 +88,6 @@ describe("postgres > user > query", { tags: "@external" }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 17777", { tags: "@skip" }, () => {
|
||||
function hideTables(tables) {
|
||||
cy.request("PUT", "/api/table", {
|
||||
ids: tables,
|
||||
visibility_type: "hidden",
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
hideTables([ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, REVIEWS_ID]);
|
||||
});
|
||||
|
||||
it("should still be able to set permissions on individual tables, even though they are hidden in data model (metabase#17777)", () => {
|
||||
cy.visit(`/admin/permissions/data/group/${ALL_USERS_GROUP}`);
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Permissions for the All Users group");
|
||||
cy.findByTextEnsureVisible("Sample Database").click();
|
||||
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/permissions/data/group/${ALL_USERS_GROUP}/database/${SAMPLE_DB_ID}`,
|
||||
);
|
||||
|
||||
cy.findByTestId("permission-table").within(() => {
|
||||
cy.findByText("Orders");
|
||||
cy.findByText("Products");
|
||||
cy.findByText("Reviews");
|
||||
cy.findByText("People");
|
||||
});
|
||||
|
||||
cy.findAllByText("No self-service").first().click();
|
||||
|
||||
H.popover().contains("Unrestricted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 19603", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -709,125 +709,3 @@ describe("issue 19742", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const QUESTION_1 = {
|
||||
name: "Q1",
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
aggregation: [["count"]],
|
||||
breakout: [["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }]],
|
||||
},
|
||||
};
|
||||
|
||||
const QUESTION_2 = {
|
||||
name: "Q2",
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
aggregation: [
|
||||
["sum", ["field", PRODUCTS.PRICE, { "base-type": "type/Float" }]],
|
||||
],
|
||||
breakout: [["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }]],
|
||||
},
|
||||
};
|
||||
|
||||
describe("issue 19893", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it(
|
||||
"should display correct join source table when joining visited questions (metabase#19893)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.createQuestion(QUESTION_1, {
|
||||
wrapId: true,
|
||||
idAlias: "questionId1",
|
||||
visitQuestion: true,
|
||||
});
|
||||
H.createQuestion(QUESTION_2, {
|
||||
wrapId: true,
|
||||
idAlias: "questionId2",
|
||||
visitQuestion: true,
|
||||
});
|
||||
|
||||
cy.then(function () {
|
||||
const { questionId1, questionId2 } = this;
|
||||
|
||||
createQ1PlusQ2Question(questionId1, questionId2).then(
|
||||
({ body: question }) => {
|
||||
cy.visit(`/question/${question.id}/notebook`);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
assertQ1PlusQ2Joins();
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should display correct join source table when joining non-visited questions (metabase#19893)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.createQuestion(QUESTION_1, { wrapId: true, idAlias: "questionId1" });
|
||||
H.createQuestion(QUESTION_2, { wrapId: true, idAlias: "questionId2" });
|
||||
|
||||
cy.then(function () {
|
||||
const { questionId1, questionId2 } = this;
|
||||
|
||||
createQ1PlusQ2Question(questionId1, questionId2).then(
|
||||
({ body: question }) => {
|
||||
cy.visit(`/question/${question.id}/notebook`);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
assertQ1PlusQ2Joins();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const createQ1PlusQ2Question = (questionId1, questionId2) => {
|
||||
return H.createQuestion({
|
||||
name: "Q1 + Q2",
|
||||
query: {
|
||||
"source-table": `card__${questionId1}`,
|
||||
joins: [
|
||||
{
|
||||
fields: "all",
|
||||
strategy: "left-join",
|
||||
alias: "Q2 - Category",
|
||||
condition: [
|
||||
"=",
|
||||
["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
|
||||
[
|
||||
"field",
|
||||
PRODUCTS.CATEGORY,
|
||||
{ "base-type": "type/Text", "join-alias": "Q2 - Category" },
|
||||
],
|
||||
],
|
||||
"source-table": `card__${questionId2}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const assertQ1PlusQ2Joins = () => {
|
||||
H.getNotebookStep("join").within(() => {
|
||||
cy.findAllByTestId("notebook-cell-item").then((items) => {
|
||||
cy.wrap(items[0]).should("contain", QUESTION_1.name);
|
||||
cy.wrap(items[1]).should("contain", QUESTION_2.name);
|
||||
});
|
||||
|
||||
cy.findByLabelText("Left column").within(() => {
|
||||
cy.findByText(QUESTION_1.name).should("exist");
|
||||
cy.findByText("Category").should("exist");
|
||||
});
|
||||
|
||||
cy.findByLabelText("Right column").within(() => {
|
||||
cy.findByText(QUESTION_2.name).should("exist");
|
||||
cy.findByText("Q2 - Category → Category").should("exist");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -681,57 +681,6 @@ describe("issue 40064", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 10493", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should not reset chart axes after adding a new query stage (metabase#10493)", () => {
|
||||
H.visitQuestionAdhoc({
|
||||
display: "bar",
|
||||
dataset_query: {
|
||||
type: "query",
|
||||
database: SAMPLE_DB_ID,
|
||||
query: {
|
||||
aggregation: [["count"]],
|
||||
breakout: [
|
||||
[
|
||||
"field",
|
||||
ORDERS.QUANTITY,
|
||||
{ "base-type": "type/Integer", binning: { strategy: "default" } },
|
||||
],
|
||||
],
|
||||
"source-table": ORDERS_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
H.filter();
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Summaries").click();
|
||||
cy.findByTestId("filter-column-Count").within(() => {
|
||||
cy.findByPlaceholderText("Min").type("0");
|
||||
cy.findByPlaceholderText("Max").type("30000");
|
||||
});
|
||||
cy.button("Apply filters").click();
|
||||
});
|
||||
cy.wait("@dataset");
|
||||
|
||||
H.echartsContainer().within(() => {
|
||||
// y axis
|
||||
cy.findByText("Count").should("exist");
|
||||
cy.findByText("21,000").should("exist");
|
||||
cy.findByText("3,000").should("exist");
|
||||
|
||||
// x axis
|
||||
cy.findByText("Quantity").should("exist");
|
||||
cy.findByText("25").should("exist");
|
||||
cy.findByText("75").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("issue 44071", () => {
|
||||
const questionDetails = {
|
||||
name: "Test",
|
||||
|
||||
@@ -568,41 +568,6 @@ describe("issue 41464", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 45359", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.intercept("/app/fonts/Lato/lato-v16-latin-regular.woff2").as(
|
||||
"font-regular",
|
||||
);
|
||||
cy.intercept("/app/fonts/Lato/lato-v16-latin-700.woff2").as("font-bold");
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("loads app fonts correctly (metabase#45359)", () => {
|
||||
H.openOrdersTable({ mode: "notebook" });
|
||||
|
||||
H.getNotebookStep("data")
|
||||
.findByText("Orders")
|
||||
.should("have.css", "font-family", "Lato, Arial, sans-serif");
|
||||
|
||||
cy.get("@font-regular.all").should("have.length", 1);
|
||||
cy.get("@font-regular").should(({ response }) => {
|
||||
expect(response).to.include({ statusCode: 200 });
|
||||
});
|
||||
|
||||
cy.get("@font-bold.all").should("have.length", 1);
|
||||
cy.get("@font-bold").should(({ response }) => {
|
||||
expect(response).to.include({ statusCode: 200 });
|
||||
});
|
||||
|
||||
cy.document()
|
||||
.then((document) => document.fonts.ready)
|
||||
.then((fonts) => {
|
||||
cy.wrap(fonts).invoke("check", "16px Lato").should("be.true");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 45452", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
|
||||
@@ -246,7 +246,6 @@ describe(
|
||||
{ tags: "@mongo" },
|
||||
() => {
|
||||
const MONGO_DB_NAME = "QA Mongo";
|
||||
const MONGO_DB_ID = 2;
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore("mongo-5");
|
||||
@@ -315,64 +314,6 @@ describe(
|
||||
cy.findByTestId("selected-table").should("have.text", "Products");
|
||||
cy.get("[data-testid=cell-data]").should("contain", "Small Marble Shoes");
|
||||
});
|
||||
|
||||
it(
|
||||
"should work for a nested GUI question (metabase#40557)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.withDatabase(
|
||||
MONGO_DB_ID,
|
||||
({ PRODUCTS_ID }: { PRODUCTS_ID: number }) => {
|
||||
H.createQuestion({
|
||||
name: "Mongo Source",
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
limit: 1,
|
||||
},
|
||||
database: MONGO_DB_ID,
|
||||
}).then(({ body: { id: sourceId } }) => {
|
||||
H.createQuestion(
|
||||
{
|
||||
name: "Mongo Nested",
|
||||
query: {
|
||||
"source-table": `card__${sourceId}`,
|
||||
},
|
||||
database: MONGO_DB_ID,
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cy.get("[data-testid=cell-data]").should(
|
||||
"contain",
|
||||
"Small Marble Shoes",
|
||||
);
|
||||
H.openNotebook();
|
||||
openSidebar("native");
|
||||
|
||||
cy.findByTestId("native-query-preview-sidebar").within(() => {
|
||||
cy.findByText("Native query for this question").should("exist");
|
||||
H.NativeEditor.get()
|
||||
.should("be.visible")
|
||||
.and("contain", "$project")
|
||||
.and("contain", "$limit")
|
||||
.and("not.contain", "BsonString")
|
||||
.and("not.contain", "BsonInt32");
|
||||
|
||||
cy.button("Convert this question to a native query").click();
|
||||
});
|
||||
|
||||
cy.log("Database and table should be pre-selected (metabase#40557)");
|
||||
cy.findByTestId("selected-database").should("have.text", MONGO_DB_NAME);
|
||||
cy.findByTestId("selected-table").should("have.text", "Products");
|
||||
cy.get("[data-testid=cell-data]").should(
|
||||
"contain",
|
||||
"Small Marble Shoes",
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -700,80 +700,6 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should be possible to sort by metric (metabase#8283,metabase#42392)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.createQuestion(
|
||||
{
|
||||
name: "Revenue",
|
||||
description: "Sum of orders subtotal",
|
||||
type: "metric",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["sum", ["field", ORDERS.SUBTOTAL, null]]],
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
idAlias: "metricId",
|
||||
},
|
||||
);
|
||||
|
||||
cy.get("@metricId").then((metricId) => {
|
||||
const questionDetails = {
|
||||
query: {
|
||||
"source-table": `card__${metricId}`,
|
||||
aggregation: ["metric", metricId],
|
||||
},
|
||||
};
|
||||
|
||||
H.createQuestion(questionDetails, { visitQuestion: true });
|
||||
|
||||
H.openNotebook();
|
||||
|
||||
cy.findByText("Pick a column to group by").click();
|
||||
cy.findByText("Created At").click();
|
||||
cy.findByText("Sort").click();
|
||||
|
||||
// Sorts ascending by default
|
||||
// Revenue appears twice, but it's the only integer column to order by
|
||||
H.popover().icon("int").click();
|
||||
|
||||
// Let's make sure it's possible to sort descending as well
|
||||
cy.icon("arrow_up").click();
|
||||
|
||||
cy.icon("arrow_down").parent().contains("Revenue");
|
||||
|
||||
H.visualize();
|
||||
// Visualization will render line chart by default. Switch to the table.
|
||||
cy.icon("table2").click();
|
||||
|
||||
cy.findAllByRole("grid").as("table");
|
||||
cy.get("@table")
|
||||
.first()
|
||||
.as("tableHeader")
|
||||
.within(() => {
|
||||
cy.get("[data-testid=cell-data]")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should("eq", "Revenue");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
cy.get("@table")
|
||||
.last()
|
||||
.as("tableBody")
|
||||
.within(() => {
|
||||
cy.get("[data-testid=cell-data]")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should("eq", "50,072.98");
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should open only one bucketing popover at a time (metabase#45036)", () => {
|
||||
H.visitQuestionAdhoc(
|
||||
{
|
||||
|
||||
@@ -48,8 +48,6 @@ describe("scenarios > question > offset", () => {
|
||||
});
|
||||
|
||||
describe("custom columns", () => {
|
||||
// This test contradicts the next 2 skipped ones.
|
||||
// Remove it once we enable offset() in custom columns.
|
||||
it("does not suggest or allow using offset()", () => {
|
||||
const expression = "Offset([Total], -1)";
|
||||
const prefixLength = 3;
|
||||
@@ -79,127 +77,6 @@ describe("scenarios > question > offset", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Skipped because we want to disable offset() in custom columns for now
|
||||
it("suggests and allows using offset()", { tags: "@skip" }, () => {
|
||||
const expression = "Offset([Total], -1)";
|
||||
const prefixLength = 3;
|
||||
const prefix = expression.substring(0, prefixLength);
|
||||
const query: StructuredQuery = {
|
||||
"source-table": ORDERS_ID,
|
||||
fields: [ORDERS_ID_FIELD_REF, ORDERS_TOTAL_FIELD_REF],
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
H.createQuestion({ query }, { visitQuestion: true });
|
||||
H.openNotebook();
|
||||
cy.button("Custom column").click();
|
||||
H.enterCustomColumnDetails({ formula: prefix });
|
||||
|
||||
cy.log("suggests offset() in custom column expressions");
|
||||
cy.findByTestId("expression-suggestions-list-item")
|
||||
.should("exist")
|
||||
.and("have.text", "Offset");
|
||||
|
||||
H.enterCustomColumnDetails({ formula: expression });
|
||||
cy.realPress("Tab");
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText("OFFSET in a custom expression requires a sort order");
|
||||
cy.button("Done").should("be.disabled");
|
||||
cy.button("Cancel").click();
|
||||
});
|
||||
|
||||
cy.button("Sort").click();
|
||||
H.popover().findByText("ID").click();
|
||||
H.getNotebookStep("expression").icon("add").click();
|
||||
H.enterCustomColumnDetails({ formula: expression });
|
||||
cy.realPress("Tab");
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.button("Done").should("be.disabled");
|
||||
|
||||
cy.findByPlaceholderText("Something nice and descriptive")
|
||||
.type("My expression")
|
||||
.blur();
|
||||
|
||||
cy.button("Done").should("be.enabled").click();
|
||||
});
|
||||
|
||||
cy.log("preview availability");
|
||||
H.getNotebookStep("data").icon("play").should("be.visible");
|
||||
H.getNotebookStep("expression").icon("play").should("not.be.visible");
|
||||
H.getNotebookStep("sort").icon("play").should("be.visible");
|
||||
H.getNotebookStep("limit").icon("play").should("be.visible");
|
||||
|
||||
H.visualize();
|
||||
verifyTableContent([
|
||||
["1", "39.72", ""],
|
||||
["2", "117.03", "39.72"],
|
||||
["3", "49.21", "117.03"],
|
||||
]);
|
||||
});
|
||||
|
||||
// Skipped because we want to disable offset() in custom columns for now
|
||||
it(
|
||||
"does not allow to use offset-based column in other clauses (metabase#42764)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
const offsettedColumnName = "xyz";
|
||||
const expression = `Offset([${offsettedColumnName}], -1)`;
|
||||
const prefixLength = "Offset([x".length;
|
||||
const prefix = expression.substring(0, prefixLength);
|
||||
const query: StructuredQuery = {
|
||||
"source-table": ORDERS_ID,
|
||||
expressions: {
|
||||
[offsettedColumnName]: [
|
||||
"offset",
|
||||
createOffsetOptions(offsettedColumnName),
|
||||
ORDERS_TOTAL_FIELD_REF,
|
||||
-1,
|
||||
],
|
||||
},
|
||||
"order-by": [["asc", ORDERS_ID_FIELD_REF]],
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
H.createQuestion({ query }, { visitQuestion: true });
|
||||
|
||||
cy.log("custom column drills");
|
||||
const rowIndex = 1;
|
||||
const columnIndex = 9;
|
||||
const columnsCount = 10;
|
||||
const cellIndex = rowIndex * columnsCount + columnIndex;
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
cy.findAllByRole("gridcell").eq(cellIndex).click();
|
||||
cy.get(H.POPOVER_ELEMENT).should("not.exist");
|
||||
|
||||
H.openNotebook();
|
||||
|
||||
cy.log("custom column expressions");
|
||||
H.getNotebookStep("expression").icon("add").click();
|
||||
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
|
||||
H.popover().button("Cancel").click();
|
||||
|
||||
cy.log("custom filter expressions");
|
||||
cy.icon("filter").click();
|
||||
H.popover().findByText("Custom Expression").click();
|
||||
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
|
||||
H.popover().button("Cancel").click();
|
||||
cy.realPress("Escape");
|
||||
|
||||
cy.log("custom aggregation expressions");
|
||||
cy.icon("sum").click();
|
||||
H.popover().findByText("Custom Expression").click();
|
||||
verifyInvalidColumnName(offsettedColumnName, prefix, expression);
|
||||
H.popover().button("Cancel").click();
|
||||
cy.realPress("Escape");
|
||||
|
||||
cy.log("sort clause");
|
||||
H.getNotebookStep("sort").icon("add").click();
|
||||
H.popover().should("not.contain", offsettedColumnName);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("filters", () => {
|
||||
@@ -530,22 +407,6 @@ function verifyNoQuestionError() {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyInvalidColumnName(
|
||||
columnName: string,
|
||||
prefix: string,
|
||||
expression: string,
|
||||
) {
|
||||
H.enterCustomColumnDetails({ formula: prefix });
|
||||
cy.findByTestId("expression-suggestions-list-item").should("not.exist");
|
||||
|
||||
H.enterCustomColumnDetails({ formula: expression });
|
||||
cy.realPress("Tab");
|
||||
H.popover().within(() => {
|
||||
cy.findByText(`Unknown Field: ${columnName}`).should("be.visible");
|
||||
cy.button("Done").should("be.disabled");
|
||||
});
|
||||
}
|
||||
|
||||
function createOffsetOptions(name = "offset") {
|
||||
return {
|
||||
"lib/uuid": uuid(),
|
||||
|
||||
@@ -652,35 +652,5 @@ describe(
|
||||
|
||||
H.modal().findByText(secondWebhookName).should("be.visible");
|
||||
});
|
||||
|
||||
// There is no api to test individual hooks for new Question Alerts
|
||||
it("should allow you to test a webhook", { tags: "@skip" }, () => {
|
||||
cy.intercept("POST", "/api/pulse/test").as("testAlert");
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
cy.findByLabelText("Move, trash, and more…").click();
|
||||
H.popover().findByText("Create an alert").click();
|
||||
|
||||
H.modal().within(() => {
|
||||
H.getAlertChannel(firstWebhookName).scrollIntoView();
|
||||
|
||||
H.getAlertChannel(firstWebhookName)
|
||||
.findByRole("checkbox")
|
||||
.click({ force: true });
|
||||
|
||||
H.getAlertChannel(firstWebhookName).button("Send a test").click();
|
||||
});
|
||||
|
||||
cy.wait("@testAlert");
|
||||
|
||||
cy.request(
|
||||
`${H.WEBHOOK_TEST_HOST}/api/session/${H.WEBHOOK_TEST_SESSION_ID}/requests`,
|
||||
).then(({ body }) => {
|
||||
expect(body).to.have.length(1);
|
||||
|
||||
cy.wrap(atob(body[0].content_base64))
|
||||
.should("have.string", "alert_creator_name")
|
||||
.and("have.string", "Bobby Tables");
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -429,128 +429,6 @@ describe("scenarios > question > settings", () => {
|
||||
cy.findByLabelText("close icon").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should allow hiding and showing aggregated columns with a post-aggregation custom column (metabase#22563)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
// products joined to orders with breakouts on 3 product columns followed by a custom column
|
||||
H.createQuestion(
|
||||
{
|
||||
name: "repro 22563",
|
||||
query: {
|
||||
"source-query": {
|
||||
"source-table": ORDERS_ID,
|
||||
joins: [
|
||||
{
|
||||
alias: "Products",
|
||||
condition: [
|
||||
"=",
|
||||
["field", ORDERS.PRODUCT_ID, null],
|
||||
[
|
||||
"field",
|
||||
PRODUCTS.ID,
|
||||
{
|
||||
"join-alias": "Products",
|
||||
},
|
||||
],
|
||||
],
|
||||
"source-table": PRODUCTS_ID,
|
||||
},
|
||||
],
|
||||
aggregation: [["count"]],
|
||||
breakout: [
|
||||
[
|
||||
"field",
|
||||
PRODUCTS.CATEGORY,
|
||||
{
|
||||
"base-type": "type/Text",
|
||||
"join-alias": "Products",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
PRODUCTS.TITLE,
|
||||
{
|
||||
"base-type": "type/Text",
|
||||
"join-alias": "Products",
|
||||
},
|
||||
],
|
||||
[
|
||||
"field",
|
||||
PRODUCTS.VENDOR,
|
||||
{
|
||||
"base-type": "type/Text",
|
||||
"join-alias": "Products",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
expressions: {
|
||||
two: ["+", 1, 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
const columnNames = [
|
||||
"Products → Category",
|
||||
"Products → Title",
|
||||
"Products → Vendor",
|
||||
"Count",
|
||||
"two",
|
||||
];
|
||||
|
||||
H.tableInteractive().within(() => {
|
||||
columnNames.forEach((text) =>
|
||||
cy.findByText(text).should("be.visible"),
|
||||
);
|
||||
});
|
||||
|
||||
H.openVizSettingsSidebar();
|
||||
|
||||
cy.findByTestId("chartsettings-sidebar").within(() => {
|
||||
columnNames.forEach((text) =>
|
||||
cy.findByText(text).should("be.visible"),
|
||||
);
|
||||
cy.findByText("More Columns").should("not.exist");
|
||||
|
||||
cy.icon("eye_outline").first().click();
|
||||
|
||||
cy.findByText("More columns").should("be.visible");
|
||||
|
||||
// disable the first column
|
||||
cy.findByTestId("disabled-columns")
|
||||
.findByText("Products → Category")
|
||||
.should("be.visible");
|
||||
cy.findByTestId("visible-columns")
|
||||
.findByText("Products → Category")
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
H.tableInteractive().within(() => {
|
||||
// the query should not have changed
|
||||
cy.icon("play").should("not.exist");
|
||||
cy.findByText("Products → Category").should("not.exist");
|
||||
});
|
||||
|
||||
cy.findByTestId("chartsettings-sidebar").within(() => {
|
||||
cy.icon("add").click();
|
||||
// re-enable the first column
|
||||
cy.findByText("More columns").should("not.exist");
|
||||
cy.findByTestId("visible-columns")
|
||||
.findByText("Products → Category")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
H.tableInteractive().within(() => {
|
||||
// the query should not have changed
|
||||
cy.icon("play").should("not.exist");
|
||||
cy.findByText("Products → Category").should("be.visible");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("resetting state", () => {
|
||||
|
||||
@@ -253,25 +253,6 @@ describe("scenarios > question > summarize sidebar", () => {
|
||||
cy.findAllByTestId("header-cell").should("have.length", 2);
|
||||
cy.get("[data-testid=cell-data]").should("contain", 744); // `Count` for year 2025
|
||||
});
|
||||
|
||||
// flaky test (#19454)
|
||||
it(
|
||||
"should show an info popover when hovering over summarize dimension options",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
H.openReviewsTable();
|
||||
|
||||
H.summarize();
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Group by")
|
||||
.parent()
|
||||
.findByText("Title")
|
||||
.trigger("mouseenter");
|
||||
|
||||
H.popover().contains("Title");
|
||||
H.popover().contains("199 distinct values");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function removeMetricFromSidebar(metricName) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const { ORDERS_ID, PEOPLE_ID, REVIEWS_ID } = SAMPLE_DATABASE;
|
||||
const { ORDERS_ID, PEOPLE_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const visitEmbeddingWithSearch = (url = "/") => {
|
||||
H.visitFullAppEmbeddingUrl({
|
||||
@@ -249,27 +249,6 @@ describe("scenarios > search", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 16785", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
cy.request("PUT", "/api/table", {
|
||||
ids: [REVIEWS_ID],
|
||||
visibility_type: "hidden",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not display hidden tables (metabase#16785)", () => {
|
||||
cy.visit("/");
|
||||
cy.findByPlaceholderText("Search…").type("Reviews");
|
||||
|
||||
cy.findByTestId("search-results-list").within(() => {
|
||||
cy.findByText("Reviews").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 28788", () => {
|
||||
const LONG_STRING = "01234567890ABCDEFGHIJKLMNOPQRSTUVXYZ0123456789";
|
||||
|
||||
|
||||
@@ -167,20 +167,6 @@ describe("scenarios > dashboard > subscriptions", () => {
|
||||
|
||||
H.popover().isRenderedWithinViewport();
|
||||
});
|
||||
|
||||
it(
|
||||
"should not send attachments by default if not explicitly selected (metabase#28673)",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
openDashboardSubscriptions();
|
||||
assignRecipient();
|
||||
|
||||
cy.findByLabelText("Attach results").should("not.be.checked");
|
||||
H.sendEmailAndAssert(
|
||||
({ attachments }) => expect(attachments).to.be.empty,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("with existing subscriptions", () => {
|
||||
|
||||
@@ -824,217 +824,6 @@ describe("scenarios > visualizations > bar chart", () => {
|
||||
H.echartsTriggerBlur();
|
||||
});
|
||||
|
||||
it(
|
||||
"should allow grouping series into a single 'Other' series",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
const AK_SERIES_COLOR = "#509EE3";
|
||||
|
||||
const USER_STATE_FIELD_REF = [
|
||||
"field",
|
||||
PEOPLE.STATE,
|
||||
{ "source-field": ORDERS.USER_ID },
|
||||
];
|
||||
const ORDER_CREATED_AT_FIELD_REF = [
|
||||
"field",
|
||||
ORDERS.CREATED_AT,
|
||||
{ "temporal-unit": "month" },
|
||||
];
|
||||
|
||||
function setMaxCategories(value, { viaBreakoutSettings = false } = {}) {
|
||||
if (viaBreakoutSettings) {
|
||||
H.leftSidebar().findByTestId("settings-STATE").click();
|
||||
} else {
|
||||
H.leftSidebar().findByLabelText("Other series settings").click();
|
||||
}
|
||||
H.popover()
|
||||
.findByTestId("graph-max-categories-input")
|
||||
.type(`{selectAll}${value}`)
|
||||
.blur();
|
||||
cy.wait(500); // wait for viz to re-render
|
||||
}
|
||||
|
||||
function setOtherCategoryAggregationFn(fnName) {
|
||||
H.leftSidebar().findByLabelText("Other series settings").click();
|
||||
H.popover()
|
||||
.findByTestId("graph-other-category-aggregation-fn-picker")
|
||||
.click();
|
||||
// eslint-disable-next-line metabase/no-unsafe-element-filtering
|
||||
H.popover().last().findByText(fnName).click();
|
||||
}
|
||||
|
||||
H.visitQuestionAdhoc({
|
||||
display: "bar",
|
||||
dataset_query: {
|
||||
type: "query",
|
||||
database: SAMPLE_DB_ID,
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["count"]],
|
||||
breakout: [USER_STATE_FIELD_REF, ORDER_CREATED_AT_FIELD_REF],
|
||||
filter: [
|
||||
"and",
|
||||
[
|
||||
"between",
|
||||
ORDER_CREATED_AT_FIELD_REF,
|
||||
"2025-09-01T00:00Z",
|
||||
"2026-02-01T00:00Z",
|
||||
],
|
||||
[
|
||||
"=",
|
||||
USER_STATE_FIELD_REF,
|
||||
"AK",
|
||||
"AL",
|
||||
"AR",
|
||||
"AZ",
|
||||
"CA",
|
||||
"CO",
|
||||
"CT",
|
||||
"DE",
|
||||
"FL",
|
||||
"GA",
|
||||
"IA",
|
||||
"ID",
|
||||
"IL",
|
||||
"KY",
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Enable 'Other' series
|
||||
H.openVizSettingsSidebar();
|
||||
H.leftSidebar().findByTestId("settings-STATE").click();
|
||||
H.popover().findByLabelText("Enforce maximum number of series").click();
|
||||
|
||||
// Test 'Other' series renders
|
||||
H.otherSeriesChartPaths().should("have.length", 6);
|
||||
|
||||
// Test drill-through is disabled for 'Other' series
|
||||
H.otherSeriesChartPaths().first().click();
|
||||
cy.findByTestId("click-actions-view").should("not.exist");
|
||||
|
||||
// Test drill-through is enabled for regular series
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().click();
|
||||
cy.findByTestId("click-actions-view").should("exist");
|
||||
|
||||
// Test legend and series visibility toggling
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.should("have.length", 9)
|
||||
.last()
|
||||
.as("other-series-legend-item");
|
||||
cy.get("@other-series-legend-item")
|
||||
.findByLabelText("Hide series")
|
||||
.click();
|
||||
H.otherSeriesChartPaths().should("have.length", 0);
|
||||
cy.get("@other-series-legend-item")
|
||||
.findByLabelText("Show series")
|
||||
.click();
|
||||
H.otherSeriesChartPaths().should("have.length", 6);
|
||||
|
||||
// Test tooltips
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "9" }] });
|
||||
H.otherSeriesChartPaths().first().realHover();
|
||||
H.assertEChartsTooltip({
|
||||
header: "September 2025",
|
||||
rows: [
|
||||
{ name: "IA", value: "3" },
|
||||
{ name: "KY", value: "2" },
|
||||
{ name: "FL", value: "1" },
|
||||
{ name: "GA", value: "1" },
|
||||
{ name: "ID", value: "1" },
|
||||
{ name: "IL", value: "1" },
|
||||
{ name: "Total", value: "9" },
|
||||
],
|
||||
});
|
||||
|
||||
// Test "graph.max_categories" change
|
||||
setMaxCategories(4);
|
||||
H.queryBuilderMain().click(); // close popover
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.echartsTooltip().find("tr").should("have.length", 5);
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.should("have.length", 5);
|
||||
|
||||
// Test can move series in/out of "Other" series
|
||||
H.getDraggableElements().eq(3).as("AZ");
|
||||
H.moveDnDKitElementByAlias("@AZ", { vertical: 150 }); // Move AZ into "Other"
|
||||
H.getDraggableElements().eq(6).as("CT");
|
||||
H.moveDnDKitElementByAlias("@CT", { vertical: -150 }); // Move CT out of "Other"
|
||||
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.should("have.length", 5);
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.contains("AZ")
|
||||
.should("not.exist");
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.contains("CT")
|
||||
.should("exist");
|
||||
|
||||
// Test "graph.max_categories" removes "Other" altogether
|
||||
setMaxCategories(0);
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.echartsTooltip().find("tr").should("have.length", 14);
|
||||
H.queryBuilderMain()
|
||||
.findAllByTestId("legend-item")
|
||||
.should("have.length", 14);
|
||||
H.otherSeriesChartPaths().should("not.exist");
|
||||
setMaxCategories(8, { viaBreakoutSettings: true });
|
||||
|
||||
// Test "graph.other_category_aggregation_fn" for native queries
|
||||
H.openNotebook();
|
||||
H.queryBuilderHeader().findByLabelText("View SQL").click();
|
||||
cy.findByTestId("native-query-preview-sidebar")
|
||||
.button("Convert this question to SQL")
|
||||
.click();
|
||||
cy.wait("@dataset");
|
||||
H.queryBuilderMain().findByTestId("visibility-toggler").click();
|
||||
|
||||
H.openVizSettingsSidebar();
|
||||
setOtherCategoryAggregationFn("Average");
|
||||
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "1.5" }] });
|
||||
|
||||
H.otherSeriesChartPaths().first().realHover();
|
||||
H.assertEChartsTooltip({
|
||||
header: "September 2025",
|
||||
rows: [
|
||||
{ name: "IA", value: "3" },
|
||||
{ name: "KY", value: "2" },
|
||||
{ name: "FL", value: "1" },
|
||||
{ name: "GA", value: "1" },
|
||||
{ name: "ID", value: "1" },
|
||||
{ name: "IL", value: "1" },
|
||||
{ name: "Average", value: "1.5" },
|
||||
],
|
||||
});
|
||||
|
||||
setOtherCategoryAggregationFn("Min");
|
||||
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "1" }] });
|
||||
|
||||
H.otherSeriesChartPaths().first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Min", value: "1" }] });
|
||||
|
||||
setOtherCategoryAggregationFn("Max");
|
||||
|
||||
H.chartPathWithFillColor(AK_SERIES_COLOR).first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Other", value: "3" }] });
|
||||
|
||||
H.otherSeriesChartPaths().first().realHover();
|
||||
H.assertEChartsTooltip({ rows: [{ name: "Max", value: "3" }] });
|
||||
},
|
||||
);
|
||||
|
||||
it("should format goal tooltip value as a percent when the Stacking option is 'Stack - 100%'", () => {
|
||||
H.visitQuestionAdhoc({
|
||||
...breakoutBarChart,
|
||||
|
||||
@@ -611,103 +611,6 @@ describe("scenarios > visualizations > line/bar chart > tooltips", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Fix series have different colors in visualizer because of different
|
||||
// column names in original dashcard and visualizer ("avg" becomes "COLUMN_2" and the color has is different)
|
||||
it(
|
||||
"should show updated column titles in tooltips after editing them via Visualization Options",
|
||||
{ tags: "@skip" },
|
||||
() => {
|
||||
// Checking the second datum since the first circle of one series is covered with a circle from the other series
|
||||
const circleIndex = 1;
|
||||
|
||||
const originalSeriesColors = ["#A989C5", "#88BF4D"];
|
||||
const addedSeriesColors = ["#509EE3", "#98D9D9"];
|
||||
const originalAvgSeriesName = "Q1: Average of Total";
|
||||
const originalCumSumSeriesName = "Q1: Cumulative sum of Quantity";
|
||||
const updatedOriginalAvgSeriesName = "Q1 Custom 1";
|
||||
const updatedOriginalCumSumSeriesName = "Q1 Custom 2";
|
||||
const addedAvgSeriesName = "Q2: Average of Discount";
|
||||
const addedSumSeriesName = "Q2: Sum of Discount";
|
||||
const updatedAddedAvgSeriesName = "Q2 Custom 1";
|
||||
const updatedAddedSumSeriesName = "Q2 Custom 2";
|
||||
|
||||
[...originalSeriesColors, ...addedSeriesColors].forEach((color) => {
|
||||
showTooltipForCircleInSeries(color, circleIndex);
|
||||
H.assertEChartsTooltip({
|
||||
header: "2026",
|
||||
rows: [
|
||||
{
|
||||
color: originalSeriesColors[0],
|
||||
name: originalAvgSeriesName,
|
||||
value: "56.86",
|
||||
},
|
||||
{
|
||||
color: originalSeriesColors[1],
|
||||
name: originalCumSumSeriesName,
|
||||
value: "17,587",
|
||||
},
|
||||
{
|
||||
color: addedSeriesColors[0],
|
||||
name: addedAvgSeriesName,
|
||||
value: "5.41",
|
||||
},
|
||||
{
|
||||
color: addedSeriesColors[1],
|
||||
name: addedSumSeriesName,
|
||||
value: "1,953.08",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
H.editDashboard();
|
||||
H.showDashcardVisualizerModalSettings(0, {
|
||||
isVisualizerCard: false,
|
||||
});
|
||||
|
||||
updateColumnTitle(originalAvgSeriesName, updatedOriginalAvgSeriesName);
|
||||
updateColumnTitle(
|
||||
originalCumSumSeriesName,
|
||||
updatedOriginalCumSumSeriesName,
|
||||
);
|
||||
|
||||
updateColumnTitle(addedAvgSeriesName, updatedAddedAvgSeriesName);
|
||||
updateColumnTitle(addedSumSeriesName, updatedAddedSumSeriesName);
|
||||
|
||||
H.saveDashcardVisualizerModalSettings();
|
||||
H.saveDashboard();
|
||||
|
||||
[...originalSeriesColors, ...addedSeriesColors].forEach((color) => {
|
||||
showTooltipForCircleInSeries(color, circleIndex);
|
||||
H.assertEChartsTooltip({
|
||||
header: "2026",
|
||||
rows: [
|
||||
{
|
||||
color: originalSeriesColors[0],
|
||||
name: updatedOriginalAvgSeriesName,
|
||||
value: "56.86",
|
||||
},
|
||||
{
|
||||
color: originalSeriesColors[1],
|
||||
name: updatedOriginalCumSumSeriesName,
|
||||
value: "17,587",
|
||||
},
|
||||
{
|
||||
color: addedSeriesColors[0],
|
||||
name: updatedAddedAvgSeriesName,
|
||||
value: "5.41",
|
||||
},
|
||||
{
|
||||
color: addedSeriesColors[1],
|
||||
name: updatedAddedSumSeriesName,
|
||||
value: "1,953.08",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should show percent change in tooltip for timeseries axis", () => {
|
||||
testAvgTotalChange(showTooltipForCircleInSeries, "Q1: Average of Total");
|
||||
testCumSumChange(false, "Q1: Cumulative sum of Quantity");
|
||||
|
||||
@@ -635,53 +635,6 @@ describe("issue 21665", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 22527", { tags: "@skip" }, () => {
|
||||
const questionDetails = {
|
||||
native: {
|
||||
query:
|
||||
"select 1 x, 1 y, 20 size\nunion all select 2 x, 10 y, 10 size\nunion all select 3 x, -9 y, 6 size\nunion all select 4 x, 100 y, 30 size\nunion all select 5 x, -20 y, 70 size",
|
||||
},
|
||||
display: "scatter",
|
||||
visualization_settings: {
|
||||
"graph.dimensions": ["X"],
|
||||
"graph.metrics": ["Y"],
|
||||
},
|
||||
};
|
||||
|
||||
function assertion() {
|
||||
cy.get("circle").should("have.length", 5).last().realHover();
|
||||
|
||||
H.popover().within(() => {
|
||||
H.testPairedTooltipValues("X", "5");
|
||||
H.testPairedTooltipValues("Y", "-20");
|
||||
H.testPairedTooltipValues("SIZE", "70");
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createNativeQuestion(questionDetails, { visitQuestion: true });
|
||||
});
|
||||
|
||||
it("should render negative values in a scatter visualziation (metabase#22527)", () => {
|
||||
assertion();
|
||||
|
||||
H.openVizSettingsSidebar();
|
||||
cy.findByTestId("sidebar-left").within(() => {
|
||||
cy.findByTextEnsureVisible("Data").click();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line metabase/no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Bubble size").parent().contains("Select a field").click();
|
||||
|
||||
H.popover().contains(/size/i).click();
|
||||
|
||||
assertion();
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 25156", () => {
|
||||
const questionDetails = {
|
||||
name: "25156",
|
||||
|
||||
@@ -652,17 +652,27 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
|
||||
},
|
||||
});
|
||||
|
||||
H.pieSlices()
|
||||
.filter("[fill^='hsla']")
|
||||
.then(($slice) => {
|
||||
const { height } = $slice[0].getBoundingClientRect();
|
||||
const otherPieSlice = () => H.pieSlices().filter("[fill^='hsla']");
|
||||
|
||||
cy.wrap($slice).realClick({
|
||||
x: 30,
|
||||
y: height / 2,
|
||||
scrollBehavior: false,
|
||||
});
|
||||
});
|
||||
H.pieSlices().should("have.length", 3);
|
||||
|
||||
otherPieSlice().then(($slice) => {
|
||||
const { height } = $slice[0].getBoundingClientRect();
|
||||
cy.wrap($slice).trigger("mousemove", 30, height / 2);
|
||||
});
|
||||
|
||||
H.assertEChartsTooltip({
|
||||
header: "Category",
|
||||
rows: [
|
||||
{ name: "Doohickey", value: "42" },
|
||||
{ name: "Gizmo", value: "51" },
|
||||
],
|
||||
});
|
||||
|
||||
otherPieSlice().then(($slice) => {
|
||||
const { height } = $slice[0].getBoundingClientRect();
|
||||
cy.wrap($slice).click(30, height / 2, { force: true });
|
||||
});
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByTestId("click-actions-view").within(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[metabase-enterprise.advanced-config.file.interface
|
||||
:as advanced-config.file.i]
|
||||
[metabase.auth-identity.core :as auth-identity]
|
||||
[metabase.setup.core :as setup]
|
||||
[metabase.users.models.user :as user]
|
||||
[metabase.util :as u]
|
||||
@@ -37,18 +38,26 @@
|
||||
|
||||
(defn- init-from-config-file!
|
||||
[user]
|
||||
(if-let [existing-user (select-user (:email user))]
|
||||
(do
|
||||
(log/info (u/format-color :blue "Updating User %d" (:id existing-user)))
|
||||
(let [new-user (update user :login_attributes
|
||||
#(merge % (:login_attributes existing-user)))]
|
||||
(t2/update! :model/User (:id existing-user) new-user)))
|
||||
;; create a new user. If they are the first non-internal User, force them to be an admin.
|
||||
(let [user (cond-> user
|
||||
(not (setup/has-user-setup)) (assoc :is_superuser true))]
|
||||
(log/info (u/colorize :green "Creating the first User for this instance. The first user is always created as an admin."))
|
||||
(log/info (u/colorize :green "Creating new User"))
|
||||
(t2/insert! :model/User user))))
|
||||
;; the profile write and the password write must land together — initialize! runs no transaction of its own
|
||||
(t2/with-transaction [_]
|
||||
(let [password (:password user)
|
||||
;; the password is stored only via set-password!; never hand it to the User model
|
||||
user (dissoc user :password)
|
||||
user-id (if-let [existing-user (select-user (:email user))]
|
||||
(do
|
||||
(log/info (u/format-color :blue "Updating User %d" (:id existing-user)))
|
||||
(let [new-user (update user :login_attributes
|
||||
#(merge % (:login_attributes existing-user)))]
|
||||
(t2/update! :model/User (:id existing-user) new-user))
|
||||
(:id existing-user))
|
||||
;; create a new user. If they are the first non-internal User, force them to be an admin.
|
||||
(let [user (cond-> user
|
||||
(not (setup/has-user-setup)) (assoc :is_superuser true))]
|
||||
(log/info (u/colorize :green "Creating the first User for this instance. The first user is always created as an admin."))
|
||||
(log/info (u/colorize :green "Creating new User"))
|
||||
(u/the-id (t2/insert-returning-instance! :model/User user))))]
|
||||
;; passwords live in the AuthIdentity, not on the User row
|
||||
(auth-identity/set-password! user-id password))))
|
||||
|
||||
(defmethod advanced-config.file.i/initialize-section! :users
|
||||
[_section-name users]
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[metabase.setup.core :as setup]
|
||||
[metabase.startup.core :as startup]
|
||||
[metabase.sync.core :as sync]
|
||||
[metabase.util.i18n :refer [tru]]
|
||||
[metabase.util.log :as log]
|
||||
[toucan2.core :as t2])
|
||||
(:import
|
||||
@@ -63,6 +64,11 @@
|
||||
|
||||
Returns the created database map."
|
||||
[user-id]
|
||||
;; An analytics-dev database is a handle onto the app-db and must not exist outside analytics dev mode; refuse to
|
||||
;; create one unless the mode is on (mirrors the connection-layer guard in
|
||||
;; [[metabase.driver.sql-jdbc.connection/db->pooled-connection-spec]]).
|
||||
(when-not (audit/analytics-dev-mode)
|
||||
(throw (ex-info (tru "Analytics dev mode is not enabled.") {:status-code 400})))
|
||||
(let [db-type (mdb/db-type)]
|
||||
(if-let [existing (find-analytics-dev-database)]
|
||||
(do
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
{:email (sag.settings/support-access-grant-email)
|
||||
:first_name (sag.settings/support-access-grant-first-name)
|
||||
:last_name (sag.settings/support-access-grant-last-name)
|
||||
:password (str (random-uuid))
|
||||
:is_superuser true})))
|
||||
|
||||
(methodical/defmethod t2/batched-hydrate [:model/SupportAccessGrantLog :user_info]
|
||||
|
||||
@@ -105,7 +105,5 @@
|
||||
(:id user) grant-ends-at)
|
||||
(t2/with-transaction [_]
|
||||
(t2/update! :model/AuthIdentity (:id auth-identity) (auth-identity/mark-token-consumed auth-identity))
|
||||
(t2/update! :model/AuthIdentity :user_id (:id user) :provider "password"
|
||||
{:expires_at grant-ends-at
|
||||
:credentials {:plaintext_password password}}))))
|
||||
(auth-identity/set-password! (:id user) password {:expires-at grant-ends-at}))))
|
||||
result)
|
||||
|
||||