Compare commits
9 Commits
python-lib
...
python-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd50ffa7dd | ||
|
|
2880033f06 | ||
|
|
87161c6d71 | ||
|
|
89117dbfb0 | ||
|
|
3a1b93b79d | ||
|
|
2f651e074d | ||
|
|
54a2642138 | ||
|
|
d3a74d538d | ||
|
|
82b097feb8 |
@@ -574,6 +574,7 @@
|
||||
:api #{metabase.legacy-mbql.normalize
|
||||
metabase.legacy-mbql.predicates
|
||||
metabase.legacy-mbql.schema
|
||||
metabase.legacy-mbql.schema.helpers
|
||||
metabase.legacy-mbql.util}
|
||||
:uses #{lib
|
||||
models
|
||||
@@ -590,7 +591,8 @@
|
||||
metabase.lib.field
|
||||
metabase.lib.field.resolution
|
||||
metabase.lib.filter
|
||||
metabase.lib.filter.desugar.jvm ; only so [[metabase.legacy-mbql.jvm-util]] can use defined regexes
|
||||
metabase.lib.filter.desugar
|
||||
metabase.lib.filter.desugar.jvm
|
||||
metabase.lib.init
|
||||
metabase.lib.join.util
|
||||
metabase.lib.metadata
|
||||
@@ -1983,15 +1985,6 @@
|
||||
upload
|
||||
util}}
|
||||
|
||||
enterprise/python-transform
|
||||
{:team "Transform"
|
||||
:api #{metabase-enterprise.python-transform.api
|
||||
metabase-enterprise.python-transform.core
|
||||
metabase-enterprise.python-transform.init}
|
||||
:uses #{api
|
||||
models
|
||||
util}}
|
||||
|
||||
enterprise/upload-management
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.upload-management.api}
|
||||
|
||||
@@ -52,13 +52,7 @@
|
||||
|
||||
(defn- lint-require* [node current-ns config]
|
||||
(let [[_require & args] (:children node)
|
||||
required-namespaces (->> (keep unwrap-require args)
|
||||
;; `require` runs through `cc/load-libs` which allows
|
||||
;; #{:as :reload :reload-all :require :use :verbose :refer :as-alias}
|
||||
;; as args. If you use a REPL tool that applies clj-kondo linting (e.g., Clojure MCP)
|
||||
;; you may wish to `:reload` or `:reload-all` a namespace. So we shouldn't assume all
|
||||
;; args are keywords:
|
||||
(remove keyword?))]
|
||||
required-namespaces (keep unwrap-require args)]
|
||||
(lint* node current-ns config required-namespaces)))
|
||||
|
||||
(defn- lint-requiring-resolve* [node current-ns config]
|
||||
|
||||
7
.github/file-paths.yaml
vendored
7
.github/file-paths.yaml
vendored
@@ -41,17 +41,10 @@ frontend_sources: &frontend_sources
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sources:
|
||||
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_sources:
|
||||
- *frontend_configs
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_package_sources:
|
||||
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_specs: &frontend_specs
|
||||
- *shared_specs
|
||||
- "frontend/test/**"
|
||||
|
||||
@@ -1,66 +1,24 @@
|
||||
const TYPES = new Set([
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param title {string}
|
||||
*/
|
||||
function isConventionalTitle(title) {
|
||||
const match = /^(?<type>\w+)\((?<rawScopes>[^)]+)\):/.exec(title);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, type, rawScopes] = match;
|
||||
const scopes = rawScopes
|
||||
.split(",")
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return TYPES.has(type) && scopes.length > 0;
|
||||
}
|
||||
exports.updateComment = updateComment;
|
||||
|
||||
/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */
|
||||
async function updateCommentForEmbedding({ github, context }) {
|
||||
async function updateComment({ github, context }) {
|
||||
const githubUsername = context.payload.sender.login;
|
||||
|
||||
const comment = `${CONVENTIONAL_COMMENT_IDENTIFIER}
|
||||
@${githubUsername} You have modified embedding code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
|
||||
Here are all the supported types:
|
||||
const comment = `${CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER}
|
||||
@${githubUsername} You have modified embedding SDK code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
|
||||
Here are all the supported types that will be shown in the changelog:
|
||||
- \`feat\`
|
||||
- \`fix\`
|
||||
- \`perf\`
|
||||
- \`docs\`
|
||||
- \`style\`
|
||||
- \`refactor\`
|
||||
- \`test\`
|
||||
- \`build\`
|
||||
- \`ci\`
|
||||
|
||||
Please also make sure to include a scope.
|
||||
These types \`docs\`, \`style\`, \`refactor\`, \`test\`, \`build\`, and \`ci\` are also encouraged for non-changelog related tasks.
|
||||
|
||||
Please also make sure to include \`sdk\` scope, otherwise, the changelog will not include this task.
|
||||
|
||||
For example, these are valid PR titles:
|
||||
- \`feat(sdk-bundle): Add interactive dashboard component\`
|
||||
- \`fix(sdk-package): Fixed MetabaseProviderPropsStore logic\`
|
||||
- \`fix(sdk-package, embed-js): Fix API\``;
|
||||
- \`feat(sdk): Add interactive dashboard component\`
|
||||
- \`feat(sdk): Support theming pie chart\`
|
||||
- \`fix(sdk): Fix static dashboard crash\``;
|
||||
|
||||
return updateComment(comment, { github, context });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param comment {string}
|
||||
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
|
||||
*/
|
||||
async function updateComment(comment, { github, context }) {
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pullRequestNumber = context.issue.number;
|
||||
@@ -91,19 +49,20 @@ async function updateComment(comment, { github, context }) {
|
||||
}
|
||||
}
|
||||
|
||||
const CONVENTIONAL_COMMENT_IDENTIFIER =
|
||||
"<!---conventional comment identifier-->";
|
||||
|
||||
const CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER =
|
||||
"<!---conventional commit reminder comment ID-->";
|
||||
/**
|
||||
* @param comments {{body: string}[]}
|
||||
* @typedef Comment
|
||||
* @property {string} body
|
||||
*
|
||||
* @param {Comment[]} comments
|
||||
*/
|
||||
function getExistingConventionalCommitReminderComment(comments) {
|
||||
for (const comment of comments) {
|
||||
if (comment.body.startsWith(CONVENTIONAL_COMMENT_IDENTIFIER)) {
|
||||
if (
|
||||
comment.body.startsWith(CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER)
|
||||
) {
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.isConventionalTitle = isConventionalTitle;
|
||||
exports.updateCommentForEmbedding = updateCommentForEmbedding;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
const { isConventionalTitle } = require("./utils");
|
||||
|
||||
describe("isConventionalTitle", () => {
|
||||
it("returns true for a simple conventional commit", () => {
|
||||
expect(isConventionalTitle("feat: add new feature")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for a commit with a scope", () => {
|
||||
expect(isConventionalTitle("fix(api): correct API bug")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for a commit with multiple scopes, comma separated", () => {
|
||||
expect(isConventionalTitle("feat(build,deps): update dependencies")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true for a commit with multiple scopes, comma separated with space", () => {
|
||||
expect(isConventionalTitle("feat(build, deps): update dependencies")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for a commit with round brackets in a message", () => {
|
||||
expect(isConventionalTitle("feat: (something) update dependencies")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for missing colon", () => {
|
||||
expect(isConventionalTitle("feat add feature")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for invalid type", () => {
|
||||
expect(isConventionalTitle("invalidType: some message")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty message", () => {
|
||||
expect(isConventionalTitle("feat:")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty input", () => {
|
||||
expect(isConventionalTitle("")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4,53 +4,39 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- enterprise/frontend/src/embedding-sdk-bundle/**
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sources: ${{ steps.changes.outputs.frontend_embedding_sources }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
pr-title-reminder:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
needs: [ files-changed ]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.files-changed.outputs.frontend_embedding_sources == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Check if the PR title is already following Conventional Commits style
|
||||
uses: actions/github-script@v7
|
||||
id: is-conventional-commit
|
||||
with:
|
||||
result-encoding: string
|
||||
script: | # js
|
||||
const { isConventionalTitle } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
const pullRequestTitle = context.payload.pull_request.title
|
||||
const conventionalCommitRegex = /^(?<type>\w+)\((?<scope>\w+)\):/
|
||||
const match = pullRequestTitle.match(conventionalCommitRegex)
|
||||
if (match) {
|
||||
const { type, scope } = match.groups
|
||||
return ['feat', 'fix', 'perf', 'docs', 'style', 'refactor', 'test', 'build', 'ci'].includes(type) && scope === 'sdk'
|
||||
}
|
||||
|
||||
return isConventionalTitle(pullRequestTitle)
|
||||
- name: Checkout repository so `gh` CLI works
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Remind PR authors to use conventional commit in the title
|
||||
if: ${{ steps.is-conventional-commit.outputs.result != 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const { updateCommentForEmbedding } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
|
||||
await updateCommentForEmbedding({github, context})
|
||||
script: |
|
||||
const { updateComment } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
await updateComment({github, context})
|
||||
|
||||
6
.github/workflows/drivers.yml
vendored
6
.github/workflows/drivers.yml
vendored
@@ -343,7 +343,7 @@ jobs:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
- name: Start python-runner
|
||||
working-directory: python-runner
|
||||
run: make run
|
||||
run: make run-all
|
||||
- name: Test H2 driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
@@ -453,7 +453,7 @@ jobs:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
- name: Start python-runner
|
||||
working-directory: python-runner
|
||||
run: make run
|
||||
run: make run-all
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
@@ -762,7 +762,7 @@ jobs:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
- name: Start python-runner
|
||||
working-directory: python-runner
|
||||
run: make run
|
||||
run: make run-all
|
||||
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
|
||||
@@ -140,10 +140,7 @@ jobs:
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js component \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }} \
|
||||
--env grepTags="-@skip-backward-compatibility",grepOmitFiltered=true
|
||||
run: node e2e/runner/run_cypress_ci.js component --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Embedding SDK Labels Reminder
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [auto_merge_enabled, review_requested, labeled, unlabeled, synchronize]
|
||||
branches:
|
||||
- "master"
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sdk_package_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_package_sources }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
embedding-sdk-package-no-release-notes-reminder:
|
||||
needs: [ files-changed ]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(needs.files-changed.outputs.frontend_embedding_sdk_package_sources == 'true')
|
||||
name: Embedding SDK package release-notes/no-release-notes label reminder
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jesusvasquez333/verify-pr-label-action@v1.4.0
|
||||
id: verify-release-notes-labels
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
valid-labels: 'release-notes, no-release-notes'
|
||||
disable-reviews: true
|
||||
- name: Comment on PR if label should be added
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: ${{ steps.verify-release-notes-labels.outcome == 'failure' }}
|
||||
message: "⚠️ Changes were made to the <b>Embedding SDK Package</b>. Review this PR and apply one of the following labels.<br><br>- <code>release-notes</code> — select if this PR must be included in the main Metabase changelog release notes.<br>- <code>no-release-notes</code> — select if this PR should not be included in the main Metabase changelog release notes."
|
||||
include-log: true
|
||||
8
.github/workflows/perf-test-labeled.yml
vendored
8
.github/workflows/perf-test-labeled.yml
vendored
@@ -9,13 +9,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
uberjar:
|
||||
if: ${{ github.event.label.name == 'Run-perf' }}
|
||||
uses: ./.github/workflows/uberjar.yml
|
||||
secrets: inherit
|
||||
|
||||
run-perf-test:
|
||||
needs: [uberjar]
|
||||
if: ${{ github.event.label.name == 'Run-perf' }}
|
||||
uses: ./.github/workflows/perf-test.yml
|
||||
with:
|
||||
wait_for_uberjar: true
|
||||
secrets: inherit
|
||||
|
||||
62
.github/workflows/perf-test.yml
vendored
62
.github/workflows/perf-test.yml
vendored
@@ -3,6 +3,11 @@ run-name: Run perf test for ${{ github.ref_name }} by @${{ github.actor }}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
wait_for_uberjar:
|
||||
description: "Wait for uberjar build"
|
||||
required: false
|
||||
type: boolean
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -26,6 +31,63 @@ jobs:
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
with:
|
||||
registries: "${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}"
|
||||
- name: Wait for uberjar
|
||||
id: wait_for_uberjar
|
||||
if: ${{ inputs.wait_for_uberjar == true }}
|
||||
run: |
|
||||
## Get workflow run id for uberjar build
|
||||
curl -Ls --output e2e-tests.json \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/workflows/run-tests.yml/runs?head_sha=${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ID=$(jq -r '.workflow_runs[0].id' e2e-tests.json)
|
||||
echo "Run ID: ${ID}"
|
||||
|
||||
## Get workflow run id for uberjar build
|
||||
while [ -z "$JOB_ID" ]; do
|
||||
NEXT_URL="https://api.github.com/repos/${{ github.repository }}/actions/runs/${ID}/jobs?filter=latest"
|
||||
|
||||
# Search for build job
|
||||
echo "Searching for build job..."
|
||||
while [ -z "$JOB_ID" ] && [ -n "$NEXT_URL" ]; do
|
||||
# Get one page of jobs
|
||||
RESPONSE=$(curl -s -i \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$NEXT_URL")
|
||||
|
||||
# Extract headers and body
|
||||
HEADERS=$(echo "$RESPONSE" | sed -n '1,/^\r$/p')
|
||||
BODY=$(echo "$RESPONSE" | sed '1,/^\r$/d')
|
||||
LINK_HEADER=$(echo "$HEADERS" | grep -i '^link:' | sed 's/^link: //')
|
||||
|
||||
# Check for next page in Link header
|
||||
if echo "$LINK_HEADER" | grep -q 'rel="next"'; then
|
||||
NEXT_URL=$(echo "$LINK_HEADER" | sed -E 's/.*<([^>]+)>; rel="next".*/\1/')
|
||||
else
|
||||
NEXT_URL=""
|
||||
fi
|
||||
|
||||
# Check for build job
|
||||
JOB_ID=$(echo "$BODY" | jq -r '.jobs[] | select(.name | contains("build (ee)")) | .id')
|
||||
done
|
||||
done
|
||||
echo "Job ID: ${JOB_ID}"
|
||||
|
||||
## Wait for uberjar build to complete
|
||||
while [ true ]; do
|
||||
curl -Ls --output uberjar.json \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/jobs/${JOB_ID}
|
||||
jq -r '.steps[] | select(.name == "Prepare uberjar artifact") | .status' uberjar.json | grep -q "completed" && break
|
||||
echo "Waiting for uberjar build..."
|
||||
sleep 10
|
||||
done
|
||||
echo "run_id=$(jq -r '.workflow_runs[0].id' e2e-tests.json)" >> $GITHUB_OUTPUT
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
|
||||
24
.github/workflows/release-embedding-sdk.yml
vendored
24
.github/workflows/release-embedding-sdk.yml
vendored
@@ -285,6 +285,10 @@ jobs:
|
||||
run: |
|
||||
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
|
||||
- name: Update changelog
|
||||
run: |
|
||||
yarn embedding-sdk:generate-changelog -o changelog-diff
|
||||
|
||||
- name: Build SDK package
|
||||
run: yarn run build-embedding-sdk-package
|
||||
|
||||
@@ -297,6 +301,12 @@ jobs:
|
||||
name: metabase-sdk
|
||||
path: ./resources/embedding-sdk
|
||||
|
||||
- name: Upload changelog diff
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sdk-changelog-diff
|
||||
path: ./changelog-diff
|
||||
|
||||
publish-npm:
|
||||
needs: [build-sdk, determine-sdk-version]
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -325,7 +335,19 @@ jobs:
|
||||
run: |
|
||||
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ env.sdk_version }}
|
||||
|
||||
- name: Create a PR updating readme + published version (using GitHub Metabase Automation account)
|
||||
- name: Retrieve SDK changelog diff
|
||||
if: inputs.branch != 'master'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sdk-changelog-diff
|
||||
|
||||
- name: Update changelog
|
||||
if: inputs.branch != 'master'
|
||||
run: |
|
||||
cat changelog-diff enterprise/frontend/src/embedding-sdk-bundle/CHANGELOG.md > new-changelog
|
||||
mv new-changelog enterprise/frontend/src/embedding-sdk-bundle/CHANGELOG.md
|
||||
|
||||
- name: Create a PR updating readme + published version, and changelog (using GitHub Metabase Automation account)
|
||||
run: |
|
||||
git checkout -b update-sdk-version-${{ env.sdk_version }}
|
||||
|
||||
|
||||
@@ -89,6 +89,6 @@ generateSdkPackage();
|
||||
copyFileToOutput("LICENSE.txt");
|
||||
copyFileToOutput("frontend/src/embedding-sdk-package/README.md", "README.md");
|
||||
copyFileToOutput(
|
||||
"frontend/src/embedding-sdk-package/CHANGELOG.md",
|
||||
"frontend/src/embedding-sdk-bundle/CHANGELOG.md",
|
||||
"CHANGELOG.md",
|
||||
);
|
||||
|
||||
@@ -14,11 +14,11 @@ See a [sample Next.js app that uses the SDK](https://github.com/metabase/metabas
|
||||
|
||||
## SDK components with Server Side Rendering (SSR) or React Server Components
|
||||
|
||||
As of Embedded Analytics SDK v57, SDK components automatically skip server-side rendering (SSR) and render only on the client.
|
||||
As of Embedding Analytics SDK v57, SDK components automatically skip server-side rendering (SSR) and render only on the client.
|
||||
|
||||
### Compatibility layer for Server Side Rendering (SSR) (DEPRECATED)
|
||||
|
||||
As of Embedded Analytics SDK 57, the compatibility layer for server-side rendering (SSR) is deprecated and no longer required. If you use the compatibility layer, change your imports from `@metabase/embedding-sdk-react/next` to `@metabase/embedding-sdk-react`.
|
||||
As of Embedding analytics SDK 57, the compatibility layer for server-side rendering (SSR) is deprecated and no longer required. If you use the compatibility layer, change your imports from `@metabase/embedding-sdk-react/next` to `@metabase/embedding-sdk-react`.
|
||||
|
||||
## Handling authentication
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ Here's a basic overview of the steps you'll want to take when upgrading your SDK
|
||||
## 1. Read the release post and changelog for Metabase and the Embedded analytics SDK
|
||||
|
||||
- [Release posts](https://www.metabase.com/releases) give a good overview of what's in each release, and call out breaking changes (which are rare).
|
||||
- [Metabase + Embedding analytics SDK changelogs](https://www.metabase.com/changelog) list Metabase and Embedding analytics SDK changes.
|
||||
- [Embedded analytics SDK package changelog](https://github.com/metabase/metabase/blob/release-x.56.x/enterprise/frontend/src/embedding-sdk-bundle/CHANGELOG.md) list changes specific to the SDK lightweight `@metabase/embedding-sdk-react` package.
|
||||
- [Metabase changelogs](https://www.metabase.com/changelog) list Metabase changes.
|
||||
- [Embedded analytics SDK changelogs](https://www.metabase.com/changelog/56) list changes specific to the SDK Package.
|
||||
|
||||
Check for any relevant changes, especially deprecations or breaking changes that require you to update your application's code. If there are deprecation changes, we'll have docs that'll walk you through what changes you'll need to make and why.
|
||||
|
||||
|
||||
@@ -127,10 +127,7 @@ const configs = {
|
||||
openMode: args["--open"] || process.env.OPEN_UI === "true",
|
||||
};
|
||||
|
||||
const userArgs = await parseArguments(args);
|
||||
|
||||
const finalConfig = Object.assign({}, sdkComponentConfig, userArgs);
|
||||
return finalConfig;
|
||||
return sdkComponentConfig;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -115,32 +115,6 @@ describe("scenarios > embedding-sdk > metabot-question", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should start a new conversation when clicking a `start new conversation` button", () => {
|
||||
setup(metabotResponse);
|
||||
|
||||
mountSdkContent(<MetabotQuestion />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("metabot-chat-input").type("Show orders {enter}");
|
||||
|
||||
H.mockMetabotResponse({
|
||||
statusCode: 200,
|
||||
body: metabotRetryResponse,
|
||||
});
|
||||
|
||||
cy.findAllByTestId("metabot-chat-message").should("have.length", 2);
|
||||
|
||||
cy.findByTestId("metabot-new-conversation").click();
|
||||
|
||||
cy.findAllByTestId("metabot-chat-message").should("not.exist");
|
||||
cy.findByTestId("metabot-chat-input").should("have.value", "");
|
||||
|
||||
cy.findByTestId("metabot-chat-input").type("Show orders {enter}");
|
||||
|
||||
cy.findAllByTestId("metabot-chat-message").should("have.length", 2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set correct metabot_id both for the a new message and when retrying", () => {
|
||||
setup(metabotResponse);
|
||||
|
||||
|
||||
@@ -26,13 +26,14 @@ const sdkBundleCleanup = () => {
|
||||
|
||||
describe(
|
||||
"scenarios > embedding-sdk > sdk-bundle",
|
||||
{
|
||||
tags: ["@skip-backward-compatibility"],
|
||||
// These test in some cases load a new SDK Bundle that in combination with the Component Testing is memory-consuming
|
||||
numTestsKeptInMemory: 1,
|
||||
},
|
||||
// These test in some cases load a new SDK Bundle that in combination with the Component Testing is memory-consuming
|
||||
{ numTestsKeptInMemory: 1 },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
|
||||
"dashcardQuery",
|
||||
);
|
||||
|
||||
signInAsAdminAndEnableEmbeddingSdk();
|
||||
|
||||
cy.signOut();
|
||||
@@ -43,6 +44,10 @@ describe(
|
||||
[{ strictMode: false }, { strictMode: true }].forEach(({ strictMode }) => {
|
||||
describe(`Common cases ${strictMode ? "with" : "without"} strict mode`, () => {
|
||||
it("should display an SDK question", () => {
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win.console, "warn").as("consoleWarn");
|
||||
});
|
||||
|
||||
mountSdkContent(
|
||||
<InteractiveQuestion questionId={ORDERS_QUESTION_ID} />,
|
||||
{ strictMode },
|
||||
@@ -194,13 +199,6 @@ describe(
|
||||
it("should show a custom loader when the SDK bundle is loading", () => {
|
||||
sdkBundleCleanup();
|
||||
|
||||
cy.intercept("GET", "/api/card/*", (request) => {
|
||||
// Delay request for 500ms to avoid flakiness
|
||||
request.continue(
|
||||
() => new Promise((resolve) => setTimeout(resolve, 500)),
|
||||
);
|
||||
});
|
||||
|
||||
mountSdkContent(
|
||||
<InteractiveQuestion questionId={ORDERS_QUESTION_ID} />,
|
||||
{
|
||||
@@ -287,7 +285,7 @@ describe(
|
||||
describe("Components", () => {
|
||||
it("should display an SDK question with custom layout components", () => {
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win.console, "error").as("consoleError");
|
||||
cy.spy(win.console, "warn").as("consoleWarn");
|
||||
});
|
||||
|
||||
mountSdkContent(
|
||||
@@ -305,13 +303,15 @@ describe(
|
||||
|
||||
cy.findByTestId("visualization-root").should("be.visible");
|
||||
});
|
||||
|
||||
cy.get("@consoleError").should("not.be.called");
|
||||
});
|
||||
|
||||
it("should show an error on a component level if SDK components are not wrapped within the MetabaseProvider", () => {
|
||||
sdkBundleCleanup();
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win.console, "warn").as("consoleWarn");
|
||||
});
|
||||
|
||||
cy.mount(<InteractiveQuestion questionId={ORDERS_QUESTION_ID} />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
@@ -320,41 +320,6 @@ describe(
|
||||
).should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show a console error if SDK Package component uses a prop that is not yet available in SDK bundle", () => {
|
||||
sdkBundleCleanup();
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win.console, "error").as("consoleError");
|
||||
});
|
||||
|
||||
mountSdkContent(
|
||||
<InteractiveQuestion
|
||||
questionId={ORDERS_QUESTION_ID}
|
||||
{...{ foo: "bar" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
cy.get("@consoleError").should(
|
||||
"be.calledWithMatch",
|
||||
"this property is not recognized by the component",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show a console error if SDK Package component does not use a prop that is still expected by SDK bundle", () => {
|
||||
sdkBundleCleanup();
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win.console, "error").as("consoleError");
|
||||
});
|
||||
|
||||
mountSdkContent(<InteractiveQuestion />);
|
||||
|
||||
cy.get("@consoleError").should(
|
||||
"be.calledWithMatch",
|
||||
"this property is required by the component",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error handling", { retries: 3 }, () => {
|
||||
@@ -379,7 +344,7 @@ describe(
|
||||
|
||||
cy.findByTestId("sdk-error-container").should(
|
||||
"contain.text",
|
||||
"Error loading the Embedded Analytics SDK",
|
||||
"Error loading the Embedding Analytics SDK",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -398,7 +363,7 @@ describe(
|
||||
|
||||
cy.findByTestId("sdk-error-container").should(
|
||||
"contain.text",
|
||||
"Custom error: Error loading the Embedded Analytics SDK",
|
||||
"Custom error: Error loading the Embedding Analytics SDK",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,6 @@ import type {
|
||||
DashboardDetails,
|
||||
StructuredQuestionDetails,
|
||||
} from "e2e/support/helpers";
|
||||
import {
|
||||
ORDERS_COUNT_BY_CREATED_AT_AND_PRODUCT_CATEGORY,
|
||||
PRODUCTS_COUNT_BY_CATEGORY_PIE,
|
||||
} from "e2e/support/test-visualizer-data";
|
||||
import type { DictionaryArray } from "metabase-types/api";
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
@@ -27,169 +23,6 @@ import { uploadTranslationDictionaryViaAPI } from "./helpers/e2e-content-transla
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > content translation > static embedding > dashboards", () => {
|
||||
describe("values translation", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
|
||||
uploadTranslationDictionaryViaAPI([
|
||||
{ locale: "fr", msgid: "Gadget", msgstr: "Le gadget" },
|
||||
{ locale: "fr", msgid: "Doohickey", msgstr: "Le doohickey" },
|
||||
{ locale: "fr", msgid: "Gizmo", msgstr: "Le gizmo" },
|
||||
{ locale: "fr", msgid: "Widget", msgstr: "Le widget" },
|
||||
]);
|
||||
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
cy.intercept("GET", "/api/embed/dashboard/*").as("dashboard");
|
||||
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createQuestion(ORDERS_COUNT_BY_CREATED_AT_AND_PRODUCT_CATEGORY, {
|
||||
idAlias: "productsCountByCreatedAtQuestionId",
|
||||
wrapId: true,
|
||||
});
|
||||
|
||||
H.createQuestion(
|
||||
{
|
||||
...PRODUCTS_COUNT_BY_CATEGORY_PIE,
|
||||
visualization_settings: {
|
||||
"pie.rows": [
|
||||
["Widget", "#69C8C8"],
|
||||
["Gadget", "#C7EAEA"],
|
||||
["Gizmo", "#98D9D9"],
|
||||
["Doohickey", "#F3F3F4"],
|
||||
].map(([key, color]) => ({
|
||||
key,
|
||||
name: key,
|
||||
originalName: key,
|
||||
color,
|
||||
defaultColor: false,
|
||||
enabled: true,
|
||||
hidden: false,
|
||||
isOther: false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
idAlias: "productsCountByCategoryPieQuestionId",
|
||||
wrapId: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should assign the proper colors to a pie", () => {
|
||||
H.createDashboard({
|
||||
name: "the_dashboard",
|
||||
}).then(({ body: { id: dashboardId } }) => {
|
||||
H.visitDashboard(dashboardId);
|
||||
H.editDashboard();
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
H.sidebar().findByText(PRODUCTS_COUNT_BY_CATEGORY_PIE.name).click();
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
H.visitEmbeddedPage(
|
||||
{
|
||||
resource: { dashboard: dashboardId as number },
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
additionalHashOptions: {
|
||||
locale: "fr",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
cy.wait("@dashboard");
|
||||
cy.wait("@cardQuery");
|
||||
|
||||
H.getDashboardCard(0).within(() => {
|
||||
cy.findAllByText("Le gadget").should("exist");
|
||||
cy.findAllByText("Le doohickey").should("exist");
|
||||
cy.findAllByText("Le gizmo").should("exist");
|
||||
cy.findAllByText("Le widget").should("exist");
|
||||
|
||||
// Verify colors
|
||||
cy.findByTestId("chart-legend").within(() => {
|
||||
cy.get("button [color]").then(($elements) => {
|
||||
const actualColors = Array.from($elements).map((el) =>
|
||||
el.getAttribute("color"),
|
||||
);
|
||||
expect(actualColors).to.deep.equal([
|
||||
"#69C8C8",
|
||||
"#C7EAEA",
|
||||
"#98D9D9",
|
||||
"#F3F3F4",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should translate static embedding dashboard values on visualizer cards (metabase#62373)", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
|
||||
H.editDashboard();
|
||||
H.removeDashboardCard(0);
|
||||
H.openQuestionsSidebar();
|
||||
|
||||
// Add the regular question
|
||||
H.sidebar()
|
||||
.findByText(ORDERS_COUNT_BY_CREATED_AT_AND_PRODUCT_CATEGORY.name)
|
||||
.click();
|
||||
|
||||
// Add the visualizer question
|
||||
H.clickVisualizeAnotherWay(
|
||||
ORDERS_COUNT_BY_CREATED_AT_AND_PRODUCT_CATEGORY.name,
|
||||
);
|
||||
H.selectVisualization("bar");
|
||||
H.saveDashcardVisualizerModal({ mode: "create" });
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
H.visitEmbeddedPage(
|
||||
{
|
||||
resource: { dashboard: ORDERS_DASHBOARD_ID as number },
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
additionalHashOptions: {
|
||||
locale: "fr",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
cy.wait("@dashboard");
|
||||
cy.wait("@cardQuery");
|
||||
cy.wait("@cardQuery");
|
||||
|
||||
H.getDashboardCard(0).within(() => {
|
||||
cy.findAllByText("Le gadget").should("exist");
|
||||
cy.findAllByText("Le doohickey").should("exist");
|
||||
cy.findAllByText("Le gizmo").should("exist");
|
||||
cy.findAllByText("Le widget").should("exist");
|
||||
});
|
||||
|
||||
H.getDashboardCard(1).within(() => {
|
||||
cy.findAllByText("Le gadget").should("exist");
|
||||
cy.findAllByText("Le doohickey").should("exist");
|
||||
cy.findAllByText("Le gizmo").should("exist");
|
||||
cy.findAllByText("Le widget").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("measure names", () => {
|
||||
before(() => {
|
||||
cy.intercept("POST", "/api/ee/content-translation/upload-dictionary").as(
|
||||
|
||||
@@ -1327,7 +1327,7 @@ describe("scenarios > admin > transforms > jobs", () => {
|
||||
});
|
||||
|
||||
describe("tags", () => {
|
||||
it("should be able to add and remove tags", { tags: "@flaky" }, () => {
|
||||
it("should be able to add and remove tags", () => {
|
||||
H.createTransformJob({ name: "New job" }, { visitTransformJob: true });
|
||||
getTagsInput().click();
|
||||
|
||||
|
||||
@@ -2495,67 +2495,3 @@ describe("issue 62987", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 63180", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
H.createQuestion(
|
||||
{
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
expressions: {
|
||||
Foo: ["+", 1, 2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
|
||||
H.openNotebook();
|
||||
});
|
||||
|
||||
it("should not be possible to close the custom expression editor when creating a new expression from a combine or extract shortcut (metabase#63180)", () => {
|
||||
function testCombineColumns() {
|
||||
H.getNotebookStep("expression").icon("add").click();
|
||||
H.expressionEditorWidget().within(() => {
|
||||
cy.findByText("Combine columns").click();
|
||||
cy.button("Done").scrollIntoView().click();
|
||||
});
|
||||
|
||||
cy.log("clicking outside the editor should not close it");
|
||||
H.getNotebookStep("data").click();
|
||||
H.expressionEditorWidget().should("be.visible");
|
||||
H.modal().should("not.exist");
|
||||
|
||||
cy.log("clearing the expression should allow clicking outside to work");
|
||||
H.CustomExpressionEditor.clear();
|
||||
H.getNotebookStep("data").click();
|
||||
H.expressionEditorWidget().should("not.exist");
|
||||
H.modal().should("not.exist");
|
||||
}
|
||||
|
||||
function testExtractColumns() {
|
||||
H.getNotebookStep("expression").icon("add").click();
|
||||
H.expressionEditorWidget().within(() => {
|
||||
cy.findByText("Extract columns").click();
|
||||
cy.findByText("Email").click();
|
||||
cy.findByText("Domain").click();
|
||||
});
|
||||
|
||||
cy.log("clicking outside the editor should not close it");
|
||||
H.getNotebookStep("data").click();
|
||||
H.expressionEditorWidget().should("be.visible");
|
||||
H.modal().should("not.exist");
|
||||
|
||||
cy.log("clearing the expression should allow clicking outside to work");
|
||||
H.CustomExpressionEditor.clear();
|
||||
H.getNotebookStep("data").click();
|
||||
H.expressionEditorWidget().should("not.exist");
|
||||
H.modal().should("not.exist");
|
||||
}
|
||||
|
||||
testCombineColumns();
|
||||
testExtractColumns();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2180,28 +2180,3 @@ describe("issue 53370", () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 63176", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
});
|
||||
|
||||
it("should not be possible to save a dashboard with an empty name and the correct error should be displayed (metabase#63176)", () => {
|
||||
cy.visit("/");
|
||||
H.newButton().click();
|
||||
H.popover().findByText("Dashboard").click();
|
||||
H.modal().within(() => {
|
||||
cy.findByPlaceholderText("What is the name of your dashboard?").type(" ");
|
||||
cy.button("Create").click();
|
||||
|
||||
cy.findByText("value must be a non-blank string.").should("be.visible");
|
||||
cy.findByPlaceholderText("What is the name of your dashboard?").should(
|
||||
"have.attr",
|
||||
"aria-invalid",
|
||||
"true",
|
||||
);
|
||||
cy.button("Failed").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -452,10 +452,6 @@ describe("scenarios > table-editing", () => {
|
||||
cy.findAllByTestId("row-select-checkbox").eq(15).click();
|
||||
cy.findAllByTestId("row-select-checkbox").eq(16).click();
|
||||
|
||||
cy.log("should not show edit icon when rows are selected");
|
||||
cy.findByTestId("row-edit-icon").should("not.exist");
|
||||
|
||||
cy.log("should bulk delete rows");
|
||||
cy.findByTestId("toast-card").findByText("Delete").click();
|
||||
|
||||
H.modal().within(() => {
|
||||
@@ -471,9 +467,6 @@ describe("scenarios > table-editing", () => {
|
||||
cy.findByTestId("toast-card").should("not.exist");
|
||||
|
||||
H.undoToast().findByText("Successfully deleted").should("be.visible");
|
||||
|
||||
cy.log("should show edit icon when no rows are selected");
|
||||
cy.findAllByTestId("row-edit-icon").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
[metabase-enterprise.metabot-v3.api]
|
||||
[metabase-enterprise.metabot-v3.tools.api]
|
||||
[metabase-enterprise.permission-debug.api]
|
||||
[metabase-enterprise.python-transform.api]
|
||||
[metabase-enterprise.sandbox.api.routes]
|
||||
[metabase-enterprise.scim.routes]
|
||||
[metabase-enterprise.semantic-search.api]
|
||||
@@ -108,7 +107,6 @@
|
||||
"/metabot-tools" metabase-enterprise.metabot-v3.tools.api/routes
|
||||
"/metabot-v3" (premium-handler metabase-enterprise.metabot-v3.api/routes :metabot-v3)
|
||||
"/permission_debug" (premium-handler metabase-enterprise.permission-debug.api/routes :advanced-permissions)
|
||||
"/python-transform" (premium-handler metabase-enterprise.python-transform.api/routes :transforms)
|
||||
"/scim" (premium-handler metabase-enterprise.scim.routes/routes :scim)
|
||||
"/semantic-search" (premium-handler metabase-enterprise.semantic-search.api/routes :semantic-search)
|
||||
"/serialization" (premium-handler metabase-enterprise.serialization.api/routes :serialization)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
(ns metabase-enterprise.python-transform.api
|
||||
(:require
|
||||
[metabase-enterprise.python-transform.models.python-library :as python-library]
|
||||
[metabase.api.common :as api]
|
||||
[metabase.api.macros :as api.macros]
|
||||
[metabase.api.routes.common :refer [+auth]]
|
||||
[metabase.api.util.handlers :as handlers]))
|
||||
|
||||
(api.macros/defendpoint :get "/library/:path"
|
||||
"Get the Python library for user modules."
|
||||
[{:keys [path]} :- [:map [:path :string]]
|
||||
_query-params]
|
||||
(api/check-superuser)
|
||||
(some-> (python-library/get-python-library-by-path path)
|
||||
(select-keys [:source :path :created_at :updated_at])))
|
||||
|
||||
(api.macros/defendpoint :put "/library/:path"
|
||||
"Update the Python library source code for user modules."
|
||||
[{:keys [path]} :- [:map [:path :string]]
|
||||
_query-params
|
||||
body :- [:map {:closed true}
|
||||
[:source :string]]]
|
||||
(api/check-superuser)
|
||||
(python-library/update-python-library-source! path (:source body)))
|
||||
|
||||
(def ^{:arglists '([request respond raise])} routes
|
||||
"`/api/ee/python-transform` routes."
|
||||
(handlers/routes
|
||||
(api.macros/ns-handler *ns* +auth)))
|
||||
@@ -1,5 +0,0 @@
|
||||
(ns metabase-enterprise.python-transform.core
|
||||
(:require
|
||||
[metabase-enterprise.python-transform.models.python-library]))
|
||||
|
||||
(comment metabase-enterprise.python-transform.models.python-library/keep-me)
|
||||
@@ -1,41 +0,0 @@
|
||||
(ns metabase-enterprise.python-transform.models.python-library
|
||||
(:require
|
||||
[metabase.app-db.core :as app-db]
|
||||
[metabase.util.i18n :refer [tru]]
|
||||
[methodical.core :as methodical]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(methodical/defmethod t2/table-name :model/PythonLibrary [_model] :python_library)
|
||||
|
||||
(doseq [trait [:metabase/model :hook/timestamped?]]
|
||||
(derive :model/PythonLibrary trait))
|
||||
|
||||
(def ^:private allowed-paths
|
||||
"Set of allowed library paths. Currently only 'common' is supported."
|
||||
#{"common"})
|
||||
|
||||
(defn- validate-path!
|
||||
"Validates that the given path is allowed. Throws an exception if not."
|
||||
[path]
|
||||
(when-not (contains? allowed-paths path)
|
||||
(throw (ex-info (tru "Invalid library path. Only ''common'' is currently supported.")
|
||||
{:status-code 400
|
||||
:path path
|
||||
:allowed-paths allowed-paths}))))
|
||||
|
||||
(defn get-python-library-by-path
|
||||
"Get the Python library by path."
|
||||
[path]
|
||||
(validate-path! path)
|
||||
(t2/select-one :model/PythonLibrary :path path))
|
||||
|
||||
(defn update-python-library-source!
|
||||
"Update the Python library source code. Creates a new record if none exists. Returns the updated library."
|
||||
[path source]
|
||||
(validate-path! path)
|
||||
(let [id (app-db/update-or-insert! :model/PythonLibrary
|
||||
{:path path}
|
||||
(constantly {:path path :source source}))]
|
||||
(t2/select-one :model/PythonLibrary id)))
|
||||
@@ -12,8 +12,7 @@
|
||||
[metabase.analytics.core :as analytics]
|
||||
[metabase.premium-features.core :refer [defenterprise]]
|
||||
[metabase.search.engine :as search.engine]
|
||||
[metabase.util.log :as log]
|
||||
[toucan2.realize :as t2.realize]))
|
||||
[metabase.util.log :as log]))
|
||||
|
||||
(defn- fallback-engine
|
||||
"Find the highest priority search engine available for fallback."
|
||||
@@ -59,12 +58,7 @@
|
||||
(analytics/inc! :metabase-search/semantic-fallback-triggered {:fallback-engine fallback})
|
||||
(analytics/observe! :metabase-search/semantic-results-before-fallback final-count)
|
||||
(let [fallback-results (try
|
||||
(cond->> (search.engine/results (assoc search-ctx :search-engine fallback))
|
||||
;; The in-place engine returns a reducible (but not seqable) result that needs to
|
||||
;; be realized before we concat and dedup with the semantic engine results.
|
||||
(= :search.engine/in-place fallback)
|
||||
(into [] (comp (map t2.realize/realize)
|
||||
(take (- (semantic.settings/semantic-search-results-limit) final-count)))))
|
||||
(search.engine/results (assoc search-ctx :search-engine fallback))
|
||||
(catch Throwable t
|
||||
(log/warn t "Semantic search fallback errored, ignoring")
|
||||
[]))
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
:creator_id creator_id
|
||||
:database_id database_id
|
||||
:last_editor_id last_editor_id
|
||||
:name (or (:name doc) "")
|
||||
:name (:name doc)
|
||||
:content searchable_text
|
||||
:display_type display_type
|
||||
:archived (some-> archived to-boolean)
|
||||
@@ -145,18 +145,18 @@
|
||||
:text_search_vector (if (:name doc)
|
||||
[:||
|
||||
(search/weighted-tsvector "A" (:name doc))
|
||||
(search/weighted-tsvector "B" (or (:searchable_text doc) ""))]
|
||||
(search/weighted-tsvector "A" (or (:searchable_text doc) "")))
|
||||
(search/weighted-tsvector "B" (:searchable_text doc ""))]
|
||||
(search/weighted-tsvector "A" (:searchable_text doc "")))
|
||||
:text_search_with_native_query_vector
|
||||
(if (:name doc)
|
||||
[:||
|
||||
(search/weighted-tsvector "A" (:name doc))
|
||||
(search/weighted-tsvector "B"
|
||||
(str/join " " (remove str/blank? [(or (:searchable_text doc) "")
|
||||
(or (:native_query doc) "")])))]
|
||||
(str/join " " (remove str/blank? [(:searchable_text doc "")
|
||||
(:native_query doc "")])))]
|
||||
(search/weighted-tsvector "A"
|
||||
(str/join " " (remove str/blank? [(or (:searchable_text doc) "")
|
||||
(or (:native_query doc) "")]))))})
|
||||
(str/join " " (remove str/blank? [(:searchable_text doc "")
|
||||
(:native_query doc "")]))))})
|
||||
|
||||
(defn index-size
|
||||
"Fetches the number of documents in the index table."
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[clojure.string :as str]
|
||||
[metabase-enterprise.transforms.api.transform-job]
|
||||
[metabase-enterprise.transforms.api.transform-tag]
|
||||
[metabase-enterprise.transforms.canceling :as transforms.canceling]
|
||||
[metabase-enterprise.transforms.canceling :as trasnforms.canceling]
|
||||
[metabase-enterprise.transforms.execute :as transforms.execute]
|
||||
[metabase-enterprise.transforms.models.transform :as transform.model]
|
||||
[metabase-enterprise.transforms.models.transform-run :as transform-run]
|
||||
@@ -230,7 +230,7 @@
|
||||
run (api/check-404 (transform-run/running-run-for-transform-id id))]
|
||||
(transform-run-cancelation/mark-cancel-started-run! (:id run))
|
||||
(when (transforms.util/python-transform? transform)
|
||||
(transforms.canceling/cancel-run! (:id run))))
|
||||
(trasnforms.canceling/cancel-run! (:id run))))
|
||||
nil)
|
||||
|
||||
(api.macros/defendpoint :post "/:id/run"
|
||||
@@ -274,7 +274,7 @@
|
||||
(api/check-superuser)
|
||||
(let [run-id python-test-run-id
|
||||
cancel-chan (a/promise-chan)]
|
||||
(transforms.canceling/chan-start-run! run-id cancel-chan)
|
||||
(trasnforms.canceling/chan-start-run! run-id cancel-chan)
|
||||
(try
|
||||
(let [{:keys [response output events]} (transforms.execute/test-python-transform! (:code body) (:tables body) run-id cancel-chan)
|
||||
{:keys [body status]} response]
|
||||
@@ -293,7 +293,7 @@
|
||||
:exit_code (:exit_code (:exit_code body))})
|
||||
(assoc :status status)))))
|
||||
(finally
|
||||
(transforms.canceling/chan-end-run! run-id)))))
|
||||
(trasnforms.canceling/chan-end-run! run-id)))))
|
||||
|
||||
(api.macros/defendpoint :post "/test-python/cancel"
|
||||
"Cancel the current test-python execution."
|
||||
@@ -301,7 +301,7 @@
|
||||
_query-params]
|
||||
(log/info "canceling test python execution")
|
||||
(api/check-superuser)
|
||||
(if (transforms.canceling/chan-signal-cancel! python-test-run-id)
|
||||
(if (trasnforms.canceling/chan-signal-cancel! python-test-run-id)
|
||||
(-> (response/response {:message (deferred-tru "Python test canceled")})
|
||||
(assoc :status 200))
|
||||
(-> (response/response {:message (deferred-tru "No running Python test to cancel")})
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[toucan2.core :as t2])
|
||||
(:import
|
||||
(clojure.lang IDeref)
|
||||
(java.io Closeable File)
|
||||
(java.time Duration)))
|
||||
|
||||
@@ -167,16 +168,24 @@
|
||||
(catch Throwable e
|
||||
(log/errorf e "An exception was caught during msg update loop, run-id: %s" run-id)))))
|
||||
|
||||
(defn- open-log-future! ^Closeable [run-id message-log]
|
||||
(let [cleanup (fn [fut]
|
||||
(future-cancel fut)
|
||||
(if (= ::timeout (try (deref fut 10000 ::timeout) (catch Throwable _)))
|
||||
(log/fatalf "Log polling task did not respond to interrupt, run-id: %s" run-id)
|
||||
(log/debugf "Log polling task done, run-id: %s")))
|
||||
fut (u.jvm/in-virtual-thread*
|
||||
(log-loop! run-id message-log))]
|
||||
(reify Closeable
|
||||
(close [_] (cleanup fut)))))
|
||||
(defn- open-log-thread! ^Closeable [run-id message-log]
|
||||
(let [log-thread-promise (promise)
|
||||
cleanup (fn []
|
||||
(let [^Thread log-thread (deref log-thread-promise 1000 nil)]
|
||||
(when-not log-thread (log/fatalf "Log thread reference not bound, run-id: %s" run-id))
|
||||
(when (.isAlive log-thread)
|
||||
(.interrupt log-thread)
|
||||
(when-not (.join log-thread (Duration/ofSeconds 10))
|
||||
(log/fatalf "Log thread could not be interrupted, run-id: %s" run-id)))))]
|
||||
(u.jvm/in-virtual-thread*
|
||||
(deliver log-thread-promise (Thread/currentThread))
|
||||
(log-loop! run-id message-log))
|
||||
(when-not (deref log-thread-promise 1000 nil) (log/fatalf "Log thread reference not bound, run-id: %s" run-id))
|
||||
(reify
|
||||
IDeref
|
||||
(deref [_] @log-thread-promise)
|
||||
Closeable
|
||||
(close [_] (cleanup)))))
|
||||
|
||||
(defn- debug-info-str [{:keys [exit-code events]}]
|
||||
(str/join "\n"
|
||||
@@ -253,7 +262,7 @@
|
||||
:output output})))
|
||||
|
||||
(defn- run-python-transform! [{:keys [source] :as transform} db run-id cancel-chan message-log]
|
||||
(with-open [log-thread-ref (open-log-future! run-id message-log)
|
||||
(with-open [log-thread-ref (open-log-thread! run-id message-log)
|
||||
shared-storage-ref (python-runner/open-s3-shared-storage! (:source-tables source))]
|
||||
(let [driver (:engine db)
|
||||
server-url (transforms.settings/python-execution-server-url)
|
||||
|
||||
@@ -393,7 +393,6 @@
|
||||
table-name->manifest-url (update-vals table-name->id (comp :url #(get objects [:table % :manifest])))
|
||||
|
||||
payload {:code code
|
||||
:library (t2/select-fn->fn :path :source :model/PythonLibrary)
|
||||
:timeout 30
|
||||
:request_id run-id
|
||||
:output_url (:url output)
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
(ns metabase-enterprise.python-transform.api-test
|
||||
"Tests for /api/ee/python-transform endpoints."
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.python-transform.models.python-library :as python-library]
|
||||
[metabase.test :as mt]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(deftest get-library-path-test
|
||||
(testing "GET /api/ee/python-transform/library/:path"
|
||||
(mt/with-premium-features #{:transforms}
|
||||
(testing "requires superuser permissions"
|
||||
(is (= "You don't have permissions to do that."
|
||||
(mt/user-http-request :rasta :get 403 "ee/python-transform/library/common"))))
|
||||
|
||||
(testing "returns nil when no library exists"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(is (nil? (mt/user-http-request :crowberto :get 204 "ee/python-transform/library/common"))))
|
||||
|
||||
(testing "returns existing library"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(python-library/update-python-library-source! "common" "def my_function():\n return 42")
|
||||
(is (=? {:source "def my_function():\n return 42"
|
||||
:path "common"
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(mt/user-http-request :crowberto :get 200 "ee/python-transform/library/common"))))
|
||||
|
||||
(testing "rejects invalid paths"
|
||||
(is (= "Invalid library path. Only 'common' is currently supported."
|
||||
(:message (mt/user-http-request :crowberto :get 400 "ee/python-transform/library/invalid-path"))))))))
|
||||
|
||||
(deftest put-library-path-test
|
||||
(testing "PUT /api/ee/python-transform/library/:path"
|
||||
(mt/with-premium-features #{:transforms}
|
||||
(testing "requires superuser permissions"
|
||||
(is (= "You don't have permissions to do that."
|
||||
(mt/user-http-request :rasta :put 403 "ee/python-transform/library/common"
|
||||
{:source "def test(): pass"}))))
|
||||
|
||||
(testing "validates request body"
|
||||
(testing "requires source field"
|
||||
(is (=? {:errors
|
||||
{:source "string"}}
|
||||
(mt/user-http-request :crowberto :put 400 "ee/python-transform/library/common"
|
||||
{}))))
|
||||
|
||||
(testing "allows empty string"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(is (=? {:source ""
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(mt/user-http-request :crowberto :put 200 "ee/python-transform/library/common"
|
||||
{:source ""})))))
|
||||
|
||||
(testing "creates new library when none exists"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(is (=? {:source "def new_function():\n return 100"
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(mt/user-http-request :crowberto :put 200 "ee/python-transform/library/common"
|
||||
{:source "def new_function():\n return 100"})))
|
||||
(is (= "def new_function():\n return 100"
|
||||
(t2/select-one-fn :source :model/PythonLibrary))))
|
||||
|
||||
(testing "updates existing library"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(python-library/update-python-library-source! "common" "def old_function():\n return 1")
|
||||
(is (=? {:source "def updated_function():\n return 2"
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(mt/user-http-request :crowberto :put 200 "ee/python-transform/library/common"
|
||||
{:source "def updated_function():\n return 2"})))
|
||||
(is (= "def updated_function():\n return 2"
|
||||
(t2/select-one-fn :source :model/PythonLibrary)))
|
||||
(is (= 1 (t2/count :model/PythonLibrary)) "Should not create duplicate records"))
|
||||
|
||||
(testing "rejects invalid paths"
|
||||
(is (= "Invalid library path. Only 'common' is currently supported."
|
||||
(:message (mt/user-http-request :crowberto :put 400 "ee/python-transform/library/invalid-path"
|
||||
{:source "def test(): pass"}))))))))
|
||||
@@ -1,56 +0,0 @@
|
||||
(ns metabase-enterprise.python-transform.models.python-library-test
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.python-transform.models.python-library :as python-library]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(deftest update-python-library-source-test
|
||||
(testing "update-python-library-source!"
|
||||
(testing "creates new record when none exists"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(is (=? {:source "def new_func(): return 1"
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(python-library/update-python-library-source! "common" "def new_func(): return 1")))
|
||||
(is (= 1 (t2/count :model/PythonLibrary)))
|
||||
(is (= "def new_func(): return 1"
|
||||
(t2/select-one-fn :source :model/PythonLibrary))))
|
||||
|
||||
(testing "updates existing record"
|
||||
(is (= 1 (t2/count :model/PythonLibrary)))
|
||||
(is (=? {:source "def updated_func(): return 2"
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(python-library/update-python-library-source! "common" "def updated_func(): return 2")))
|
||||
(is (= 1 (t2/count :model/PythonLibrary)) "Should not create duplicate")
|
||||
(is (= "def updated_func(): return 2"
|
||||
(t2/select-one-fn :source :model/PythonLibrary))))
|
||||
|
||||
(testing "rejects invalid paths"
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
#"Invalid library path"
|
||||
(python-library/update-python-library-source! "invalid-path" "def test(): pass")))
|
||||
(is (= 1 (t2/count :model/PythonLibrary)) "Should not create library with invalid path"))))
|
||||
|
||||
(deftest get-python-library-by-path-test
|
||||
(testing "get-python-library-by-path"
|
||||
(testing "returns library when path is valid"
|
||||
(t2/delete! :model/PythonLibrary)
|
||||
(python-library/update-python-library-source! "common" "def test(): pass")
|
||||
(is (=? {:source "def test(): pass"
|
||||
:path "common"
|
||||
:id integer?
|
||||
:created_at some?
|
||||
:updated_at some?}
|
||||
(python-library/get-python-library-by-path "common"))))
|
||||
|
||||
(testing "rejects invalid paths"
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
#"Invalid library path"
|
||||
(python-library/get-python-library-by-path "invalid-path"))))))
|
||||
@@ -186,12 +186,13 @@
|
||||
:query {:source-query {:source-table $$checkins
|
||||
:fields [$id $date $user_id $venue_id]
|
||||
:filter [:and
|
||||
;; This still gets :default bucketing! auto-bucket-datetimes
|
||||
;; puts :day bucketing on both parts of this filter, since it's
|
||||
;; matching a YYYY-mm-dd string. Then optimize-temporal-filters
|
||||
;; sees that the
|
||||
;; :type/Date column already has :day
|
||||
;; granularity, and switches both to :default
|
||||
;; This still gets :default bucketing!
|
||||
;; auto-bucket-datetimes puts :day bucketing
|
||||
;; on both parts of this filter, since it's
|
||||
;; matching a YYYY-mm-dd string. Then
|
||||
;; optimize-temporal-filters sees that the
|
||||
;; :type/Date column already has :day
|
||||
;; granularity, and switches both to :default
|
||||
[:> !default.date [:absolute-datetime
|
||||
#t "2014-01-01"
|
||||
:default]]
|
||||
|
||||
@@ -3,20 +3,31 @@
|
||||
[clojure.core.memoize :as memoize]
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.api :as semantic.api]
|
||||
[metabase-enterprise.semantic-search.embedding :as semantic.embedding]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
[metabase-enterprise.semantic-search.pgvector-api :as semantic.pgvector-api]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase.search.ingestion :as search.ingestion]
|
||||
[metabase.test :as mt]))
|
||||
[metabase.test :as mt]
|
||||
[metabase.test.fixtures :as fixtures]))
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
(use-fixtures :once (compose-fixtures
|
||||
(fixtures/initialize :db)
|
||||
#'semantic.tu/once-fixture))
|
||||
|
||||
(defmacro with-mock-index-metadata!
|
||||
[& body]
|
||||
`(with-redefs [semantic.env/get-pgvector-datasource! (constantly semantic.tu/db)
|
||||
semantic.embedding/get-configured-model (constantly semantic.tu/mock-embedding-model)
|
||||
semantic.env/get-index-metadata (constantly semantic.tu/mock-index-metadata)]
|
||||
~@body))
|
||||
|
||||
(deftest status-endpoint-test
|
||||
(testing "GET /api/ee/semantic-search/status"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db! {:mode :mock-initialized}
|
||||
(with-mock-index-metadata!
|
||||
(with-open [index-ref (semantic.tu/open-temp-index!)]
|
||||
(with-redefs [semantic.index-metadata/get-active-index-state (fn [_ _]
|
||||
{:index @index-ref
|
||||
@@ -60,18 +71,17 @@
|
||||
(deftest re-init-endpoint-test
|
||||
(testing "POST /api/search/re-init with semantic search support"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(let [original-index semantic.tu/mock-index
|
||||
original-table-name (:table-name original-index)
|
||||
new-index (with-redefs [semantic.index/model-table-suffix (constantly 345)]
|
||||
(#'semantic.pgvector-api/fresh-index semantic.tu/mock-index-metadata semantic.tu/mock-embedding-model :force-reset? true))
|
||||
new-table-name (:table-name new-index)
|
||||
pgvector (semantic.env/get-pgvector-datasource!)]
|
||||
new-table-name (:table-name new-index)]
|
||||
|
||||
(is (semantic.tu/table-exists-in-db? original-table-name))
|
||||
(is (not (semantic.tu/table-exists-in-db? new-table-name)))
|
||||
|
||||
(let [best-index (semantic.index-metadata/find-compatible-index! pgvector semantic.tu/mock-index-metadata semantic.tu/mock-embedding-model)]
|
||||
(let [best-index (semantic.index-metadata/find-compatible-index! semantic.tu/db semantic.tu/mock-index-metadata semantic.tu/mock-embedding-model)]
|
||||
(is (=? original-index (:index best-index)))
|
||||
(is (:active best-index)))
|
||||
|
||||
@@ -86,7 +96,7 @@
|
||||
|
||||
(is (zero? (semantic.tu/index-count new-index))))
|
||||
|
||||
(let [best-index (semantic.index-metadata/find-compatible-index! pgvector semantic.tu/mock-index-metadata semantic.tu/mock-embedding-model)]
|
||||
(let [best-index (semantic.index-metadata/find-compatible-index! semantic.tu/db semantic.tu/mock-index-metadata semantic.tu/mock-embedding-model)]
|
||||
(is (=? new-index (:index best-index)))
|
||||
(is (:active best-index)))
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.core :as semantic.core]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.pgvector-api :as semantic.pgvector-api]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase.app-db.core :as mdb]
|
||||
@@ -11,17 +10,19 @@
|
||||
[metabase.search.in-place.legacy :as in-place.legacy]
|
||||
[metabase.search.in-place.scoring :as in-place.scoring]
|
||||
[metabase.search.settings :as search.settings]
|
||||
[metabase.test :as mt]))
|
||||
[metabase.test :as mt]
|
||||
[metabase.test.fixtures :as fixtures]))
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
(use-fixtures :once (compose-fixtures
|
||||
(fixtures/initialize :db)
|
||||
#'semantic.tu/once-fixture))
|
||||
|
||||
(deftest fallback-engine-available-with-semantic-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/with-temporary-setting-values [search.settings/search-engine "semantic"]
|
||||
(with-open [_ (semantic.tu/open-temp-index!)]
|
||||
(semantic.tu/cleanup-index-metadata! (semantic.env/get-pgvector-datasource!)
|
||||
semantic.tu/mock-index-metadata)
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/cleanup-index-metadata! semantic.tu/db semantic.tu/mock-index-metadata)
|
||||
(semantic.tu/with-index!
|
||||
(is (search.engine/supported-engine? (if (= :mysql (mdb/db-type))
|
||||
;; appdb is not supported on :mysql, should fallback to in-place
|
||||
:search.engine/in-place
|
||||
@@ -55,9 +56,7 @@
|
||||
in-place.legacy/results
|
||||
(fn [_]
|
||||
(reset! legacy-called? true)
|
||||
(reify clojure.lang.IReduceInit
|
||||
(reduce [_this rf init]
|
||||
(reduce rf init [{:id 2 :name "legacy-result" :model "card" :collection_id 1 :score 0.7}]))))]
|
||||
[{:id 2 :name "legacy-result" :model "card" :collection_id 1 :score 0.7}])]
|
||||
(testing "API defaults to semantic engine when configured"
|
||||
(let [response (mt/user-http-request :crowberto :get 200 "search" :q "test")]
|
||||
(is @semantic-called? "Semantic search should be called by default")
|
||||
@@ -98,15 +97,11 @@
|
||||
{:result (dissoc result :score)
|
||||
:score (:score result)})
|
||||
search.engine/results (fn [ctx]
|
||||
(let [fallback-results* #(if (fn? fallback-results)
|
||||
(fallback-results ctx)
|
||||
fallback-results)]
|
||||
(case (:search-engine ctx)
|
||||
:search.engine/semantic (semantic.core/results ctx)
|
||||
:search.engine/in-place (reify clojure.lang.IReduceInit
|
||||
(reduce [_this rf init]
|
||||
(reduce rf init (fallback-results*))))
|
||||
(fallback-results*))))]
|
||||
(case (:search-engine ctx)
|
||||
:search.engine/semantic (semantic.core/results ctx)
|
||||
(if (fn? fallback-results)
|
||||
(fallback-results ctx)
|
||||
fallback-results)))]
|
||||
(thunk)))
|
||||
|
||||
(defn- make-card-result
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.test :refer :all]
|
||||
[honey.sql :as sql]
|
||||
[metabase-enterprise.semantic-search.dlq :as semantic.dlq]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.gate :as semantic.gate]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
@@ -47,7 +46,7 @@
|
||||
|
||||
(deftest dlq-table-creation-test
|
||||
(testing "DLQ table creation and existence checks"
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
index-id 42]
|
||||
(with-open [_ (semantic.tu/open-metadata! pgvector index-metadata)]
|
||||
@@ -59,7 +58,7 @@
|
||||
|
||||
(deftest dlq-entry-management-test
|
||||
(testing "DLQ entry add, query, and delete operations"
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
index-id 42]
|
||||
(with-open [_ (semantic.tu/open-metadata! pgvector index-metadata)
|
||||
@@ -98,7 +97,7 @@
|
||||
|
||||
(deftest dlq-entry-upsert-test
|
||||
(testing "DLQ entry upsert behavior on conflict"
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
index-id 42]
|
||||
(with-open [_ (semantic.tu/open-metadata! pgvector index-metadata)
|
||||
@@ -130,7 +129,7 @@
|
||||
(is (= t2 (:error_gated_at (first results))))))))))
|
||||
|
||||
(deftest dlq-poll-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -195,7 +194,7 @@
|
||||
(is (= 3 (count (semantic.dlq/poll pgvector index-metadata @index-id-ref 10)))))))))
|
||||
|
||||
(deftest dlq-retry-loop-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -297,7 +296,7 @@
|
||||
(is (pos? (:failure-count result)))))))))))
|
||||
|
||||
(deftest try-batch-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[clojure.test :refer :all]
|
||||
[clojure.walk :as walk]
|
||||
[honey.sql :as sql]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.gate :as semantic.gate]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
@@ -90,7 +89,7 @@
|
||||
(:ts (jdbc/execute-one! pgvector ["select clock_timestamp() ts"])))
|
||||
|
||||
(deftest gate-documents!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
t0 (ts "2025-01-01T00:00:00Z")
|
||||
t1 (ts "2025-01-01T00:01:00Z")
|
||||
@@ -169,7 +168,7 @@
|
||||
(comparable-gate-row (dissoc (get-gate-row! pgvector index-metadata (:id (version c3 t3))) :gated_at))))))))
|
||||
|
||||
(deftest poll-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
epoch-watermark {:last-poll Instant/EPOCH :last-seen Instant/EPOCH}
|
||||
c1 {:model "card" :id "123" :searchable_text "a"}
|
||||
@@ -276,7 +275,7 @@
|
||||
(:last-seen watermark))))))
|
||||
|
||||
(deftest flush-watermark!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model1 {:provider "foo" :model-name "m1" :vector-dimensions 42}
|
||||
model2 {:provider "foo" :model-name "m2" :vector-dimensions 42}
|
||||
@@ -331,7 +330,7 @@
|
||||
|
||||
(deftest gate-documents-metrics-test
|
||||
(mt/with-prometheus-system! [_ system]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
t1 (ts "2025-01-01T00:01:00Z")
|
||||
c1 {:model "card" :id "123" :searchable_text "Dog Training Guide"}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns metabase-enterprise.semantic-search.index-metadata-test
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
@@ -24,7 +23,7 @@
|
||||
semantic.index/hnsw-index-name)))))))
|
||||
|
||||
(deftest create-tables-if-not-exists!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
;; could use open-tables here but I chose to be explicit
|
||||
sut #(semantic.tu/closeable
|
||||
@@ -47,7 +46,7 @@
|
||||
(fn [_] (semantic.tu/cleanup-index-metadata! pgvector index-metadata))))
|
||||
|
||||
(deftest drop-tables-if-exists!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
sut semantic.index-metadata/drop-tables-if-exists!]
|
||||
(with-open [_ (open-tables! pgvector index-metadata)]
|
||||
@@ -60,7 +59,7 @@
|
||||
(is (= now-expected (semantic.tu/get-table-names pgvector)))))))
|
||||
|
||||
(deftest ensure-control-row-exists!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
sut semantic.index-metadata/ensure-control-row-exists!]
|
||||
(with-open [_ (open-tables! pgvector index-metadata)]
|
||||
@@ -75,7 +74,7 @@
|
||||
(is (= control-snap (semantic.tu/get-control-rows pgvector index-metadata))))))))
|
||||
|
||||
(deftest activate-index!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
un-index (semantic.index/default-index semantic.tu/mock-embedding-model)
|
||||
index1 (semantic.index-metadata/qualify-index (assoc un-index :table-name "i1") index-metadata)
|
||||
@@ -99,7 +98,7 @@
|
||||
(is (=? [{:active_id index-id2}] (semantic.tu/get-control-rows pgvector index-metadata)))))))))
|
||||
|
||||
(deftest get-active-index-state-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
un-index (semantic.index/default-index semantic.tu/mock-embedding-model)
|
||||
index (semantic.index-metadata/qualify-index un-index index-metadata)
|
||||
@@ -135,7 +134,7 @@
|
||||
(run! #(add-index! pgvector index-metadata %) inactive))
|
||||
|
||||
(deftest find-compatible-index!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
embedding-model1 semantic.tu/mock-embedding-model
|
||||
embedding-model2 (assoc semantic.tu/mock-embedding-model
|
||||
:model-name "mock2")
|
||||
@@ -191,7 +190,7 @@
|
||||
(is (nil? (sut' model))))))))))))
|
||||
|
||||
(deftest create-new-index-spec-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
embedding-model semantic.tu/mock-embedding-model
|
||||
sut semantic.index-metadata/create-new-index-spec]
|
||||
@@ -203,7 +202,7 @@
|
||||
result))))))
|
||||
|
||||
(deftest record-new-index-table!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
embedding-model semantic.tu/mock-embedding-model
|
||||
index (default-index embedding-model index-metadata)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.embedding :as semantic.embedding]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase.analytics.core :as analytics]
|
||||
@@ -19,14 +18,14 @@
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(with-open [index-ref (semantic.tu/open-temp-index!)]
|
||||
;; open-temp-index-table! creates the temp table, so drop it in order to test create!.
|
||||
(semantic.index/drop-index-table! (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index)
|
||||
(semantic.index/drop-index-table! semantic.tu/db semantic.tu/mock-index)
|
||||
(testing "index table is not present before create!"
|
||||
(is (not (semantic.tu/table-exists-in-db? (:table-name @index-ref))))
|
||||
(is (not (semantic.tu/table-has-index? (:table-name @index-ref) (semantic.index/hnsw-index-name @index-ref))))
|
||||
(is (not (semantic.tu/table-has-index? (:table-name @index-ref) (semantic.index/fts-index-name @index-ref))))
|
||||
(is (not (semantic.tu/table-has-index? (:table-name @index-ref) (semantic.index/fts-native-index-name @index-ref)))))
|
||||
(testing "index table is present after create!"
|
||||
(semantic.index/create-index-table-if-not-exists! (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index {:force-reset? false})
|
||||
(semantic.index/create-index-table-if-not-exists! semantic.tu/db semantic.tu/mock-index {:force-reset? false})
|
||||
(is (semantic.tu/table-exists-in-db? (:table-name @index-ref)))
|
||||
(is (semantic.tu/table-has-index? (:table-name @index-ref) (semantic.index/hnsw-index-name @index-ref)))
|
||||
(is (semantic.tu/table-has-index? (:table-name @index-ref) (semantic.index/fts-index-name @index-ref)))
|
||||
@@ -38,7 +37,7 @@
|
||||
(testing "index table is present before drop!"
|
||||
(is (semantic.tu/table-exists-in-db? (:table-name @index-ref))))
|
||||
(testing "index table is not present after drop!"
|
||||
(semantic.index/drop-index-table! (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index)
|
||||
(semantic.index/drop-index-table! semantic.tu/db semantic.tu/mock-index)
|
||||
(is (not (semantic.tu/table-exists-in-db? (:table-name @index-ref))))))))
|
||||
|
||||
(deftest upsert-index!-test
|
||||
@@ -344,7 +343,7 @@
|
||||
(testing "Permission filtering metrics"
|
||||
(reset! analytics-calls [])
|
||||
(mt/with-test-user :crowberto
|
||||
(semantic.index/query-index (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index
|
||||
(semantic.index/query-index semantic.tu/db semantic.tu/mock-index
|
||||
{:search-string "dog training"}))
|
||||
|
||||
(let [permission-calls (filter #(= :metabase-search/semantic-permission-filter-ms (first %)) @analytics-calls)]
|
||||
@@ -356,7 +355,7 @@
|
||||
(testing "Semantic search timing metrics"
|
||||
(reset! analytics-calls [])
|
||||
(mt/with-test-user :crowberto
|
||||
(semantic.index/query-index (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index
|
||||
(semantic.index/query-index semantic.tu/db semantic.tu/mock-index
|
||||
{:search-string "elephant migration"
|
||||
:filter-items-in-personal-collection "only"}))
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.test :refer :all]
|
||||
[honey.sql :as sql]
|
||||
[metabase-enterprise.semantic-search.dlq :as semantic.dlq]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.gate :as semantic.gate]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
@@ -34,7 +33,7 @@
|
||||
(Timestamp/from (Instant/parse s)))
|
||||
|
||||
(deftest indexing-step-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -215,7 +214,7 @@
|
||||
(log/fatal "Indexing loop thread not exiting during test!")))))))
|
||||
|
||||
(deftest indexing-loop-thread-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
index semantic.tu/mock-index
|
||||
metadata-row {:indexer_last_poll Instant/EPOCH
|
||||
@@ -284,7 +283,7 @@
|
||||
(is (.join thread (Duration/ofSeconds 1)))))))))
|
||||
|
||||
(deftest indexing-loop-exit-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -372,7 +371,7 @@
|
||||
(fn [_] (semantic.dlq/drop-dlq-table-if-exists! pgvector index-metadata index-id))))
|
||||
|
||||
(deftest quartz-job-run!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
open-job-thread (fn [& args]
|
||||
(let [caught-ex (volatile! nil)]
|
||||
@@ -459,7 +458,7 @@
|
||||
|
||||
(deftest dlq-step-test
|
||||
(mt/with-prometheus-system! [_ system]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -542,7 +541,7 @@
|
||||
|
||||
(deftest indexer-stall-and-recovery-test
|
||||
(mt/with-prometheus-system! [_ system]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -661,7 +660,7 @@
|
||||
(mt/metric-value system :metabase-search/semantic-indexer-poll-to-poll-interval-ms)))))))))))
|
||||
|
||||
(deftest dlq-integration-with-indexer-loop-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model
|
||||
index (semantic.index-metadata/qualify-index (semantic.index/default-index model) index-metadata)
|
||||
@@ -777,7 +776,7 @@
|
||||
|
||||
(deftest indexer-loop-metric-test
|
||||
(mt/with-prometheus-system! [_ system]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
metadata-row {:id 42
|
||||
:indexer_last_poll (ts "2025-01-01T12:00:00Z")
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
[metabase-enterprise.semantic-search.db.datasource :as semantic.db.datasource]
|
||||
[metabase-enterprise.semantic-search.db.migration :as semantic.db.migration]
|
||||
[metabase-enterprise.semantic-search.db.migration.impl :as semantic.db.migration.impl]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.pgvector-api :as semantic.pgvector-api]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
@@ -22,10 +21,11 @@
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
(use-fixtures :each #'semantic.tu/ensure-no-migration-table-fixture)
|
||||
|
||||
(deftest migration-table-versions-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db-defaults!
|
||||
(semantic.tu/with-test-db! semantic.tu/default-test-db
|
||||
(letfn [(migrate-and-get-db-version
|
||||
[attempted-version]
|
||||
(with-redefs [semantic.db.migration.impl/schema-version attempted-version
|
||||
@@ -56,14 +56,15 @@
|
||||
|
||||
(deftest migration-lock-coordination-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db-defaults!
|
||||
(semantic.tu/with-test-db! semantic.tu/default-test-db
|
||||
(testing "Migration of simultaneous init attempt is blocked"
|
||||
(let [original-write-fn @#'semantic.db.migration/write-successful-migration!
|
||||
original-migrate-fn @#'semantic.db.connection/do-with-migrate-tx
|
||||
original-maybe-migrate @#'semantic.db.migration/maybe-migrate!
|
||||
results (atom {:executed-migrations 0
|
||||
:log []})]
|
||||
(with-redefs-fn {#'semantic.pgvector-api/index-documents! (constantly nil)
|
||||
(with-redefs-fn {;; TODO: why in ci init index won't succeed -- http on embedding
|
||||
#'semantic.pgvector-api/index-documents! (constantly nil)
|
||||
#'semantic.db.connection/do-with-migrate-tx
|
||||
(fn [& args]
|
||||
(let [tid (.getId (Thread/currentThread))]
|
||||
@@ -120,12 +121,12 @@
|
||||
(deftest expected-db-schema-after-migration-test
|
||||
(try
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db-defaults!
|
||||
(semantic.tu/with-test-db! semantic.tu/default-test-db
|
||||
(with-redefs [semantic.pgvector-api/index-documents! (constantly nil)]
|
||||
(semantic.core/init! (semantic.tu/mock-documents) nil)
|
||||
(testing "migration table has expected columns"
|
||||
(is (map-contains-keys?
|
||||
(jdbc/execute-one! (semantic.env/get-pgvector-datasource!)
|
||||
(jdbc/execute-one! (semantic.db.datasource/ensure-initialized-data-source!)
|
||||
(sql/format {:select [:*]
|
||||
:from [:migration]}))
|
||||
(qualify :migration [:migrated_at
|
||||
@@ -133,7 +134,7 @@
|
||||
:version]))))
|
||||
(testing "control table has expected columns"
|
||||
(is (map-contains-keys?
|
||||
(jdbc/execute-one! (semantic.env/get-pgvector-datasource!)
|
||||
(jdbc/execute-one! (semantic.db.datasource/ensure-initialized-data-source!)
|
||||
(sql/format {:select [:*]
|
||||
:from [:index_control]}))
|
||||
(qualify :index_control [:active_id
|
||||
@@ -142,7 +143,7 @@
|
||||
:version]))))
|
||||
(testing "metadata table has expected columns"
|
||||
(is (map-contains-keys?
|
||||
(jdbc/execute-one! (semantic.env/get-pgvector-datasource!)
|
||||
(jdbc/execute-one! semantic.tu/db
|
||||
(sql/format {:select [:*]
|
||||
:from [:index_metadata]}))
|
||||
(qualify :index_metadata [:id
|
||||
@@ -189,7 +190,7 @@
|
||||
"text_search_with_native_query_vector"
|
||||
"verified"
|
||||
"view_count"}
|
||||
(->> (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(->> (jdbc/execute! semantic.tu/db
|
||||
(sql/format {:select [:column_name]
|
||||
:from [:information_schema.columns]
|
||||
:where [[:= :table_name [:inline index-table]]]}))
|
||||
@@ -197,7 +198,7 @@
|
||||
set)))))
|
||||
(testing "index table has expected columns"
|
||||
(is (= ["document" "document_hash" "gated_at" "id" "model" "model_id" "updated_at"]
|
||||
(->> (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(->> (jdbc/execute! semantic.tu/db
|
||||
(sql/format {:select [:column_name]
|
||||
:from [:information_schema.columns]
|
||||
:where [[:= :table_name [:inline "index_gate"]]]}))
|
||||
@@ -218,7 +219,7 @@
|
||||
|
||||
(deftest dynamic-schema-migration-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db-defaults!
|
||||
(semantic.tu/with-test-db! semantic.tu/default-test-db
|
||||
(with-redefs [semantic.pgvector-api/index-documents! (constantly nil)]
|
||||
(semantic.core/init! (semantic.tu/mock-documents) nil)
|
||||
;; add column to index table
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.test :refer :all]
|
||||
[honey.sql :as sql]
|
||||
[metabase-enterprise.semantic-search.dlq :as semantic.dlq]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.index-metadata :as semantic.index-metadata]
|
||||
[metabase-enterprise.semantic-search.indexer :as semantic.indexer]
|
||||
@@ -22,11 +21,12 @@
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
(use-fixtures :each #'semantic.tu/ensure-no-migration-table-fixture)
|
||||
|
||||
;; NOTE: isolation tests are absent, in prod there is only one index-metadata
|
||||
|
||||
(deftest init-semantic-search!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model1 semantic.tu/mock-embedding-model
|
||||
model2 (assoc semantic.tu/mock-embedding-model :model-name "embed-harder")
|
||||
@@ -69,7 +69,7 @@
|
||||
(semantic.index-metadata/find-compatible-index! pgvector index-metadata model2))))))))))
|
||||
|
||||
(deftest init-recreates-missing-index-table-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model1 semantic.tu/mock-embedding-model
|
||||
sut* semantic.pgvector-api/init-semantic-search!
|
||||
@@ -98,7 +98,7 @@
|
||||
(is (thrown? Exception (apply sut args)))))
|
||||
|
||||
(deftest index-documents!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model1 semantic.tu/mock-embedding-model
|
||||
model2 (assoc semantic.tu/mock-embedding-model :model-name "embedagain")
|
||||
@@ -125,7 +125,7 @@
|
||||
@calls))))))))))
|
||||
|
||||
(deftest delete-documents!-test
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
dash "dashboard"
|
||||
card "card"
|
||||
@@ -166,7 +166,7 @@
|
||||
(deftest query-test
|
||||
(semantic.tu/with-indexable-documents!
|
||||
(mt/with-dynamic-fn-redefs [semantic.index/model-table-suffix semantic.tu/mock-table-suffix]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model1 semantic.tu/mock-embedding-model
|
||||
model2 (assoc semantic.tu/mock-embedding-model :model-name "judge-embedd")
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
(deftest e2e-index-a-sample-db-with-gate-test
|
||||
(let [docs (mt/dataset test-data (vec (search.ingestion/searchable-documents)))
|
||||
pgvector (semantic.env/get-pgvector-datasource!)
|
||||
pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
embedding-model semantic.tu/mock-embedding-model
|
||||
open-job-thread (fn [& args]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns metabase-enterprise.semantic-search.query-test
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.semantic-search.db.datasource :as semantic.db.datasource]
|
||||
[metabase-enterprise.semantic-search.index :as semantic.index]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase.permissions.models.data-permissions :as data-perms]
|
||||
@@ -11,11 +12,15 @@
|
||||
|
||||
(use-fixtures :once #'semantic.tu/once-fixture)
|
||||
|
||||
(deftest database-initialised-test
|
||||
(is (some? @semantic.db.datasource/data-source))
|
||||
(is (= {:test 1} (semantic.db.datasource/test-connection!))))
|
||||
|
||||
(deftest basic-query-test
|
||||
(testing "Simple queries with no filters"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(semantic.tu/with-only-semantic-weights
|
||||
(testing "Dog-related query finds dog content"
|
||||
(let [results (-> (semantic.tu/query-index {:search-string "puppy"})
|
||||
@@ -39,7 +44,7 @@
|
||||
(testing "Tests for :search-native-query"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(semantic.tu/with-only-semantic-weights
|
||||
(let [not-top-10? #(or (nil? %) (< 10 %))]
|
||||
(doseq [{:keys [name desc search-string search-native-query? expected-index?]}
|
||||
@@ -108,7 +113,7 @@
|
||||
(testing "Filter results by model type"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(testing "Filter for cards only"
|
||||
(let [card-results (semantic.tu/query-index {:search-string "avian" :models ["card"]})
|
||||
filtered-results (semantic.tu/filter-for-mock-embeddings card-results)]
|
||||
@@ -131,7 +136,7 @@
|
||||
(testing "Filter results by archived status"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(testing "Include only non-archived items"
|
||||
(let [active-results (semantic.tu/query-index {:search-string "feline" :archived? false})]
|
||||
(is (every? #(not (:archived %)) active-results))))
|
||||
@@ -151,7 +156,7 @@
|
||||
(testing "Filter results by creator"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(testing "Filter by single creator"
|
||||
(let [crowberto-results (semantic.tu/query-index {:search-string "aquatic" :created-by [(mt/user->id :crowberto)]})
|
||||
filtered-results (semantic.tu/filter-for-mock-embeddings crowberto-results)]
|
||||
@@ -168,7 +173,7 @@
|
||||
(testing "Filter results by verified status"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(testing "Include only verified items"
|
||||
(let [verified-results (semantic.tu/query-index {:search-string "puppy" :verified true})
|
||||
filtered-results (semantic.tu/filter-for-mock-embeddings verified-results)]
|
||||
@@ -184,7 +189,7 @@
|
||||
(testing "Complex filters with multiple criteria"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(testing "Non-archived cards by specific creator"
|
||||
(let [results (semantic.tu/query-index {:search-string "equine"
|
||||
:models ["card"]
|
||||
@@ -218,7 +223,7 @@
|
||||
|
||||
(deftest permissions-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(let [monsters-table (t2/select-one-pk :model/Table :name "Monsters Table")
|
||||
q (fn [model s] (map :name (semantic.tu/query-index {:search-string s, :models [model]})))
|
||||
all-users (perms-group/all-users)
|
||||
@@ -254,7 +259,7 @@
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/as-admin
|
||||
(binding [semantic.index/*batch-size* 1]
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(semantic.tu/with-index!
|
||||
(semantic.tu/with-only-semantic-weights
|
||||
(testing "Horse-related query finds horse content"
|
||||
(let [results (-> (semantic.tu/query-index {:search-string "marine mammal"})
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[honey.sql :as sql]
|
||||
[java-time.api :as t]
|
||||
[metabase-enterprise.semantic-search.core :as semantic.core]
|
||||
[metabase-enterprise.semantic-search.env :as semantic.env]
|
||||
[metabase-enterprise.semantic-search.repair :as semantic.repair]
|
||||
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
|
||||
[metabase-enterprise.semantic-search.util :as semantic.util]
|
||||
@@ -58,9 +57,9 @@
|
||||
(deftest repair-index-integration-test
|
||||
(testing "repair-index! properly handles document additions and deletions via gate table"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
index-metadata (semantic.env/get-index-metadata)
|
||||
(semantic.tu/with-index!
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata semantic.tu/mock-index-metadata
|
||||
gate-table (:gate-table-name index-metadata)
|
||||
_ (clear-gate-table! pgvector gate-table)
|
||||
initial-docs [(create-test-document "card" 1 "Dog Training Guide")
|
||||
@@ -92,8 +91,8 @@
|
||||
(deftest repair-table-cleanup-test
|
||||
(testing "The repair table gets cleaned up properly at the end of a repair-index! job"
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(semantic.tu/with-test-db! {:mode :mock-indexed}
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(semantic.tu/with-index!
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata semantic.tu/mock-index-metadata
|
||||
gate-table (:gate-table-name index-metadata)
|
||||
initial-docs [(create-test-document "card" 6 "Dog Training Guide")]]
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"Populate the index with the given documents."
|
||||
{:style/indent :defn}
|
||||
[documents & body]
|
||||
`(semantic.tu/with-test-db! {:mode :mock-initialized}
|
||||
`(with-open [_# (semantic.tu/open-temp-index!)]
|
||||
(semantic.tu/upsert-index! (map add-doc-defaults ~documents))
|
||||
~@body))
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
(deftest stale-index-cleanup-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
retention-hours 24
|
||||
old-time (t/minus (t/offset-date-time) (t/hours (inc retention-hours)))
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
(deftest tombstone-cleanup-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
retention-hours (semantic.settings/tombstone-retention-hours)
|
||||
old-time (t/minus (t/offset-date-time) (t/hours (inc retention-hours)))
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
(deftest repair-table-cleanup-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
retention-hours 2
|
||||
old-time (t/minus (t/instant) (t/hours (inc retention-hours))) ; 3 hours ago
|
||||
recent-time (t/minus (t/instant) (t/hours 1))] ; 1 hour ago
|
||||
|
||||
@@ -67,10 +67,11 @@
|
||||
(deftest metric-collector-test
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(mt/with-prometheus-system! [_ system]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
(let [pgvector semantic.tu/db
|
||||
index-metadata (semantic.tu/unique-index-metadata)
|
||||
model semantic.tu/mock-embedding-model]
|
||||
(with-redefs [semantic.env/get-index-metadata (fn [] index-metadata)
|
||||
(with-redefs [semantic.env/get-pgvector-datasource! (fn [] pgvector)
|
||||
semantic.env/get-index-metadata (fn [] index-metadata)
|
||||
semantic.env/get-configured-embedding-model (fn [] model)]
|
||||
(testing "Missing tables are handled gracefully"
|
||||
(let [result (try
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[metabase.util.json :as json]
|
||||
[metabase.util.log :as log]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.protocols :as jdbc.protocols]
|
||||
[next.jdbc.result-set :as jdbc.rs])
|
||||
(:import
|
||||
(clojure.lang IDeref)
|
||||
@@ -31,11 +32,7 @@
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
;; Purpose of this fixure is to block running tests if db-url is not set. That's true for enterprise app-db tests in CI.
|
||||
(defn once-fixture
|
||||
[f]
|
||||
(when semantic.db.datasource/db-url
|
||||
(f)))
|
||||
;; If I won't find any use for following muted code in follow-up tasks I'll delete it -- lbrdnk
|
||||
|
||||
(def default-test-db "my_test_db")
|
||||
|
||||
@@ -64,104 +61,27 @@
|
||||
[db-name & body]
|
||||
`(do-with-temp-datasource! ~db-name (fn [] ~@body)))
|
||||
|
||||
(defmulti do-with-setup-test-db!
|
||||
"Setup pgvector database for tests."
|
||||
{:arglists '([mode thunk])}
|
||||
(fn [mode _thunk] mode))
|
||||
|
||||
(defmethod do-with-setup-test-db! :blank
|
||||
[_mode thunk]
|
||||
(thunk))
|
||||
|
||||
(declare mock-embedding-model
|
||||
mock-index-metadata
|
||||
mock-table-suffix)
|
||||
|
||||
(defmethod do-with-setup-test-db! :mock-initialized
|
||||
[_mode thunk]
|
||||
(with-redefs [semantic.embedding/get-configured-model (fn [] mock-embedding-model)
|
||||
semantic.index-metadata/default-index-metadata mock-index-metadata
|
||||
semantic.index/model-table-suffix mock-table-suffix]
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
index-metadata (semantic.env/get-index-metadata)
|
||||
embedding-model (semantic.env/get-configured-embedding-model)]
|
||||
(semantic.pgvector-api/init-semantic-search! pgvector index-metadata embedding-model)
|
||||
(thunk))))
|
||||
|
||||
;; TODO: declare with macro (the do- less version) throws weird errors -- investigate!
|
||||
(declare do-with-indexable-documents!)
|
||||
|
||||
(defmethod do-with-setup-test-db! :mock-gated
|
||||
[mode thunk]
|
||||
((get-method do-with-setup-test-db! :mock-initialized)
|
||||
mode
|
||||
(fn []
|
||||
(do-with-indexable-documents!
|
||||
(fn []
|
||||
(let [pgvector (semantic.env/get-pgvector-datasource!)
|
||||
index-metadata (semantic.env/get-index-metadata)]
|
||||
(semantic.pgvector-api/gate-updates! pgvector
|
||||
index-metadata
|
||||
(search.ingestion/searchable-documents))
|
||||
(thunk)))))))
|
||||
|
||||
(declare index-all!)
|
||||
|
||||
(defmethod do-with-setup-test-db! :mock-indexed
|
||||
[mode thunk]
|
||||
((get-method do-with-setup-test-db! :mock-gated)
|
||||
mode
|
||||
(fn []
|
||||
(index-all!)
|
||||
(thunk))))
|
||||
|
||||
;; Reminder: this can be adjusted so (1) each database is unique and (2) redefs are thread local (latter is not simple
|
||||
;; but possible I believe), so we can take advantage of parallel tests.
|
||||
(defn do-with-test-db!
|
||||
"Impl [[with-test-db]]"
|
||||
[{:keys [dbname mode cleanup]
|
||||
:or {dbname default-test-db
|
||||
mode :blank
|
||||
cleanup :before}
|
||||
:as _opts}
|
||||
thunk]
|
||||
[db-name thunk]
|
||||
(with-temp-datasource! "postgres"
|
||||
(try
|
||||
(when (#{:before :both} cleanup)
|
||||
(jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
[(str "DROP DATABASE IF EXISTS " dbname " (FORCE)")]))
|
||||
(log/debugf "Creating database %s" dbname)
|
||||
(jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
[(str "CREATE DATABASE " dbname)])
|
||||
(log/debugf "Created test pgvector database %s" dbname)
|
||||
(jdbc/execute! (semantic.db.datasource/ensure-initialized-data-source!)
|
||||
[(str "DROP DATABASE IF EXISTS " db-name " (FORCE)")])
|
||||
(log/fatal "creating database")
|
||||
(jdbc/execute! (semantic.db.datasource/ensure-initialized-data-source!)
|
||||
[(str "CREATE DATABASE " db-name)])
|
||||
(log/fatal "created database")
|
||||
(catch java.sql.SQLException e
|
||||
(log/debugf "Creation of test pgvector database %s failed" dbname)
|
||||
(log/fatal "creation failed")
|
||||
(throw e))))
|
||||
(with-temp-datasource! db-name
|
||||
(thunk)))
|
||||
|
||||
(with-temp-datasource! dbname
|
||||
(do-with-setup-test-db! mode thunk))
|
||||
|
||||
(when (#{:after :both} cleanup)
|
||||
(with-temp-datasource! "postgres"
|
||||
(try
|
||||
(jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
[(str "DROP DATABASE IF EXISTS " dbname " (FORCE)")])
|
||||
(catch java.sql.SQLException e
|
||||
(log/debugf "Test pgvector database teardown %s failed" dbname)
|
||||
(throw e))))))
|
||||
|
||||
;; TODO: When we are parallelizing tests, we'd have to make
|
||||
;; - `with-test-db!` use thread-safe version of with-redefs or similar,
|
||||
;; - instead of `default-test-db` we could make unique database per test by means of eg. some counter.
|
||||
(defmacro with-test-db!
|
||||
"Drop, create database dbname on pgvector and redefine datasource accordingly. Not thread safe."
|
||||
[opts & body]
|
||||
`(do-with-test-db! ~opts (fn [] ~@body)))
|
||||
|
||||
(defmacro with-test-db-defaults!
|
||||
"Tiny wrapper to avoid at this point redundant {} arg of [[with-test-db!]]."
|
||||
[& body]
|
||||
`(with-test-db! {} ~@body))
|
||||
[db-name & body]
|
||||
`(do-with-test-db! ~db-name (fn [] ~@body)))
|
||||
|
||||
(defmacro with-weights
|
||||
"Execute `body` overriding search weights with `weight-map`."
|
||||
@@ -175,6 +95,33 @@
|
||||
`(with-weights {:rrf 1}
|
||||
~@body))
|
||||
|
||||
(def ^:private init-delay
|
||||
(delay
|
||||
(when-not @semantic.db.datasource/data-source
|
||||
(semantic.db.datasource/init-db!))))
|
||||
|
||||
(defn once-fixture [f]
|
||||
(when semantic.db.datasource/db-url
|
||||
@init-delay
|
||||
(f)))
|
||||
|
||||
(declare db)
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn ensure-no-migration-table-fixture [f]
|
||||
(semantic.db.migration/drop-migration-table! db)
|
||||
(f)
|
||||
(semantic.db.migration/drop-migration-table! db))
|
||||
|
||||
(def db
|
||||
"Proxies the semantic.db.datasource/data-source, avoids the deref and prettifies a little"
|
||||
;; proxy because semantic.db.datasource/data-source is not initialised until the fixture runs
|
||||
(reify jdbc.protocols/Sourceable
|
||||
(get-datasource [_] (jdbc.protocols/get-datasource @semantic.db.datasource/data-source))))
|
||||
|
||||
(comment
|
||||
(jdbc/execute! db ["select 1"]))
|
||||
|
||||
(def mock-embeddings
|
||||
"Static mapping from strings to (made-up) 4-dimensional embedding vectors for testing. Each pair of strings represents a
|
||||
document and a search query that should be most semantically similar to it, according to the embeddings."
|
||||
@@ -249,16 +196,14 @@
|
||||
(defmethod semantic.embedding/get-embeddings-batch "mock" [_ texts] (get-mock-embeddings-batch texts))
|
||||
(defmethod semantic.embedding/pull-model "mock" [_])
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn query-index [search-context]
|
||||
(:results (semantic.index/query-index (semantic.env/get-pgvector-datasource!) mock-index search-context)))
|
||||
(:results (semantic.index/query-index db mock-index search-context)))
|
||||
|
||||
;; TODO: this should go!!!
|
||||
(defn upsert-index! [documents & {:keys [index] :or {index mock-index} :as opts}]
|
||||
(semantic.index/upsert-index! (semantic.env/get-pgvector-datasource!) index documents opts))
|
||||
(semantic.index/upsert-index! db index documents opts))
|
||||
|
||||
(defn delete-from-index! [model ids]
|
||||
(semantic.index/delete-from-index! (semantic.env/get-pgvector-datasource!) mock-index model ids))
|
||||
(semantic.index/delete-from-index! db mock-index model ids))
|
||||
|
||||
(defn dog-training-native-query []
|
||||
(mt/native-query {:query "SELECT AVG(tricks) FROM dogs WHERE age > 7 GROUP BY breed"}))
|
||||
@@ -303,116 +248,74 @@
|
||||
|
||||
(defn open-temp-index! ^Closeable [& {:keys [index] :or {index mock-index}}]
|
||||
(closeable
|
||||
(do (semantic.index/create-index-table-if-not-exists! (semantic.env/get-pgvector-datasource!) index {:force-reset? true})
|
||||
(do (semantic.index/create-index-table-if-not-exists! db index {:force-reset? true})
|
||||
index)
|
||||
(fn cleanup-temp-index-table! [{:keys [table-name] :as index}]
|
||||
(try
|
||||
(semantic.index/drop-index-table! (semantic.env/get-pgvector-datasource!) index)
|
||||
(semantic.index/drop-index-table! db index)
|
||||
(catch Exception e
|
||||
(log/error e "Warning: failed to clean up test table" table-name))))))
|
||||
|
||||
(defn do-with-indexable-documents!
|
||||
"Wrap the thunk into with-temp, creating entities used throughout semantic search test.
|
||||
(declare cleanup-index-metadata!)
|
||||
|
||||
N.B. *disable-updates* is bound to avoid processing of those entities by logic in [[search.ingestion]] ns.
|
||||
The processing is triggered by means :hook/search-index which :model/Card (and others) derive.
|
||||
|
||||
As of 2025-09-10, processing triggered by insertion, combined with manual gating of documents that callers
|
||||
of this fn do, would result in duplicate processing and deletion of those entitities from index
|
||||
due to [[search.ingestion/bulk-ingest!]].
|
||||
|
||||
For details see the https://metaboat.slack.com/archives/C07SJT1P0ET/p1757452434713309?thread_ts=1757410361.879029&cid=C07SJT1P0ET"
|
||||
[thunk]
|
||||
;; NB: *disable-updates*
|
||||
(binding [search.ingestion/*disable-updates* true]
|
||||
(mt/dataset
|
||||
test-data
|
||||
(mt/with-temp [:model/Collection
|
||||
{col1 :id}
|
||||
{:name "Wildlife Collection" :archived false}
|
||||
|
||||
:model/Collection
|
||||
{col2 :id}
|
||||
{:name "Archived Animals" :archived true}
|
||||
|
||||
:model/Collection
|
||||
{col3 :id}
|
||||
{:name "Cryptozoology", :archived false}
|
||||
|
||||
:model/Card
|
||||
{card1 :id}
|
||||
{:name "Dog Training Guide" :collection_id col1 :creator_id (mt/user->id :crowberto)
|
||||
:archived false :query_type "native" :dataset_query (dog-training-native-query)}
|
||||
|
||||
:model/Card
|
||||
_
|
||||
{:name "Bird Watching Tips" :collection_id col1 :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Card
|
||||
_
|
||||
{:name "Cat Behavior Study" :collection_id col2 :creator_id (mt/user->id :crowberto) :archived true}
|
||||
|
||||
:model/Card
|
||||
_
|
||||
{:name "Horse Racing Analysis" :collection_id col1 :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Card
|
||||
_
|
||||
{:name "Fish Tank Setup" :collection_id col2 :creator_id (mt/user->id :crowberto) :archived true}
|
||||
|
||||
:model/Card
|
||||
_
|
||||
{:name "Bigfoot Sightings" :collection_id col3 :creator_id (mt/user->id :crowberto) :archived false}
|
||||
|
||||
:model/ModerationReview
|
||||
_
|
||||
{:moderated_item_type "card"
|
||||
:moderated_item_id card1
|
||||
:moderator_id (mt/user->id :crowberto)
|
||||
:status "verified"
|
||||
:most_recent true}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Elephant Migration" :collection_id col1 :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Lion Pride Dynamics" :collection_id col1 :creator_id (mt/user->id :crowberto) :archived false}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Penguin Colony Study" :collection_id col2 :creator_id (mt/user->id :rasta) :archived true}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Whale Communication" :collection_id col1 :creator_id (mt/user->id :crowberto) :archived false}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Tiger Conservation" :collection_id col2 :creator_id (mt/user->id :rasta) :archived true}
|
||||
|
||||
:model/Dashboard
|
||||
_
|
||||
{:name "Loch Ness Stuff" :collection_id col3 :creator_id (mt/user->id :crowberto), :archived false}
|
||||
|
||||
:model/Database
|
||||
{db-id :id}
|
||||
{:name "Animal Database"}
|
||||
|
||||
:model/Table
|
||||
_
|
||||
{:name "Species Table", :db_id db-id}
|
||||
|
||||
:model/Table
|
||||
_
|
||||
{:name "Monsters Table", :db_id db-id, :active true}]
|
||||
(thunk)))))
|
||||
(defn open-temp-index-and-metadata! ^Closeable []
|
||||
(closeable
|
||||
(do (cleanup-index-metadata! db mock-index-metadata)
|
||||
(semantic.pgvector-api/init-semantic-search! db mock-index-metadata mock-embedding-model)
|
||||
mock-index-metadata)
|
||||
(fn cleanup-temp-index-and-metadata! [index-metadata]
|
||||
(try
|
||||
(cleanup-index-metadata! db index-metadata)
|
||||
(catch Exception e
|
||||
(log/error e "Warning: failed to clean up index and metadata" index-metadata))))))
|
||||
|
||||
(defmacro with-indexable-documents!
|
||||
"Wrapper for [[do-with-indexable-documents!]]."
|
||||
"Add a collection of test documents to that can be indexed to the appdb."
|
||||
[& body]
|
||||
`(do-with-indexable-documents! (fn [] ~@body)))
|
||||
`(mt/dataset ~(symbol "test-data")
|
||||
(mt/with-temp [:model/Collection {col1# :id} {:name "Wildlife Collection" :archived false}
|
||||
|
||||
:model/Collection {col2# :id} {:name "Archived Animals" :archived true}
|
||||
|
||||
:model/Collection {col3# :id} {:name "Cryptozoology", :archived false}
|
||||
|
||||
:model/Card {card1# :id} {:name "Dog Training Guide" :collection_id col1# :creator_id (mt/user->id :crowberto) :archived false
|
||||
:query_type "native" :dataset_query (dog-training-native-query)}
|
||||
|
||||
:model/Card {} {:name "Bird Watching Tips" :collection_id col1# :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Card {} {:name "Cat Behavior Study" :collection_id col2# :creator_id (mt/user->id :crowberto) :archived true}
|
||||
|
||||
:model/Card {} {:name "Horse Racing Analysis" :collection_id col1# :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Card {} {:name "Fish Tank Setup" :collection_id col2# :creator_id (mt/user->id :crowberto) :archived true}
|
||||
|
||||
:model/Card {} {:name "Bigfoot Sightings" :collection_id col3# :creator_id (mt/user->id :crowberto), :archived false}
|
||||
|
||||
:model/ModerationReview {} {:moderated_item_type "card"
|
||||
:moderated_item_id card1#
|
||||
:moderator_id (mt/user->id :crowberto)
|
||||
:status "verified"
|
||||
:most_recent true}
|
||||
|
||||
:model/Dashboard {} {:name "Elephant Migration" :collection_id col1# :creator_id (mt/user->id :rasta) :archived false}
|
||||
|
||||
:model/Dashboard {} {:name "Lion Pride Dynamics" :collection_id col1# :creator_id (mt/user->id :crowberto) :archived false}
|
||||
|
||||
:model/Dashboard {} {:name "Penguin Colony Study" :collection_id col2# :creator_id (mt/user->id :rasta) :archived true}
|
||||
|
||||
:model/Dashboard {} {:name "Whale Communication" :collection_id col1# :creator_id (mt/user->id :crowberto) :archived false}
|
||||
|
||||
:model/Dashboard {} {:name "Tiger Conservation" :collection_id col2# :creator_id (mt/user->id :rasta) :archived true}
|
||||
|
||||
:model/Dashboard {} {:name "Loch Ness Stuff" :collection_id col3# :creator_id (mt/user->id :crowberto), :archived false}
|
||||
|
||||
:model/Database {db-id# :id} {:name "Animal Database"}
|
||||
|
||||
:model/Table {} {:name "Species Table", :db_id db-id#}
|
||||
|
||||
:model/Table {} {:name "Monsters Table", :db_id db-id#, :active true}]
|
||||
~@body)))
|
||||
|
||||
(defn index-all!
|
||||
"Run indexer synchonously until we've exhausted polling all documents"
|
||||
@@ -420,25 +323,44 @@
|
||||
(let [metadata-row {:indexer_last_poll Instant/EPOCH
|
||||
:indexer_last_seen Instant/EPOCH}
|
||||
indexing-state (semantic.indexer/init-indexing-state metadata-row)
|
||||
pgvector (semantic.env/get-pgvector-datasource!)
|
||||
step (fn [] (semantic.indexer/indexing-step pgvector mock-index-metadata mock-index indexing-state))]
|
||||
step (fn [] (semantic.indexer/indexing-step db mock-index-metadata mock-index indexing-state))]
|
||||
(while (do (step) (pos? (:last-novel-count @indexing-state))))))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defmacro blocking-index!
|
||||
"Execute body ensuring [[index-all!]] is invoked at the end"
|
||||
[& body]
|
||||
`(let [ret# (do ~@body)]
|
||||
(index-all!)
|
||||
ret#))
|
||||
|
||||
(defmacro with-index!
|
||||
"Ensure a clean, small index for testing populated with a few collections, cards, and dashboards."
|
||||
[& body]
|
||||
`(with-indexable-documents!
|
||||
(with-redefs [semantic.embedding/get-configured-model (fn [] mock-embedding-model)
|
||||
semantic.index-metadata/default-index-metadata mock-index-metadata
|
||||
semantic.index/model-table-suffix mock-table-suffix]
|
||||
(with-open [_# (open-temp-index-and-metadata!)]
|
||||
(binding [search.ingestion/*force-sync* true]
|
||||
(blocking-index!
|
||||
(semantic.pgvector-api/gate-updates! (semantic.env/get-pgvector-datasource!)
|
||||
mock-index-metadata
|
||||
(search.ingestion/searchable-documents)))
|
||||
~@body)))))
|
||||
|
||||
(defn table-exists-in-db?
|
||||
"Check if a table actually exists in the database"
|
||||
[table-name]
|
||||
(when table-name
|
||||
(try
|
||||
(semantic.util/table-exists? (semantic.env/get-pgvector-datasource!) (name table-name))
|
||||
(semantic.util/table-exists? db (name table-name))
|
||||
(catch Exception _ false))))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn table-has-index?
|
||||
[table-name index-name]
|
||||
(when table-name
|
||||
(try
|
||||
(let [result (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(let [result (jdbc/execute! db
|
||||
["SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = ? AND indexname = ?)"
|
||||
(name table-name)
|
||||
(name index-name)])]
|
||||
@@ -513,7 +435,7 @@
|
||||
(defn index-count
|
||||
"Count the number of documents in the index."
|
||||
[index]
|
||||
(let [result (jdbc/execute-one! (semantic.env/get-pgvector-datasource!)
|
||||
(let [result (jdbc/execute-one! db
|
||||
(-> (sql.helpers/select [:%count.* :count])
|
||||
(sql.helpers/from (keyword (:table-name index)))
|
||||
semantic.index/sql-format-quoted)
|
||||
@@ -525,18 +447,17 @@
|
||||
"Query the full index table and return all documents with decoded embeddings.
|
||||
Not used in tests, but useful for debugging."
|
||||
[]
|
||||
(->> (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(->> (jdbc/execute! db
|
||||
(-> (sql.helpers/select :model :model_id :content :creator_id :embedding)
|
||||
(sql.helpers/from (keyword (:table-name mock-index)))
|
||||
semantic.index/sql-format-quoted)
|
||||
{:builder-fn jdbc.rs/as-unqualified-lower-maps})
|
||||
(mapv decode-embedding)))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn query-embeddings
|
||||
"Query the `mock-index` table and return the decoded `:embedding`s for the given `model`"
|
||||
[{:keys [model model_id]}]
|
||||
(->> (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(->> (jdbc/execute! db
|
||||
(-> (sql.helpers/select :model :model_id :content :creator_id :embedding)
|
||||
(sql.helpers/from (keyword (:table-name mock-index)))
|
||||
(sql.helpers/where :and
|
||||
@@ -546,11 +467,10 @@
|
||||
{:builder-fn jdbc.rs/as-unqualified-lower-maps})
|
||||
(mapv decode-embedding)))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn query-tsvectors
|
||||
"Query the `mock-index` table and return the unwrapped tsvector columns for the given `model`"
|
||||
[{:keys [model model_id]}]
|
||||
(->> (jdbc/execute! (semantic.env/get-pgvector-datasource!)
|
||||
(->> (jdbc/execute! db
|
||||
(-> (sql.helpers/select :model :model_id :content :creator_id
|
||||
:text_search_vector :text_search_with_native_query_vector)
|
||||
(sql.helpers/from (keyword (:table-name mock-index)))
|
||||
@@ -573,11 +493,10 @@
|
||||
(query-embeddings {:model "dashboard"
|
||||
:model_id "456"})))))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/test-helpers-use-non-thread-safe-functions]}
|
||||
(defn check-index-has-no-mock-docs []
|
||||
(let [{:keys [table-name]} mock-index
|
||||
table-exists-sql "select exists(select * from information_schema.tables where table_name = ?) table_exists"
|
||||
[{:keys [table_exists]}] (jdbc/execute! (semantic.env/get-pgvector-datasource!) [table-exists-sql table-name])]
|
||||
[{:keys [table_exists]}] (jdbc/execute! db [table-exists-sql table-name])]
|
||||
(when table_exists
|
||||
(check-index-has-no-mock-card)
|
||||
(check-index-has-no-mock-dashboard))))
|
||||
|
||||
@@ -17,15 +17,14 @@
|
||||
(defn- without-init! [test-func]
|
||||
(mt/with-premium-features #{:semantic-search}
|
||||
(try
|
||||
(semantic.tu/cleanup-index-metadata! (semantic.env/get-pgvector-datasource!)
|
||||
semantic.tu/mock-index-metadata)
|
||||
(semantic.tu/cleanup-index-metadata! semantic.tu/db semantic.tu/mock-index-metadata)
|
||||
(with-redefs [semantic.db.datasource/data-source (atom nil)
|
||||
semantic.env/get-index-metadata (constantly semantic.tu/mock-index-metadata)
|
||||
semantic.env/get-configured-embedding-model (constantly semantic.tu/mock-embedding-model)
|
||||
semantic.index/model-table-suffix semantic.tu/mock-table-suffix]
|
||||
(test-func))
|
||||
(finally
|
||||
(semantic.tu/cleanup-index-metadata! (semantic.env/get-pgvector-datasource!) semantic.tu/mock-index-metadata)))))
|
||||
(semantic.tu/cleanup-index-metadata! semantic.tu/db semantic.tu/mock-index-metadata)))))
|
||||
|
||||
(deftest update-index!-without-init!-test
|
||||
(without-init!
|
||||
|
||||
@@ -360,70 +360,6 @@
|
||||
:timeout 30000})))
|
||||
(is (true? (t2/exists? :model/Table :name (:name target))))))))))))
|
||||
|
||||
(deftest transform-function-with-library-test
|
||||
(testing "transform function can use libraries"
|
||||
(mt/test-drivers #{:postgres}
|
||||
(mt/with-temp [:model/PythonLibrary _ {:path "circle"
|
||||
:source "import math\n\ndef calculate_circle_area(radius):\n return math.pi * radius ** 2"}
|
||||
:model/PythonLibrary _ {:path "utils"
|
||||
:source "def format_currency(amount):\n return f\"${amount:,.2f}\""}]
|
||||
(let [transform-code (str "import pandas as pd\n"
|
||||
"from circle import calculate_circle_area\n"
|
||||
"from utils import format_currency\n"
|
||||
"\n"
|
||||
"def transform():\n"
|
||||
" data = [\n"
|
||||
" {'radius': 5, 'area': calculate_circle_area(5), 'price': format_currency(78.54)},\n"
|
||||
" {'radius': 10, 'area': calculate_circle_area(10), 'price': format_currency(314.16)}\n"
|
||||
" ]\n"
|
||||
" return pd.DataFrame(data)")
|
||||
result (execute {:code transform-code})]
|
||||
(is (=? {:output #(and (str/includes? % "radius,area,price")
|
||||
(str/includes? % "5,78.5")
|
||||
(str/includes? % "$78.54")
|
||||
(str/includes? % "10,314.1")
|
||||
(str/includes? % "$314.16"))
|
||||
:stdout "Successfully saved 2 rows to S3\nSuccessfully saved output manifest with 3 fields"
|
||||
:stderr ""}
|
||||
result)))))))
|
||||
|
||||
(deftest transform-function-without-libraries-test
|
||||
(testing "transform function works when no libraries exist"
|
||||
(mt/test-drivers #{:postgres}
|
||||
(with-redefs [t2/select-fn->fn (fn [k v model]
|
||||
(when (and (= k :path)
|
||||
(= v :source)
|
||||
(= model :model/PythonLibrary))
|
||||
{}))]
|
||||
(let [transform-code (str "import pandas as pd\n"
|
||||
"\n"
|
||||
"def transform():\n"
|
||||
" return pd.DataFrame({'status': ['ok']})")
|
||||
result (execute {:code transform-code})]
|
||||
(is (=? {:output "status\nok\n"
|
||||
:stdout "Successfully saved 1 rows to CSV\nSuccessfully saved output manifest with 1 fields"
|
||||
:stderr ""}
|
||||
result)))))))
|
||||
|
||||
(deftest transform-function-library-import-error-test
|
||||
(testing "transform function handles missing library gracefully"
|
||||
(mt/test-drivers #{:postgres}
|
||||
(with-redefs [t2/select-fn->fn (fn [k v model]
|
||||
(when (and (= k :path)
|
||||
(= v :source)
|
||||
(= model :model/PythonLibrary))
|
||||
{"utils" "def helper():\n return 42"}))]
|
||||
(let [transform-code (str "import pandas as pd\n"
|
||||
"from common import some_function # This library doesn't exist\n"
|
||||
"\n"
|
||||
"def transform():\n"
|
||||
" return pd.DataFrame({'value': [some_function()]})")
|
||||
result (execute {:code transform-code})]
|
||||
(is (=? {:error "Execution failed"
|
||||
:exit-code 1
|
||||
:stderr #(str/includes? % "No module named 'common'")}
|
||||
result)))))))
|
||||
|
||||
(deftest transform-type-roundtrip-test
|
||||
(mt/test-drivers #{:postgres :h2 :mysql :bigquery-cloud-sdk :redshift :snowflake :sqlserver :clickhouse}
|
||||
(mt/with-empty-db
|
||||
|
||||
@@ -58,10 +58,6 @@ module.exports = {
|
||||
...baseRestrictredConfig,
|
||||
paths: [
|
||||
...baseRestrictredConfig.paths,
|
||||
{
|
||||
name: "zod",
|
||||
message: "Please import from `zod/mini` instead.",
|
||||
},
|
||||
{
|
||||
name: "metabase/lib/redux",
|
||||
importNames: ["useStore", "useDispatch"],
|
||||
|
||||
@@ -1,107 +1,3 @@
|
||||
# Embedding SDK package changelog
|
||||
|
||||
This changelog contains only important/breaking changes for the Embedding SDK package (`@metabase/embedding-sdk-react` npm package).
|
||||
|
||||
Read the main Embedded analytics SDK changelog [here](https://www.metabase.com/changelog/57) under the `Embedding` category.
|
||||
|
||||
## 0.57.0 and above
|
||||
|
||||
### Features
|
||||
|
||||
- the Embedding SDK now contains 2 parts:
|
||||
- the SDK package (`@metabase/embedding-sdk-react`), that is now a lightweight library that loads the main Embedding SDK code from a Metabase Instance.
|
||||
- the SDK bundle, that is released together with Metabase and is served from a Metabase Instance.
|
||||
|
||||
## Legacy changelog (before `@metabase/embedding-sdk-react@57`)
|
||||
|
||||
## [0.56.3](https://github.com/metabase/metabase/compare/embedding-sdk-0.56.2...embedding-sdk-0.56.3) (2025-08-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sdk:** Add background-light to derived colors for the new embed ([#62522](https://github.com/metabase/metabase/issues/62522)) ([#62586](https://github.com/metabase/metabase/issues/62586)) ([c906f5e](https://github.com/metabase/metabase/commit/c906f5e948c8647ac1422d0fd107eb2e35082aac))
|
||||
* **sdk:** Properly calculate the `dirty` state for the `InteractiveQuestion.SaveButton` button ([#62423](https://github.com/metabase/metabase/issues/62423)) ([#62681](https://github.com/metabase/metabase/issues/62681)) ([1e854fd](https://github.com/metabase/metabase/commit/1e854fdacd5d9be4a04bf992078e331fd446d5ac))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **sdk:** Fix and improve Metabot for SDK ([#62886](https://github.com/metabase/metabase/issues/62886)) ([46c899b](https://github.com/metabase/metabase/commit/46c899b8bbb767ee74a31b07f4a684602663ec4a))
|
||||
|
||||
|
||||
|
||||
## [0.56.2](https://github.com/metabase/metabase/compare/embedding-sdk-0.56.1-nightly...embedding-sdk-0.56.2) (2025-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sdk:** send CORS headers for error messages when embedding is disabled ([#61808](https://github.com/metabase/metabase/issues/61808)) ([#61937](https://github.com/metabase/metabase/issues/61937)) ([f22706f](https://github.com/metabase/metabase/commit/f22706f1fc6450a9e68ad17ff116e16fb68572c9))
|
||||
* **sdk:** targetCollection should take precedence over recent dashboard ([#61874](https://github.com/metabase/metabase/issues/61874)) ([#61929](https://github.com/metabase/metabase/issues/61929)) ([e911a3a](https://github.com/metabase/metabase/commit/e911a3af2b1256faef59f0fa1f13c845408388b5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **sdk:** add translation context and aria-label to embedding sdk settings ([#61856](https://github.com/metabase/metabase/issues/61856)) ([#61872](https://github.com/metabase/metabase/issues/61872)) ([6348adc](https://github.com/metabase/metabase/commit/6348adc439c3cd33702141de6eeae03a1bec6418))
|
||||
* **sdk:** hide tables by default in embedding data picker if there are more than 2 models ([#61022](https://github.com/metabase/metabase/issues/61022)) ([#61760](https://github.com/metabase/metabase/issues/61760)) ([7bb6dcf](https://github.com/metabase/metabase/commit/7bb6dcf549f1f8a002586817a9a28caa38f4f48e))
|
||||
|
||||
|
||||
|
||||
## [0.56.1-nightly](https://github.com/metabase/metabase/compare/embedding-sdk-0.55.1-nightly...embedding-sdk-0.56.1-nightly) (2025-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sdk:** clicking on "and X more" in chart legend throws error ([#57175](https://github.com/metabase/metabase/issues/57175)) ([6b427ef](https://github.com/metabase/metabase/commit/6b427efbfcf2dfc114cdc892a4bf9ec9cee3f38d))
|
||||
* **sdk:** crash when changing questionId from new to native question ([#60161](https://github.com/metabase/metabase/issues/60161)) ([ebcf446](https://github.com/metabase/metabase/commit/ebcf446ad9a186a6cefd7c901a8460a15a882aa5))
|
||||
* **sdk:** css variables leak from Mantine to the host app ([#56985](https://github.com/metabase/metabase/issues/56985)) ([2b3f683](https://github.com/metabase/metabase/commit/2b3f6833e71696827cb61a4b5a504859ce674f01))
|
||||
* **sdk:** custom GeoJSON maps does not load due to wrong domain ([#57539](https://github.com/metabase/metabase/issues/57539)) ([7f92780](https://github.com/metabase/metabase/commit/7f92780e9e1641fb79054e4f9b99c7819f5d9833))
|
||||
* **sdk:** Ensure that questions don't flicker when fetching entity IDs ([#57073](https://github.com/metabase/metabase/issues/57073)) ([af0188a](https://github.com/metabase/metabase/commit/af0188ade10002d948efe3199839cb3f000f164d))
|
||||
* **sdk:** Fix flickered "Question new not found error" ([#59696](https://github.com/metabase/metabase/issues/59696)) ([15dcbf0](https://github.com/metabase/metabase/commit/15dcbf0b7cba224a4efcd4a92c90729f0136b7da))
|
||||
* **sdk:** fix popovers can't be rendered inside modals ([#60431](https://github.com/metabase/metabase/issues/60431)) ([68549e8](https://github.com/metabase/metabase/commit/68549e8664d29114d9fdd95fff0877c7e44e23de))
|
||||
* **sdk:** fix tooltip crash by migrating away from renderToString ([#58394](https://github.com/metabase/metabase/issues/58394)) ([d40df5e](https://github.com/metabase/metabase/commit/d40df5ea08082a1ea22668f7f6363ef39e43f2e2))
|
||||
* **sdk:** hide the "back to previous results" button in questions ([#61018](https://github.com/metabase/metabase/issues/61018)) ([fd5a433](https://github.com/metabase/metabase/commit/fd5a43326523dd7638960d21711211b8c5b6e87d))
|
||||
* **sdk:** make isEmbeddingSdk consistent on new iframe embed using singletons ([#60349](https://github.com/metabase/metabase/issues/60349)) ([efef12d](https://github.com/metabase/metabase/commit/efef12dbb0f19d52b8f72a1eb1b42bc335a7424d))
|
||||
* **sdk:** manual fixes for module level ttag calls causing broken translations ([#57391](https://github.com/metabase/metabase/issues/57391)) ([2adc027](https://github.com/metabase/metabase/commit/2adc0271787a0a07b1ec5e704198432166071ac3))
|
||||
* **sdk:** Move @cypress/react as dev dependency for SDK ([#57962](https://github.com/metabase/metabase/issues/57962)) ([424be85](https://github.com/metabase/metabase/commit/424be8573c5cabae040c8aa9c940386ca56921bd))
|
||||
* **sdk:** omit jwt token response from error messages ([#59108](https://github.com/metabase/metabase/issues/59108)) ([12351f9](https://github.com/metabase/metabase/commit/12351f93d8bf9a1e2902ea2c018758a98817351c))
|
||||
* **sdk:** questions shows an error while loading on strict mode ([#58183](https://github.com/metabase/metabase/issues/58183)) ([f5f8a13](https://github.com/metabase/metabase/commit/f5f8a13af95892795446ee7ce1d8b65a8f8cb89a))
|
||||
* **sdk:** Remove fullscreen and refresh buttons from SDK dashboard as they don't make sense ([#60198](https://github.com/metabase/metabase/issues/60198)) ([50ba791](https://github.com/metabase/metabase/commit/50ba7910a0e9940721f24f00eef88d55867bb4b0))
|
||||
* **sdk:** Remove unhelpful error `Error: null` ([#59168](https://github.com/metabase/metabase/issues/59168)) ([30405be](https://github.com/metabase/metabase/commit/30405beba7f2501a5cac387ce4fc4744a17006fc))
|
||||
* **sdk:** Render ChartSettingColorPicker color picker without a portal ([#59821](https://github.com/metabase/metabase/issues/59821)) ([027dcf9](https://github.com/metabase/metabase/commit/027dcf907185dc320ff3c7812bcc62e319b42bc1))
|
||||
* **sdk:** SAML + JWT + New Auth Flow ([#56869](https://github.com/metabase/metabase/issues/56869)) ([98e1166](https://github.com/metabase/metabase/commit/98e1166ea0492732c6f4c76e12d83c1329a740bb))
|
||||
* **sdk:** switching from existing to new question should keep query builder open ([#60077](https://github.com/metabase/metabase/issues/60077)) ([a41e383](https://github.com/metabase/metabase/commit/a41e383e0ecc3ff93d43c73d48f2087023abe9ca))
|
||||
* **sdk:** table cell color is always white on the sdk ([#57770](https://github.com/metabase/metabase/issues/57770)) ([fb06c2b](https://github.com/metabase/metabase/commit/fb06c2b571ab2dc45b73be217ea8bae16d9fed1f))
|
||||
* **sdk:** updating collection's entity id crashes InteractiveQuestion and CollectionBrowser ([#57529](https://github.com/metabase/metabase/issues/57529)) ([92fac6c](https://github.com/metabase/metabase/commit/92fac6cabb08e59ef39a80a5c589df458c749cb5))
|
||||
* **sdk:** use background color in sankey chart text outline ([#57135](https://github.com/metabase/metabase/issues/57135)) ([dd76006](https://github.com/metabase/metabase/commit/dd76006c42a5220db0097bdc583ffd110cf174ae))
|
||||
* **sdk:** use instance locale if no locale is passed ([#56683](https://github.com/metabase/metabase/issues/56683)) ([88a8827](https://github.com/metabase/metabase/commit/88a88270be99c24095deb6bbf3bf6eb61ca73e1a))
|
||||
* **sdk:** use supbath for `/auth/sso` http call ([#60488](https://github.com/metabase/metabase/issues/60488)) ([dccd4f9](https://github.com/metabase/metabase/commit/dccd4f970ec9685c0c2647c6ebedfa99ae5badf4))
|
||||
* **sdk:** wait for locales to be loaded before rendering SDK components ([#58566](https://github.com/metabase/metabase/issues/58566)) ([3bcdb8a](https://github.com/metabase/metabase/commit/3bcdb8ae5d81ad783d2ca8ca577d107d5fbde18b))
|
||||
* **sdk:** when starting from an entity id dashboard, use the numeric id ([#61188](https://github.com/metabase/metabase/issues/61188)) ([c4aca65](https://github.com/metabase/metabase/commit/c4aca6545e1542b3aea7fca55c4ba452600844fe))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **sdk:** ability to specify preferred authentication method ([#58641](https://github.com/metabase/metabase/issues/58641)) ([cdbd112](https://github.com/metabase/metabase/commit/cdbd112bffb3fcdd060ea3900b4019de800828bf))
|
||||
* **sdk:** Add Visualization Button, hook, and onRun event ([#58080](https://github.com/metabase/metabase/issues/58080)) ([61bf62e](https://github.com/metabase/metabase/commit/61bf62e6b930552c005fcabce056875042f4bbc8))
|
||||
* **sdk:** Add withDownloads to the static question ([#60976](https://github.com/metabase/metabase/issues/60976)) ([2ff8906](https://github.com/metabase/metabase/commit/2ff890692e437dda1b42c05b1f3dd3dd8ec16499))
|
||||
* **sdk:** Automatically set a sensible display to interactive questions ([#56584](https://github.com/metabase/metabase/issues/56584)) ([#56667](https://github.com/metabase/metabase/issues/56667)) ([b7a74d0](https://github.com/metabase/metabase/commit/b7a74d02c7082b1dd180d059c255e2ebb1d0cbaf))
|
||||
* **sdk:** Create new dashboard question from `EditableDashboard` ([#60920](https://github.com/metabase/metabase/issues/60920)) ([1bf726b](https://github.com/metabase/metabase/commit/1bf726bddc9e9248fa4f66b3007c7fa5edf53674))
|
||||
* **sdk:** Do not add unused Empty state SVG images to the SDK bundle ([#58013](https://github.com/metabase/metabase/issues/58013)) ([801e9b6](https://github.com/metabase/metabase/commit/801e9b6e6b82d237abaace2669a0c3e462174695))
|
||||
* **sdk:** Do not bundle main-app plugins to the SDK bundle ([#58028](https://github.com/metabase/metabase/issues/58028)) ([a664244](https://github.com/metabase/metabase/commit/a6642444d60be7676327612c43ff210087685ada))
|
||||
* **sdk:** do not show "New Question" as fallback for new question ([#61015](https://github.com/metabase/metabase/issues/61015)) ([aa7ced1](https://github.com/metabase/metabase/commit/aa7ced174f847b9914c0b7778e344fd20d0d30ed))
|
||||
* **sdk:** download button is now in an always visible ... menu ([#61556](https://github.com/metabase/metabase/issues/61556)) ([f517801](https://github.com/metabase/metabase/commit/f51780142f21c1aaa9b96fa37bfc5f9d168839f3))
|
||||
* **sdk:** Export more public SDK types ([#56838](https://github.com/metabase/metabase/issues/56838)) ([764b236](https://github.com/metabase/metabase/commit/764b2366015df4594d3e7745234fdef126abc3b5))
|
||||
* **sdk:** Export public plugin and icon types ([#56851](https://github.com/metabase/metabase/issues/56851)) ([fe1df8d](https://github.com/metabase/metabase/commit/fe1df8df8839eca275dd1dd2c02d1a8872278643))
|
||||
* **sdk:** expose the title prop in StaticQuestion ([#61001](https://github.com/metabase/metabase/issues/61001)) ([206f7ea](https://github.com/metabase/metabase/commit/206f7eaccbba6278a050f386382060c1ca177e51))
|
||||
* **sdk:** Reduce bundle size by avoiding the usage of `jsrsasign` dependency ([#58009](https://github.com/metabase/metabase/issues/58009)) ([9fcd2fe](https://github.com/metabase/metabase/commit/9fcd2fef3412c13d19d698154a86cb315e757288))
|
||||
* **sdk:** remove footer from dashboards ([#57779](https://github.com/metabase/metabase/issues/57779)) ([7fff64e](https://github.com/metabase/metabase/commit/7fff64eaf730631282bc7727a13b53ee5a5a6491))
|
||||
* **sdk:** remove question and metric from entity type filters in the sdk's data picker ([#58339](https://github.com/metabase/metabase/issues/58339)) ([608bb8c](https://github.com/metabase/metabase/commit/608bb8c1c46a39ac50dae3fe80bd00496c4755e0))
|
||||
* **sdk:** theme-dependent default question toolbar colors ([#57747](https://github.com/metabase/metabase/issues/57747)) ([d52a4e3](https://github.com/metabase/metabase/commit/d52a4e35da9765cbc4218c4fe9091df1df4daba0))
|
||||
* **sdk:** use entity ids directly in questions and dashboards ([#60555](https://github.com/metabase/metabase/issues/60555)) ([9258906](https://github.com/metabase/metabase/commit/92589063d615280be84dd4f1fe4dd5df981143a9))
|
||||
* **sdk:** Use public types for Dashboard/Collection/User and other entities ([#56875](https://github.com/metabase/metabase/issues/56875)) ([9bb5e3e](https://github.com/metabase/metabase/commit/9bb5e3ec502f6a2bb80ad0be0c01124668abf387))
|
||||
* **sdk:** warn when using dev instance and change banner design ([#57746](https://github.com/metabase/metabase/issues/57746)) ([32b3f46](https://github.com/metabase/metabase/commit/32b3f46331b4fbb23c8aebd8cd887297be5a02b4))
|
||||
|
||||
|
||||
|
||||
## [0.55.5-metabot](https://github.com/metabase/metabase/compare/embedding-sdk-0.55.4-metabot...embedding-sdk-0.55.5-metabot) (2025-05-07)
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { CollectionBrowserProps } from "./CollectionBrowser";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
EmptyContentComponent: optional(any()),
|
||||
className: optional(any()),
|
||||
collectionId: optional(any()),
|
||||
onClick: optional(any()),
|
||||
pageSize: optional(any()),
|
||||
style: optional(any()),
|
||||
visibleColumns: optional(any()),
|
||||
visibleEntityTypes: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
CollectionBrowserProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const collectionBrowserPropsSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -22,8 +22,6 @@ import CollectionBreadcrumbs from "metabase/nav/containers/CollectionBreadcrumbs
|
||||
import { Stack } from "metabase/ui";
|
||||
import type { CollectionId, CollectionItemModel } from "metabase-types/api";
|
||||
|
||||
import { collectionBrowserPropsSchema } from "./CollectionBrowser.schema";
|
||||
|
||||
const USER_FACING_ENTITY_NAMES = [
|
||||
"collection",
|
||||
"dashboard",
|
||||
@@ -202,7 +200,6 @@ const CollectionBrowserWrapper = ({
|
||||
return <CollectionBrowserInner collectionId={collectionId} {...restProps} />;
|
||||
};
|
||||
|
||||
export const CollectionBrowser = Object.assign(
|
||||
withPublicComponentWrapper(CollectionBrowserWrapper),
|
||||
{ schema: collectionBrowserPropsSchema },
|
||||
export const CollectionBrowser = withPublicComponentWrapper(
|
||||
CollectionBrowserWrapper,
|
||||
);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { CreateDashboardModalProps } from "./CreateDashboardModal";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
initialCollectionId: optional(any()),
|
||||
isOpen: optional(any()),
|
||||
onClose: optional(any()),
|
||||
onCreate: nonoptional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
CreateDashboardModalProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const createDashboardModalSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -8,8 +8,6 @@ import { useCollectionQuery, useLocale } from "metabase/common/hooks";
|
||||
import { CreateDashboardModal as CreateDashboardModalCore } from "metabase/dashboard/containers/CreateDashboardModal";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
|
||||
import { createDashboardModalSchema } from "./CreateDashboardModal.schema";
|
||||
|
||||
/**
|
||||
* @expand
|
||||
* @category CreateDashboardModal
|
||||
@@ -70,7 +68,6 @@ const CreateDashboardModalInner = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateDashboardModal = Object.assign(
|
||||
withPublicComponentWrapper(CreateDashboardModalInner),
|
||||
{ schema: createDashboardModalSchema },
|
||||
export const CreateDashboardModal = withPublicComponentWrapper(
|
||||
CreateDashboardModalInner,
|
||||
);
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
type infer as zInfer,
|
||||
} from "zod/mini";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { InteractiveQuestionProps } from "./InteractiveQuestion";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
componentPlugins: optional(
|
||||
strictObject({
|
||||
mapQuestionClickActions: optional(any()),
|
||||
dashboard: optional(any()),
|
||||
}),
|
||||
),
|
||||
deserializedCard: optional(any()),
|
||||
entityTypes: optional(any()),
|
||||
height: optional(any()),
|
||||
initialSqlParameters: optional(any()),
|
||||
isSaveEnabled: optional(any()),
|
||||
onBeforeSave: optional(any()),
|
||||
onNavigateBack: optional(any()),
|
||||
onRun: optional(any()),
|
||||
onSave: optional(any()),
|
||||
options: optional(any()),
|
||||
plugins: optional(
|
||||
strictObject({
|
||||
mapQuestionClickActions: optional(any()),
|
||||
dashboard: optional(any()),
|
||||
}),
|
||||
),
|
||||
questionId: nonoptional(any()),
|
||||
style: optional(any()),
|
||||
targetCollection: optional(any()),
|
||||
targetDashboardId: optional(any()),
|
||||
title: optional(any()),
|
||||
width: optional(any()),
|
||||
withChartTypeSelector: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withResetButton: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
InteractiveQuestionProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const interactiveQuestionSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
type SdkQuestionProps,
|
||||
} from "embedding-sdk-bundle/components/public/SdkQuestion/SdkQuestion";
|
||||
|
||||
import { interactiveQuestionSchema } from "./InteractiveQuestion.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
@@ -102,5 +100,4 @@ const subComponents: InteractiveQuestionComponents = {
|
||||
export const InteractiveQuestion = Object.assign(
|
||||
_InteractiveQuestion,
|
||||
subComponents,
|
||||
{ schema: interactiveQuestionSchema },
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
position: relative;
|
||||
max-width: 41.5rem;
|
||||
width: 100%;
|
||||
border: 1px solid var(--mb-color-brand);
|
||||
box-shadow: 0 2px 13.8px -1px #1871bf52;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cx from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useSdkDispatch } from "embedding-sdk-bundle/store";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "metabase/ui";
|
||||
import { MetabotIcon } from "metabase-enterprise/metabot/components/MetabotIcon";
|
||||
import {
|
||||
useMetabotAgent,
|
||||
useMetabotChatHandlers,
|
||||
@@ -29,15 +30,10 @@ export const MetabotChatEmbedding = () => {
|
||||
const metabot = useMetabotAgent();
|
||||
const { handleSubmitInput, handleResetInput } = useMetabotChatHandlers();
|
||||
|
||||
const resetInput = () => {
|
||||
const resetInput = useCallback(() => {
|
||||
handleResetInput();
|
||||
setInputExpanded(false);
|
||||
};
|
||||
|
||||
const startNewConversation = () => {
|
||||
resetInput();
|
||||
metabot.resetConversation();
|
||||
};
|
||||
}, [handleResetInput]);
|
||||
|
||||
const [inputExpanded, setInputExpanded] = useState(false);
|
||||
const handleMaybeExpandInput = () => {
|
||||
@@ -70,9 +66,6 @@ export const MetabotChatEmbedding = () => {
|
||||
dispatch(resetConversationId());
|
||||
}, [dispatch]);
|
||||
|
||||
const isOngoingConversation = metabot.messages.length > 0;
|
||||
const isPromptDefined = metabot.prompt.length > 0;
|
||||
|
||||
return (
|
||||
<Box className={Styles.container} data-testid="metabot-chat">
|
||||
<Flex
|
||||
@@ -81,20 +74,20 @@ export const MetabotChatEmbedding = () => {
|
||||
metabot.isDoingScience && Styles.innerContainerLoading,
|
||||
inputExpanded && Styles.innerContainerExpanded,
|
||||
)}
|
||||
gap="sm"
|
||||
>
|
||||
<Flex
|
||||
w="16px"
|
||||
h="16px"
|
||||
w="33px"
|
||||
h="24px"
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
{metabot.isDoingScience ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<Icon name="ai" c="var(--mb-color-brand)" size="1rem" />
|
||||
<MetabotIcon isLoading={false} />
|
||||
)}
|
||||
</Flex>
|
||||
<Textarea
|
||||
@@ -130,7 +123,7 @@ export const MetabotChatEmbedding = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{metabot.isDoingScience && (
|
||||
{metabot.isDoingScience ? (
|
||||
<UnstyledButton
|
||||
h="1rem"
|
||||
data-testid="metabot-cancel-request"
|
||||
@@ -140,40 +133,17 @@ export const MetabotChatEmbedding = () => {
|
||||
<Icon name="stop" c="var(--mb-color-text-primary)" size="1rem" />
|
||||
</Tooltip>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
|
||||
{!metabot.isDoingScience && (
|
||||
<>
|
||||
{isOngoingConversation && !isPromptDefined && (
|
||||
<UnstyledButton
|
||||
h="1rem"
|
||||
onClick={startNewConversation}
|
||||
data-testid="metabot-new-conversation"
|
||||
>
|
||||
<Tooltip label={t`Start new chat`}>
|
||||
<Icon
|
||||
name="edit_document_outlined"
|
||||
c="var(--mb-color-text-primary)"
|
||||
size="1rem"
|
||||
/>
|
||||
</Tooltip>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
|
||||
{isPromptDefined && (
|
||||
<UnstyledButton
|
||||
h="1rem"
|
||||
onClick={resetInput}
|
||||
data-testid="metabot-close-chat"
|
||||
>
|
||||
<Icon
|
||||
name="close"
|
||||
c="var(--mb-color-text-primary)"
|
||||
size="1rem"
|
||||
/>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<UnstyledButton
|
||||
h="1rem"
|
||||
onClick={resetInput}
|
||||
data-testid="metabot-close-chat"
|
||||
style={{
|
||||
visibility: metabot.prompt.length > 0 ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<Icon name="close" c="var(--mb-color-text-primary)" size="1rem" />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { function as functionSchema } from "zod/mini";
|
||||
|
||||
export const metabotQuestionSchema = functionSchema({
|
||||
input: [],
|
||||
});
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { useMetabotReactions } from "metabase-enterprise/metabot/hooks/use-metabot-reactions";
|
||||
|
||||
import { MetabotChatEmbedding } from "./MetabotChatEmbedding";
|
||||
import { metabotQuestionSchema } from "./MetabotQuestion.schema";
|
||||
import { QuestionDetails } from "./QuestionDetails";
|
||||
import { QuestionTitle } from "./QuestionTitle";
|
||||
|
||||
@@ -110,7 +109,6 @@ const MetabotQuestionWrapped = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const MetabotQuestion = Object.assign(
|
||||
withPublicComponentWrapper(MetabotQuestionWrapped),
|
||||
{ schema: metabotQuestionSchema },
|
||||
export const MetabotQuestion = withPublicComponentWrapper(
|
||||
MetabotQuestionWrapped,
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { function as functionSchema } from "zod/mini";
|
||||
|
||||
export const sdkDebugInfoSchema = functionSchema({
|
||||
input: [],
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { SdkDebugInfo } from "./SdkDebugInfo";
|
||||
@@ -1,33 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { StaticQuestionProps } from "./StaticQuestion";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
height: optional(any()),
|
||||
initialSqlParameters: optional(any()),
|
||||
questionId: nonoptional(any()),
|
||||
style: optional(any()),
|
||||
title: optional(any()),
|
||||
width: optional(any()),
|
||||
withChartTypeSelector: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
StaticQuestionProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const staticQuestionSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -29,8 +29,6 @@ import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/mode
|
||||
import type { ClickActionModeGetter } from "metabase/visualizations/types";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
|
||||
import { staticQuestionSchema } from "./StaticQuestion.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
@@ -154,6 +152,4 @@ const subComponents: StaticQuestionComponents = {
|
||||
DownloadWidgetDropdown: DownloadWidgetDropdown,
|
||||
};
|
||||
|
||||
export const StaticQuestion = Object.assign(_StaticQuestion, subComponents, {
|
||||
schema: staticQuestionSchema,
|
||||
});
|
||||
export const StaticQuestion = Object.assign(_StaticQuestion, subComponents);
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { EditableDashboardProps } from "./EditableDashboard";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
dashboardId: nonoptional(any()),
|
||||
dataPickerProps: optional(
|
||||
strictObject({
|
||||
entityTypes: optional(any()),
|
||||
}),
|
||||
),
|
||||
drillThroughQuestionHeight: optional(any()),
|
||||
drillThroughQuestionProps: optional(
|
||||
strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
entityTypes: optional(any()),
|
||||
height: optional(any()),
|
||||
initialSqlParameters: optional(any()),
|
||||
isSaveEnabled: optional(any()),
|
||||
onBeforeSave: optional(any()),
|
||||
onRun: optional(any()),
|
||||
onSave: optional(any()),
|
||||
plugins: optional(any()),
|
||||
style: optional(any()),
|
||||
targetCollection: optional(any()),
|
||||
title: optional(any()),
|
||||
width: optional(any()),
|
||||
withChartTypeSelector: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withResetButton: optional(any()),
|
||||
}),
|
||||
),
|
||||
hiddenParameters: optional(any()),
|
||||
initialParameters: optional(any()),
|
||||
onLoad: optional(any()),
|
||||
onLoadWithoutCards: optional(any()),
|
||||
plugins: optional(
|
||||
strictObject({
|
||||
mapQuestionClickActions: optional(any()),
|
||||
dashboard: optional(any()),
|
||||
}),
|
||||
),
|
||||
renderDrillThroughQuestion: optional(any()),
|
||||
style: optional(any()),
|
||||
withCardTitle: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withTitle: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
EditableDashboardProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const editableDashboardSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
type SdkDashboardProps,
|
||||
} from "../SdkDashboard";
|
||||
|
||||
import { editableDashboardSchema } from "./EditableDashboard.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
@@ -21,7 +19,7 @@ import { editableDashboardSchema } from "./EditableDashboard.schema";
|
||||
export type EditableDashboardProps = SdkDashboardProps &
|
||||
EditableDashboardOwnProps;
|
||||
|
||||
export const EditableDashboardInner = (props: EditableDashboardProps) => {
|
||||
export const EditableDashboard = (props: EditableDashboardProps) => {
|
||||
const dashboardActions: SdkDashboardInnerProps["dashboardActions"] = ({
|
||||
isEditing,
|
||||
downloadsEnabled,
|
||||
@@ -50,7 +48,3 @@ export const EditableDashboardInner = (props: EditableDashboardProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableDashboard = Object.assign(EditableDashboardInner, {
|
||||
schema: editableDashboardSchema,
|
||||
});
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { InteractiveDashboardProps } from "./InteractiveDashboard";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
dashboardId: nonoptional(any()),
|
||||
dataPickerProps: optional(
|
||||
strictObject({
|
||||
entityTypes: optional(any()),
|
||||
}),
|
||||
),
|
||||
drillThroughQuestionHeight: optional(any()),
|
||||
drillThroughQuestionProps: optional(
|
||||
strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
entityTypes: optional(any()),
|
||||
height: optional(any()),
|
||||
initialSqlParameters: optional(any()),
|
||||
isSaveEnabled: optional(any()),
|
||||
onBeforeSave: optional(any()),
|
||||
onRun: optional(any()),
|
||||
onSave: optional(any()),
|
||||
plugins: optional(any()),
|
||||
style: optional(any()),
|
||||
targetCollection: optional(any()),
|
||||
title: optional(any()),
|
||||
width: optional(any()),
|
||||
withChartTypeSelector: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withResetButton: optional(any()),
|
||||
}),
|
||||
),
|
||||
hiddenParameters: optional(any()),
|
||||
initialParameters: optional(any()),
|
||||
onLoad: optional(any()),
|
||||
onLoadWithoutCards: optional(any()),
|
||||
plugins: optional(
|
||||
strictObject({
|
||||
mapQuestionClickActions: optional(any()),
|
||||
dashboard: optional(any()),
|
||||
}),
|
||||
),
|
||||
renderDrillThroughQuestion: optional(any()),
|
||||
style: optional(any()),
|
||||
withCardTitle: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withTitle: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
InteractiveDashboardProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const interactiveDashboardSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -10,8 +10,6 @@ import type { ClickActionModeGetter } from "metabase/visualizations/types";
|
||||
|
||||
import { SdkDashboard, type SdkDashboardProps } from "../SdkDashboard";
|
||||
|
||||
import { interactiveDashboardSchema } from "./InteractiveDashboard.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
@@ -19,7 +17,7 @@ import { interactiveDashboardSchema } from "./InteractiveDashboard.schema";
|
||||
*/
|
||||
export type InteractiveDashboardProps = SdkDashboardProps;
|
||||
|
||||
const InteractiveDashboardInner = (props: InteractiveDashboardProps) => {
|
||||
export const InteractiveDashboard = (props: InteractiveDashboardProps) => {
|
||||
const getClickActionMode: ClickActionModeGetter = useCallback(
|
||||
({ question }) =>
|
||||
getEmbeddingMode({
|
||||
@@ -48,7 +46,3 @@ const InteractiveDashboardInner = (props: InteractiveDashboardProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveDashboard = Object.assign(InteractiveDashboardInner, {
|
||||
schema: interactiveDashboardSchema,
|
||||
});
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
any,
|
||||
function as functionSchema,
|
||||
nonoptional,
|
||||
optional,
|
||||
strictObject,
|
||||
} from "zod/mini";
|
||||
import type { infer as zInfer } from "zod/v4/core/core";
|
||||
|
||||
import type { ValidateInferredSchema } from "embedding-sdk-bundle/types/schema";
|
||||
|
||||
import type { StaticDashboardProps } from "./StaticDashboard";
|
||||
|
||||
const rawPropsSchema = strictObject({
|
||||
children: optional(any()),
|
||||
className: optional(any()),
|
||||
dashboardId: nonoptional(any()),
|
||||
dataPickerProps: optional(
|
||||
strictObject({
|
||||
entityTypes: optional(any()),
|
||||
}),
|
||||
),
|
||||
hiddenParameters: optional(any()),
|
||||
initialParameters: optional(any()),
|
||||
onLoad: optional(any()),
|
||||
onLoadWithoutCards: optional(any()),
|
||||
plugins: optional(
|
||||
strictObject({
|
||||
mapQuestionClickActions: optional(any()),
|
||||
dashboard: optional(any()),
|
||||
}),
|
||||
),
|
||||
style: optional(any()),
|
||||
withCardTitle: optional(any()),
|
||||
withDownloads: optional(any()),
|
||||
withTitle: optional(any()),
|
||||
});
|
||||
const propsSchema: ValidateInferredSchema<
|
||||
StaticDashboardProps,
|
||||
zInfer<typeof rawPropsSchema>
|
||||
> = rawPropsSchema;
|
||||
|
||||
export const staticDashboardSchema = functionSchema({
|
||||
input: [propsSchema],
|
||||
});
|
||||
@@ -7,21 +7,14 @@ import type { ClickActionModeGetter } from "metabase/visualizations/types";
|
||||
import { StaticQuestionSdkMode } from "../../StaticQuestion/mode";
|
||||
import { SdkDashboard, type SdkDashboardProps } from "../SdkDashboard";
|
||||
|
||||
import { staticDashboardSchema } from "./StaticDashboard.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
* @category Dashboard
|
||||
*/
|
||||
export type StaticDashboardProps = Omit<
|
||||
SdkDashboardProps,
|
||||
| "drillThroughQuestionProps"
|
||||
| "drillThroughQuestionHeight"
|
||||
| "renderDrillThroughQuestion"
|
||||
>;
|
||||
export type StaticDashboardProps = SdkDashboardProps;
|
||||
|
||||
const StaticDashboardInner = (props: StaticDashboardProps) => {
|
||||
export const StaticDashboard = (props: StaticDashboardProps) => {
|
||||
const getClickActionMode: ClickActionModeGetter = ({ question }) =>
|
||||
getEmbeddingMode({
|
||||
question,
|
||||
@@ -47,7 +40,3 @@ const StaticDashboardInner = (props: StaticDashboardProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const StaticDashboard = Object.assign(StaticDashboardInner, {
|
||||
schema: staticDashboardSchema,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getBuildInfo } from "embedding-sdk-shared/lib/get-build-info";
|
||||
import type { BuildInfo } from "metabase/embedding-sdk/types/build-info";
|
||||
|
||||
import S from "./SdkDebugInfo.module.css";
|
||||
import { sdkDebugInfoSchema } from "./SdkDebugInfo.schema";
|
||||
|
||||
type BuildTimeData = {
|
||||
formattedDate: string;
|
||||
@@ -89,7 +88,7 @@ const DebugTable = ({
|
||||
</table>
|
||||
);
|
||||
|
||||
const SdkDebugInfoInner = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
export const SdkDebugInfo = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
const sdkPackageDebugInfoData = getDebugInfoData(
|
||||
getBuildInfo("METABASE_EMBEDDING_SDK_PACKAGE_BUILD_INFO"),
|
||||
);
|
||||
@@ -109,7 +108,3 @@ const SdkDebugInfoInner = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SdkDebugInfo = Object.assign(SdkDebugInfoInner, {
|
||||
schema: sdkDebugInfoSchema,
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// eslint-disable-next-line import/no-commonjs
|
||||
module.exports = {
|
||||
gitRawCommitsOpts: {
|
||||
grep: "(sdk)",
|
||||
},
|
||||
options: {
|
||||
preset: "angular",
|
||||
pkg: {
|
||||
path: "enterprise/frontend/src/embedding-sdk-package/package.template.json",
|
||||
},
|
||||
tagPrefix: "embedding-sdk-",
|
||||
transform: function (commit, cb) {
|
||||
if (typeof commit.gitTags === "string") {
|
||||
const tagPrefix = "embedding-sdk-";
|
||||
const rtag = new RegExp(`tag:\\s*[=]?${tagPrefix}(.+?)[,)]`, "gi");
|
||||
const match = rtag.exec(commit.gitTags);
|
||||
rtag.lastIndex = 0;
|
||||
if (match) {
|
||||
commit.version = match[1];
|
||||
}
|
||||
}
|
||||
if (commit.committerDate) {
|
||||
const dateFormatter = Intl.DateTimeFormat("sv-SE", {
|
||||
timeZone: "UTC",
|
||||
});
|
||||
commit.committerDate = dateFormatter.format(
|
||||
new Date(commit.committerDate),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBackportPrefix(commit)) {
|
||||
const { groups } =
|
||||
/\"(?<type>\w*)(?:\((?<scope>[\w\$\.\-\* ]*)\))?\: (?<message1>.*)\"(?<message2>.*)$/.exec(
|
||||
commit.header,
|
||||
);
|
||||
commit = {
|
||||
...commit,
|
||||
type: groups.type,
|
||||
scope: groups.scope,
|
||||
subject: groups.message1 + groups.message2,
|
||||
};
|
||||
}
|
||||
|
||||
cb(null, commit);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function hasBackportPrefix(commit) {
|
||||
return commit.header.startsWith("🤖 backported");
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
InteractiveDashboard,
|
||||
StaticDashboard,
|
||||
} from "./components/public/dashboard";
|
||||
import { SdkDebugInfo } from "./components/public/SdkDebugInfo";
|
||||
import { SdkDebugInfo } from "./components/public/debug/SdkDebugInfo";
|
||||
import { getApplicationName } from "metabase/selectors/whitelabel";
|
||||
import { getSdkStore } from "./store/index";
|
||||
import {
|
||||
@@ -41,7 +41,6 @@ import { useInitData } from "./hooks/private/use-init-data";
|
||||
import { useLogVersionInfo } from "embedding-sdk-bundle/hooks/private/use-log-version-info";
|
||||
import { createDashboard } from "embedding-sdk-bundle/lib/create-dashboard";
|
||||
import { defineBuildInfo } from "metabase/embedding-sdk/lib/define-build-info";
|
||||
import { validateFunctionSchema } from "embedding-sdk-bundle/lib/validate-function-schema";
|
||||
|
||||
defineBuildInfo("METABASE_EMBEDDING_SDK_BUNDLE_BUILD_INFO");
|
||||
|
||||
@@ -70,7 +69,6 @@ const sdkBundleExports: MetabaseEmbeddingSdkBundleExports = {
|
||||
getUser,
|
||||
useInitData,
|
||||
useLogVersionInfo,
|
||||
validateFunctionSchema,
|
||||
};
|
||||
|
||||
window.METABASE_EMBEDDING_SDK_BUNDLE = sdkBundleExports;
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { ZodMiniFunction, ZodMiniTuple, core } from "zod/mini";
|
||||
import { safeParse } from "zod/mini";
|
||||
|
||||
import type {
|
||||
FunctionParametersSchemaValidationErrorMetadata,
|
||||
FunctionSchemaValidationResult,
|
||||
SchemaValidationErrorMetadata,
|
||||
} from "embedding-sdk-shared/types/validation";
|
||||
|
||||
type FunctionSchema = ZodMiniFunction<ZodMiniTuple<any, null> | never, any>;
|
||||
|
||||
const getErrorMetadata = <
|
||||
TMetadata extends
|
||||
| SchemaValidationErrorMetadata
|
||||
| FunctionParametersSchemaValidationErrorMetadata,
|
||||
>({
|
||||
error,
|
||||
parameterIndex,
|
||||
}: {
|
||||
error: core.$ZodError;
|
||||
parameterIndex?: number;
|
||||
}): TMetadata | null => {
|
||||
for (const issue of error.issues) {
|
||||
switch (issue.code) {
|
||||
case "invalid_type":
|
||||
if (issue.expected !== "nonoptional") {
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
errorCode: "missing_required_property",
|
||||
data: issue.path[0]?.toString() ?? "",
|
||||
parameterIndex,
|
||||
} as TMetadata;
|
||||
case "unrecognized_keys":
|
||||
return {
|
||||
errorCode: "unrecognized_keys",
|
||||
data: issue.keys[0]?.toString() ?? "",
|
||||
parameterIndex,
|
||||
} as TMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateFunctionSchema = <
|
||||
TSchema extends FunctionSchema,
|
||||
TParameters extends unknown[],
|
||||
TReturnValue,
|
||||
>(
|
||||
schema: TSchema,
|
||||
): FunctionSchemaValidationResult<TParameters, TReturnValue> => {
|
||||
const parameterSchemas = schema.def.input.def.items;
|
||||
const returnValueSchema = schema.def.output.def;
|
||||
|
||||
const validateParameters = (parameters: TParameters) => {
|
||||
for (let i = 0; i < parameterSchemas.length; i++) {
|
||||
const parameter = parameters[i];
|
||||
const parameterSchema = parameterSchemas[i];
|
||||
|
||||
const parseResult = safeParse(parameterSchema, parameter);
|
||||
|
||||
if (!parseResult.success) {
|
||||
const errorMetadata =
|
||||
getErrorMetadata<FunctionParametersSchemaValidationErrorMetadata>({
|
||||
error: parseResult.error,
|
||||
parameterIndex: i,
|
||||
});
|
||||
|
||||
if (errorMetadata) {
|
||||
return {
|
||||
success: false,
|
||||
errorMetadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
const validateReturnValue = (returnValue: TReturnValue) => {
|
||||
const parseResult = safeParse(returnValueSchema, returnValue);
|
||||
|
||||
if (!parseResult.success) {
|
||||
const errorMetadata = getErrorMetadata<SchemaValidationErrorMetadata>({
|
||||
error: parseResult.error,
|
||||
});
|
||||
|
||||
if (errorMetadata) {
|
||||
return {
|
||||
success: false,
|
||||
errorMetadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
return {
|
||||
validateParameters,
|
||||
validateReturnValue,
|
||||
};
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
type ValidateTypeAgainstInferredSchema<TType, TSchemaInferType> =
|
||||
Required<TType> extends Required<TSchemaInferType>
|
||||
? TType
|
||||
: Required<TSchemaInferType>;
|
||||
|
||||
type ValidateInferredSchemaAgainstType<TType, TSchemaInferType> =
|
||||
Required<TSchemaInferType> extends Required<TType>
|
||||
? TSchemaInferType
|
||||
: Required<TType>;
|
||||
|
||||
export type ValidateInferredSchema<
|
||||
TType extends ValidateTypeAgainstInferredSchema<_TType, _TSchemaInferType>,
|
||||
TSchemaInferType extends ValidateInferredSchemaAgainstType<
|
||||
_TType,
|
||||
_TSchemaInferType
|
||||
>,
|
||||
_TType = TType,
|
||||
_TSchemaInferType = TSchemaInferType,
|
||||
> = any;
|
||||
@@ -1,27 +1,22 @@
|
||||
import type { JSXElementConstructor } from "react";
|
||||
|
||||
import type { CollectionBrowser } from "embedding-sdk-bundle/components/public/CollectionBrowser";
|
||||
import type { ComponentProvider } from "embedding-sdk-bundle/components/public/ComponentProvider";
|
||||
import type { CreateDashboardModal } from "embedding-sdk-bundle/components/public/CreateDashboardModal";
|
||||
import type { CreateQuestion } from "embedding-sdk-bundle/components/public/CreateQuestion";
|
||||
import type { InteractiveQuestion } from "embedding-sdk-bundle/components/public/InteractiveQuestion/InteractiveQuestion";
|
||||
import type { MetabotQuestion } from "embedding-sdk-bundle/components/public/MetabotQuestion";
|
||||
import type { SdkDebugInfo } from "embedding-sdk-bundle/components/public/SdkDebugInfo/SdkDebugInfo";
|
||||
import type { StaticQuestion } from "embedding-sdk-bundle/components/public/StaticQuestion/StaticQuestion";
|
||||
import type { EditableDashboard } from "embedding-sdk-bundle/components/public/dashboard/EditableDashboard";
|
||||
import type { InteractiveDashboard } from "embedding-sdk-bundle/components/public/dashboard/InteractiveDashboard";
|
||||
import type { StaticDashboard } from "embedding-sdk-bundle/components/public/dashboard/StaticDashboard";
|
||||
import type { SdkDebugInfo } from "embedding-sdk-bundle/components/public/debug/SdkDebugInfo";
|
||||
import type { SdkStore, SdkStoreState } from "embedding-sdk-bundle/store/types";
|
||||
import type { LoginStatus } from "embedding-sdk-bundle/types/user";
|
||||
import type {
|
||||
CreateDashboardValues,
|
||||
MetabaseDashboard,
|
||||
} from "embedding-sdk-bundle/types";
|
||||
import type { LoginStatus } from "embedding-sdk-bundle/types/user";
|
||||
import type { FunctionSchemaValidationResult } from "embedding-sdk-shared/types/validation";
|
||||
} from "embedding-sdk-package";
|
||||
import type { User } from "metabase-types/api";
|
||||
|
||||
export type InternalComponent<TComponent extends JSXElementConstructor<any>> =
|
||||
TComponent & { schema?: any };
|
||||
type InternalHook = () => void;
|
||||
type ReduxStoreFactory = () => SdkStore;
|
||||
type ReduxStoreSelector<T> = (state: SdkStoreState) => T;
|
||||
@@ -38,34 +33,33 @@ export type MetabaseEmbeddingSdkBundleExports = PublicExports &
|
||||
ReduxStoreExports &
|
||||
ReduxStoreUtilityFunctionExports &
|
||||
ReduxStoreSelectorsExports &
|
||||
InternalHooksExports &
|
||||
SchemaValidationUtils;
|
||||
InternalHooksExports;
|
||||
|
||||
export type PublicExports = {
|
||||
CollectionBrowser: InternalComponent<typeof CollectionBrowser>;
|
||||
CreateDashboardModal: InternalComponent<typeof CreateDashboardModal>;
|
||||
CreateQuestion: InternalComponent<typeof CreateQuestion>;
|
||||
EditableDashboard: InternalComponent<typeof EditableDashboard>;
|
||||
InteractiveDashboard: InternalComponent<typeof InteractiveDashboard>;
|
||||
InteractiveQuestion: InternalComponent<typeof InteractiveQuestion>;
|
||||
ComponentProvider: InternalComponent<typeof ComponentProvider>;
|
||||
MetabotQuestion: InternalComponent<typeof MetabotQuestion>;
|
||||
SdkDebugInfo: InternalComponent<typeof SdkDebugInfo>;
|
||||
StaticDashboard: InternalComponent<typeof StaticDashboard>;
|
||||
StaticQuestion: InternalComponent<typeof StaticQuestion>;
|
||||
type PublicExports = {
|
||||
CollectionBrowser: typeof CollectionBrowser;
|
||||
CreateDashboardModal: typeof CreateDashboardModal;
|
||||
CreateQuestion: typeof CreateQuestion;
|
||||
EditableDashboard: typeof EditableDashboard;
|
||||
InteractiveDashboard: typeof InteractiveDashboard;
|
||||
InteractiveQuestion: typeof InteractiveQuestion;
|
||||
ComponentProvider: typeof ComponentProvider;
|
||||
MetabotQuestion: typeof MetabotQuestion;
|
||||
SdkDebugInfo: typeof SdkDebugInfo;
|
||||
StaticDashboard: typeof StaticDashboard;
|
||||
StaticQuestion: typeof StaticQuestion;
|
||||
};
|
||||
|
||||
export type ReduxStoreExports = {
|
||||
type ReduxStoreExports = {
|
||||
getSdkStore: ReduxStoreFactory;
|
||||
};
|
||||
|
||||
export type ReduxStoreUtilityFunctionExports = {
|
||||
type ReduxStoreUtilityFunctionExports = {
|
||||
createDashboard: ReduxStoreUtilityFunction<
|
||||
(params: CreateDashboardValues) => Promise<MetabaseDashboard>
|
||||
>;
|
||||
};
|
||||
|
||||
export type ReduxStoreSelectorsExports = {
|
||||
type ReduxStoreSelectorsExports = {
|
||||
getApplicationName: ReduxStoreSelector<string>;
|
||||
getAvailableFonts: ReduxStoreSelector<string[]>;
|
||||
getLoginStatus: ReduxStoreSelector<LoginStatus>;
|
||||
@@ -76,9 +70,3 @@ export type InternalHooksExports = {
|
||||
useInitData: InternalHook;
|
||||
useLogVersionInfo: InternalHook;
|
||||
};
|
||||
|
||||
export type SchemaValidationUtils = {
|
||||
validateFunctionSchema: (
|
||||
schema: any,
|
||||
) => FunctionSchemaValidationResult<unknown[], unknown>;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import {
|
||||
type ComponentProps,
|
||||
type JSX,
|
||||
type JSXElementConstructor,
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { type FunctionComponent, useEffect, useId, useRef } from "react";
|
||||
|
||||
import type { InternalComponent } from "embedding-sdk-bundle/types/sdk-bundle";
|
||||
import { ClientSideOnlyWrapper } from "embedding-sdk-package/components/private/ClientSideOnlyWrapper/ClientSideOnlyWrapper";
|
||||
import { Error } from "embedding-sdk-package/components/private/Error/Error";
|
||||
import { Loader } from "embedding-sdk-package/components/private/Loader/Loader";
|
||||
import {
|
||||
SDK_COMPONENT_MISSING_REQUIRED_PROPERTY_MESSAGE,
|
||||
SDK_COMPONENT_NOT_YET_AVAILABLE_MESSAGE,
|
||||
SDK_COMPONENT_UNRECOGNIZED_PROPERTY_MESSAGE,
|
||||
SDK_LOADING_ERROR_MESSAGE,
|
||||
SDK_NOT_LOADED_YET_MESSAGE,
|
||||
SDK_NOT_STARTED_LOADING_MESSAGE,
|
||||
@@ -29,13 +18,9 @@ import {
|
||||
SdkLoadingError,
|
||||
SdkLoadingState,
|
||||
} from "embedding-sdk-shared/types/sdk-loading";
|
||||
import type { FunctionParametersSchemaValidationErrorMetadata } from "embedding-sdk-shared/types/validation";
|
||||
|
||||
type Props<TComponentProps> = {
|
||||
getComponent: () =>
|
||||
| InternalComponent<JSXElementConstructor<TComponentProps>>
|
||||
| null
|
||||
| undefined;
|
||||
getComponent: () => FunctionComponent<TComponentProps> | null | undefined;
|
||||
componentProps: TComponentProps | undefined;
|
||||
};
|
||||
|
||||
@@ -70,74 +55,6 @@ const NotStartedLoadingTrigger = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getValidationErrorMessage = (
|
||||
errorMetadata: FunctionParametersSchemaValidationErrorMetadata | undefined,
|
||||
) => {
|
||||
if (!errorMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { errorCode, data } = errorMetadata;
|
||||
|
||||
switch (errorCode) {
|
||||
case "missing_required_property":
|
||||
return `"${data}": ${SDK_COMPONENT_MISSING_REQUIRED_PROPERTY_MESSAGE}`;
|
||||
case "unrecognized_keys":
|
||||
return `"${data}": ${SDK_COMPONENT_UNRECOGNIZED_PROPERTY_MESSAGE}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const RenderComponentWithValidation = <
|
||||
TComponent extends InternalComponent<JSXElementConstructor<any>>,
|
||||
>({
|
||||
$component: Component,
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
{
|
||||
$component: TComponent;
|
||||
} & ComponentProps<TComponent>
|
||||
>) => {
|
||||
const validateFunctionSchema =
|
||||
getWindow()?.METABASE_EMBEDDING_SDK_BUNDLE?.validateFunctionSchema;
|
||||
const schema = Component.schema;
|
||||
|
||||
// We validate only initial props currently
|
||||
const propsToValidateRef = useRef(props);
|
||||
|
||||
useEffect(() => {
|
||||
if (!validateFunctionSchema || !schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAsync = async () => {
|
||||
try {
|
||||
const { validateParameters } = validateFunctionSchema(schema);
|
||||
const validationResult = validateParameters([
|
||||
propsToValidateRef.current,
|
||||
]);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const error = getValidationErrorMessage(
|
||||
validationResult.errorMetadata,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error during component schema validation: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
validateAsync();
|
||||
}, [schema, validateFunctionSchema]);
|
||||
|
||||
return <Component {...props}>{props.children}</Component>;
|
||||
};
|
||||
|
||||
const ComponentWrapperInner = <TComponentProps,>({
|
||||
getComponent,
|
||||
componentProps,
|
||||
@@ -181,16 +98,14 @@ const ComponentWrapperInner = <TComponentProps,>({
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderComponentWithValidation
|
||||
$component={ComponentProvider}
|
||||
<ComponentProvider
|
||||
{...metabaseProviderProps}
|
||||
reduxStore={metabaseProviderInternalProps.reduxStore}
|
||||
>
|
||||
<RenderComponentWithValidation
|
||||
$component={Component as InternalComponent<JSXElementConstructor<any>>}
|
||||
{...componentProps}
|
||||
<Component
|
||||
{...(componentProps as JSX.IntrinsicAttributes & TComponentProps)}
|
||||
/>
|
||||
</RenderComponentWithValidation>
|
||||
</ComponentProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -203,10 +118,7 @@ type ComponentWrapperFunction<P> = [P] extends [never]
|
||||
export const createComponent = <
|
||||
TComponentProps extends Record<any, any> | undefined | never = never,
|
||||
>(
|
||||
getComponent: () =>
|
||||
| InternalComponent<JSXElementConstructor<TComponentProps>>
|
||||
| null
|
||||
| undefined,
|
||||
getComponent: () => FunctionComponent<TComponentProps> | null | undefined,
|
||||
): ComponentWrapperFunction<TComponentProps> => {
|
||||
return function ComponentWrapper(props: TComponentProps) {
|
||||
const ensureSingleInstanceId = useId();
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
export const SDK_NOT_STARTED_LOADING_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. Ensure all SDK components are wrapped in the Provider component.";
|
||||
"Error loading the Embedding Analytics SDK. Ensure all SDK components are wrapped in the Provider component.";
|
||||
|
||||
export const SDK_LOADING_ERROR_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. Verify the instance URL is correct and reachable.";
|
||||
"Error loading the Embedding Analytics SDK. Verify the instance URL is correct and reachable.";
|
||||
|
||||
export const SDK_NOT_LOADED_YET_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. The loading state is `Loaded` but the SDK bundle is not loaded yet.";
|
||||
"Error loading the Embedding Analytics SDK. The loading state is `Loaded` but the SDK bundle is not loaded yet.";
|
||||
|
||||
export const SDK_COMPONENT_NOT_YET_AVAILABLE_MESSAGE =
|
||||
"The component is not available in the Embedded Analytics SDK bundle. Update your analytics server to access the component.";
|
||||
|
||||
export const SDK_COMPONENT_MISSING_REQUIRED_PROPERTY_MESSAGE =
|
||||
// eslint-disable-next-line no-literal-metabase-strings -- console.error message
|
||||
"this property is required by the component. Your Embedded Analytics SDK and Metabase instance are out of sync. Update them to matching versions and adjust props based on the updated TypeScript types.";
|
||||
|
||||
export const SDK_COMPONENT_UNRECOGNIZED_PROPERTY_MESSAGE =
|
||||
// eslint-disable-next-line no-literal-metabase-strings -- console.error message
|
||||
"this property is not recognized by the component and will be ignored. Your Embedded Analytics SDK and Metabase instance are out of sync. Update them to matching versions and adjust props based on the updated TypeScript types.";
|
||||
"The component is not available in the Embedding Analytics SDK bundle. Update the analytics server to serve the correct bundle.";
|
||||
|
||||
@@ -84,7 +84,7 @@ TEST_SUITE=metabase-nodejs-react-sdk-embedding-sample-e2e OPEN_UI=false EMBEDDIN
|
||||
```
|
||||
|
||||
##### :warning: Obtaining the Shoppy's Metabase App DB Dump locally
|
||||
For the Shoppy's Sample App Tests (`TEST_SUITE=shoppy-e2e`) locally, a proper App DB dump of the Shoppy's Metabase instance must be placed to the `./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql`
|
||||
For the Shoppy's Sample App Tests (`TEST_SUITE=shoppy-e2e`) locally, a proper App DB dump of the Shoppy's Metabase Instance must be placed to the `./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql`
|
||||
|
||||
You can get it by:
|
||||
- Enabling the `Tailscale` and logging in using your work email address.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
export type SchemaValidationErrorCode =
|
||||
| "missing_required_property"
|
||||
| "unrecognized_keys";
|
||||
|
||||
export type SchemaValidationErrorMetadata = {
|
||||
errorCode: SchemaValidationErrorCode;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type FunctionParametersSchemaValidationErrorMetadata =
|
||||
SchemaValidationErrorMetadata & {
|
||||
parameterIndex: number;
|
||||
};
|
||||
|
||||
export type FunctionSchemaValidationResult<
|
||||
TParameters extends unknown[],
|
||||
TReturnValue,
|
||||
> = {
|
||||
validateParameters: (parameters: TParameters) => {
|
||||
success: boolean;
|
||||
errorMetadata?: FunctionParametersSchemaValidationErrorMetadata;
|
||||
};
|
||||
validateReturnValue: (returnValue: TReturnValue) => {
|
||||
success: boolean;
|
||||
errorMetadata?: SchemaValidationErrorMetadata;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Api } from "metabase/api";
|
||||
|
||||
export type PythonLibrary = {
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type GetPythonLibraryRequest = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type UpdatePythonLibraryRequest = {
|
||||
name: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export const pythonLibraryApi = Api.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
getPythonLibrary: builder.query<PythonLibrary, GetPythonLibraryRequest>({
|
||||
query: ({ name }) => ({
|
||||
url: `/api/ee/python-transform/user-modules/${name}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
updatePythonLibrary: builder.mutation<void, UpdatePythonLibraryRequest>({
|
||||
query: ({ name, source }) => ({
|
||||
url: `/api/ee/python-transform/user-modules/${name}`,
|
||||
method: "PUT",
|
||||
body: { source },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetPythonLibraryQuery, useUpdatePythonLibraryMutation } =
|
||||
pythonLibraryApi;
|
||||
@@ -2,7 +2,6 @@ import type { HoveredObject } from "metabase/visualizations/types";
|
||||
import type { DatasetColumn, DictionaryArray } from "metabase-types/api";
|
||||
import { createMockColumn, createMockSeries } from "metabase-types/api/mocks";
|
||||
|
||||
import { leaveUntranslated } from "../use-translate-content";
|
||||
import {
|
||||
translateCardNames,
|
||||
translateContentString,
|
||||
@@ -401,8 +400,9 @@ describe("content translation utils", () => {
|
||||
|
||||
it("should return the original series if no translations are available", () => {
|
||||
const series = createMockSeries();
|
||||
const result = translateFieldValuesInSeries(series, leaveUntranslated);
|
||||
const result = translateFieldValuesInSeries(series, mockTC);
|
||||
expect(result).toEqual(series);
|
||||
expect(mockTC).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should translate all columns in the series", () => {
|
||||
|
||||
@@ -4,11 +4,7 @@ import _ from "underscore";
|
||||
|
||||
import type { ContentTranslationFunction } from "metabase/i18n/types";
|
||||
import type { HoveredObject } from "metabase/visualizations/types";
|
||||
import type {
|
||||
DictionaryArray,
|
||||
MaybeTranslatedSeries,
|
||||
Series,
|
||||
} from "metabase-types/api";
|
||||
import type { DictionaryArray, Series } from "metabase-types/api";
|
||||
|
||||
import { hasTranslations, useTranslateContent } from "./use-translate-content";
|
||||
|
||||
@@ -123,7 +119,7 @@ export const useTranslateFieldValuesInHoveredObject = (
|
||||
export const translateFieldValuesInSeries = (
|
||||
series: Series,
|
||||
tc: ContentTranslationFunction,
|
||||
): MaybeTranslatedSeries => {
|
||||
) => {
|
||||
if (!hasTranslations(tc)) {
|
||||
return series;
|
||||
}
|
||||
@@ -131,18 +127,12 @@ export const translateFieldValuesInSeries = (
|
||||
if (!singleSeries.data) {
|
||||
return singleSeries;
|
||||
}
|
||||
const untranslatedRows = singleSeries.data.rows.concat();
|
||||
|
||||
const translatedRows = singleSeries.data.rows.map((row) =>
|
||||
row.map((value) => tc(value)),
|
||||
);
|
||||
return {
|
||||
...singleSeries,
|
||||
data: {
|
||||
...singleSeries.data,
|
||||
untranslatedRows,
|
||||
rows: translatedRows,
|
||||
},
|
||||
data: { ...singleSeries.data, rows: translatedRows },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export const Editor: React.FC<EditorProps> = ({
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: t`Start writing, type "/" to list commands, or "@" to mention an item...`,
|
||||
placeholder: t`Start writing, press "/" to open command palette, or "@" to insert a link...`,
|
||||
}),
|
||||
CardEmbed,
|
||||
MentionExtension.configure({
|
||||
|
||||
@@ -149,7 +149,7 @@ export const MetabotPurchasePage = () => {
|
||||
label={
|
||||
<Text>
|
||||
{t`I agree with the Metabot AI add-on`}{" "}
|
||||
<ExternalLink href="https://www.metabase.com/license/hosting">{t`Terms of Service`}</ExternalLink>
|
||||
<ExternalLink href="https://www.metabase.com/license/metabot-addendum">{t`Terms of Service`}</ExternalLink>
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from "metabase/ui";
|
||||
import { useGetSuggestedMetabotPromptsQuery } from "metabase-enterprise/api";
|
||||
import { MetabotResetLongChatButton } from "metabase-enterprise/metabot/components/MetabotChat/MetabotResetLongChatButton";
|
||||
@@ -69,14 +68,12 @@ export const MetabotChat = () => {
|
||||
</Flex>
|
||||
|
||||
<Flex gap="sm">
|
||||
<Tooltip label={t`Clear conversation`} position="bottom">
|
||||
<ActionIcon
|
||||
onClick={() => metabot.resetConversation()}
|
||||
data-testid="metabot-reset-chat"
|
||||
>
|
||||
<Icon c="text-primary" name="revert" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
onClick={() => metabot.resetConversation()}
|
||||
data-testid="metabot-reset-chat"
|
||||
>
|
||||
<Icon c="text-primary" name="revert" />
|
||||
</ActionIcon>
|
||||
<ActionIcon onClick={handleClose} data-testid="metabot-close-chat">
|
||||
<Icon c="text-primary" name="close" />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.dot {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes fade-in-out {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dotLoading {
|
||||
animation: fade-in-out 1800ms infinite linear alternate;
|
||||
}
|
||||
|
||||
.dotOneLoading {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
.dotTwoLoading {
|
||||
animation-delay: 600ms;
|
||||
}
|
||||
|
||||
.dotThreeLoading {
|
||||
animation-delay: -600ms;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import cx from "classnames";
|
||||
|
||||
import Styles from "./MetabotIcon.module.css";
|
||||
|
||||
const COLORS = {
|
||||
brand: "var(--mb-color-brand)",
|
||||
brandLighter: "var(--mb-color-brand-lighter)",
|
||||
focus: "var(--mb-color-focus)",
|
||||
summarize: "var(--mb-color-summarize)",
|
||||
};
|
||||
|
||||
export const MetabotIcon = ({ isLoading }: { isLoading: boolean }) => {
|
||||
const dotsStrokeColor = isLoading ? COLORS.summarize : COLORS.brand;
|
||||
const dotsFillColor = isLoading ? COLORS.summarize : COLORS.focus;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="33"
|
||||
height="24"
|
||||
viewBox="0 0 33 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="0.75"
|
||||
y="0.75"
|
||||
width="30.8721"
|
||||
height="22.5"
|
||||
rx="3.25"
|
||||
fill={COLORS.focus}
|
||||
stroke={COLORS.brand}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="3.90698"
|
||||
y="3.34961"
|
||||
width="24.5581"
|
||||
height="14.5116"
|
||||
rx="2"
|
||||
fill={COLORS.brandLighter}
|
||||
stroke={COLORS.brand}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
{isLoading && (
|
||||
<>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18 13H15V12H18V13Z"
|
||||
fill={COLORS.brand}
|
||||
/>
|
||||
<path
|
||||
d="M10 8.5C10 9 10.8 10 12 10C13.2 10 13.8333 9 14 8.5"
|
||||
stroke={COLORS.brand}
|
||||
/>
|
||||
<path
|
||||
d="M19 8.5C19 9 19.8 10 21 10C22.2 10 22.8333 9 23 8.5"
|
||||
stroke={COLORS.brand}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<>
|
||||
<path
|
||||
d="M17.3023 11.4414H17.8605C17.8605 12.4289 17.2754 13.1158 16.186 13.1158C15.0967 13.1158 14.5116 12.4289 14.5116 11.4414H15.0698C15.0698 11.9995 15.3488 12.5577 16.186 12.5577C17.0233 12.5577 17.3023 11.9995 17.3023 11.4414Z"
|
||||
fill={COLORS.brand}
|
||||
/>
|
||||
<circle cx="11.5814" cy="8.7912" r="0.976744" fill={COLORS.brand} />
|
||||
<ellipse
|
||||
cx="20.5117"
|
||||
cy="8.7912"
|
||||
rx="0.976745"
|
||||
ry="0.976744"
|
||||
fill={COLORS.brand}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* dots */}
|
||||
{isLoading && (
|
||||
<circle
|
||||
cx="21.1163"
|
||||
cy="20.6509"
|
||||
r="1.11628"
|
||||
fill={dotsFillColor}
|
||||
className={cx(
|
||||
Styles.dot,
|
||||
isLoading && Styles.dotLoading,
|
||||
isLoading && Styles.dotOneLoading,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<path
|
||||
d="M27.9651 20.6514C27.9651 20.9918 27.6892 21.2677 27.3488 21.2677C27.0085 21.2677 26.7325 20.9918 26.7325 20.6514C26.7325 20.3111 27.0085 20.0352 27.3488 20.0352C27.6892 20.0352 27.9651 20.3111 27.9651 20.6514Z"
|
||||
fill={dotsFillColor}
|
||||
stroke={dotsStrokeColor}
|
||||
className={cx(
|
||||
Styles.dot,
|
||||
isLoading && Styles.dotLoading,
|
||||
isLoading && Styles.dotTwoLoading,
|
||||
)}
|
||||
/>
|
||||
<circle
|
||||
cx="24.2791"
|
||||
cy="20.6514"
|
||||
r="0.61628"
|
||||
fill={dotsFillColor}
|
||||
stroke={dotsStrokeColor}
|
||||
className={cx(
|
||||
Styles.dot,
|
||||
isLoading && Styles.dotLoading,
|
||||
isLoading && Styles.dotThreeLoading,
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./MetabotIcon";
|
||||
@@ -65,7 +65,7 @@ export function AdminDatabaseTableEditingSection({
|
||||
settings: { [DATABASE_TABLE_EDITING_SETTING]: enabled },
|
||||
});
|
||||
} catch (err) {
|
||||
setError(getResponseErrorMessageReason(err) || t`An error occurred`);
|
||||
setError(getResponseErrorMessage(err) || t`An error occurred`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,19 +112,3 @@ export function AdminDatabaseTableEditingSection({
|
||||
</DatabaseInfoSection>
|
||||
);
|
||||
}
|
||||
|
||||
function getResponseErrorMessageReason(error: unknown): string | undefined {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"data" in error &&
|
||||
typeof error.data === "object" &&
|
||||
error.data &&
|
||||
"reasons" in error.data &&
|
||||
Array.isArray(error.data.reasons)
|
||||
) {
|
||||
return error.data.reasons[0]?.message ?? undefined;
|
||||
}
|
||||
|
||||
return getResponseErrorMessage(error);
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ export const TableActionInputSearchableSelect = ({
|
||||
key={item.value}
|
||||
>
|
||||
{item.label}
|
||||
{searchFieldId ? ` [${item.value}]` : ""}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : isLoading ? (
|
||||
|
||||
@@ -52,19 +52,14 @@ export function getRowSelectColumn({
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
cell: ({
|
||||
row,
|
||||
table,
|
||||
}: {
|
||||
row: Row<RowValues>;
|
||||
table: Table<RowValues>;
|
||||
}) => (
|
||||
cell: ({ row }: { row: Row<RowValues> }) => (
|
||||
<BaseCell className={CellS.cell}>
|
||||
<Group align="center" justify="flex-start" h="100%">
|
||||
<Checkbox
|
||||
size={rem(16)}
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
data-testid="row-select-checkbox"
|
||||
styles={{
|
||||
@@ -74,17 +69,14 @@ export function getRowSelectColumn({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Only show the edit icon when no rows are selected */}
|
||||
{!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && (
|
||||
<Tooltip label="Edit record">
|
||||
<Icon
|
||||
name="pencil"
|
||||
data-testid="row-edit-icon"
|
||||
className={S.pencilIcon}
|
||||
onClick={() => onRowEditClick(row.index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Edit record">
|
||||
<Icon
|
||||
name="pencil"
|
||||
data-testid="row-edit-icon"
|
||||
className={S.pencilIcon}
|
||||
onClick={() => onRowEditClick(row.index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</BaseCell>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import cx from "classnames";
|
||||
|
||||
import { CodeEditor } from "metabase/common/components/CodeEditor";
|
||||
|
||||
import S from "./PythonEditor.module.css";
|
||||
import { completion } from "./utils";
|
||||
|
||||
export function PythonEditor({
|
||||
value,
|
||||
onChange,
|
||||
withPandasCompletions,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
withPandasCompletions?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<CodeEditor
|
||||
className={cx(S.editor, className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
language="python"
|
||||
extensions={withPandasCompletions ? completion : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./PythonEditor";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user