Compare commits

..

9 Commits

Author SHA1 Message Date
Romeo Van Snick
fd50ffa7dd Parametrise PythonLibraryEditor 2025-09-12 16:04:06 +10:00
Romeo Van Snick
2880033f06 Update python lib terminology and parametrise API 2025-09-12 15:54:41 +10:00
Romeo Van Snick
87161c6d71 Add shared lib toggle 2025-09-12 12:42:19 +10:00
Romeo Van Snick
89117dbfb0 Remove trailing whitespace 2025-09-12 11:27:36 +10:00
Romeo Van Snick
3a1b93b79d Add python library editor 2025-09-11 20:29:59 +10:00
Romeo Van Snick
2f651e074d Allow passing className to python editor 2025-09-11 20:28:01 +10:00
Romeo Van Snick
54a2642138 Add api endpoints for python library 2025-09-11 20:28:01 +10:00
Romeo Van Snick
d3a74d538d Add fullWidth prop to admin layout 2025-09-11 20:28:01 +10:00
Romeo Van Snick
82b097feb8 Move PythonCodeEditor to separate component 2025-09-11 20:28:01 +10:00
180 changed files with 2956 additions and 4310 deletions

View File

@@ -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}

View File

@@ -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]

View File

@@ -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/**"

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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})

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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",
);

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
},
};

View File

@@ -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);

View File

@@ -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",
);
});
});

View File

@@ -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(

View File

@@ -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();

View File

@@ -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();
});
});

View File

@@ -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");
});
});
});

View File

@@ -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");
});
});
});

View File

@@ -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)

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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)))

View File

@@ -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")
[]))

View File

@@ -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."

View File

@@ -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")})

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"}))))))))

View File

@@ -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"))))))

View File

@@ -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]]

View File

@@ -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)))

View File

@@ -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

View File

@@ -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)

View File

@@ -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"}

View File

@@ -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)

View File

@@ -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"}))

View File

@@ -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")

View File

@@ -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

View File

@@ -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]

View File

@@ -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"})

View File

@@ -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")]]

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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))))

View File

@@ -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!

View File

@@ -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

View File

@@ -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"],

View File

@@ -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)

View File

@@ -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],
});

View File

@@ -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,
);

View File

@@ -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],
});

View File

@@ -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,
);

View File

@@ -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],
});

View File

@@ -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 },
);

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
import { function as functionSchema } from "zod/mini";
export const metabotQuestionSchema = functionSchema({
input: [],
});

View File

@@ -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,
);

View File

@@ -1,5 +0,0 @@
import { function as functionSchema } from "zod/mini";
export const sdkDebugInfoSchema = functionSchema({
input: [],
});

View File

@@ -1 +0,0 @@
export { SdkDebugInfo } from "./SdkDebugInfo";

View File

@@ -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],
});

View File

@@ -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);

View File

@@ -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],
});

View File

@@ -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,
});

View File

@@ -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],
});

View File

@@ -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,
});

View File

@@ -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],
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -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>;
};

View File

@@ -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();

View File

@@ -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.";

View File

@@ -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.

View File

@@ -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;
};
};

View File

@@ -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;

View File

@@ -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", () => {

View File

@@ -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 },
};
});
};

View File

@@ -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({

View File

@@ -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>
}
/>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -0,0 +1 @@
export * from "./MetabotIcon";

View File

@@ -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);
}

View File

@@ -197,6 +197,7 @@ export const TableActionInputSearchableSelect = ({
key={item.value}
>
{item.label}
{searchFieldId ? ` [${item.value}]` : ""}
</Combobox.Option>
))
) : isLoading ? (

View File

@@ -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>
),

View File

@@ -0,0 +1,4 @@
.editor {
width: 100%;
height: 100%;
}

View File

@@ -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}
/>
);
}

View File

@@ -0,0 +1 @@
export * from "./PythonEditor";

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