Compare commits
32 Commits
persistent
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44199b4fe6 | ||
|
|
aeb73c0ccf | ||
|
|
dadc072583 | ||
|
|
e7e2061cf6 | ||
|
|
21e4ab1db8 | ||
|
|
7c568b1b70 | ||
|
|
3a0096f0f9 | ||
|
|
a47e0e5556 | ||
|
|
c7c35e2cb0 | ||
|
|
3ae4f895fa | ||
|
|
629f8df242 | ||
|
|
03fca79f68 | ||
|
|
e51c5799ed | ||
|
|
3ac5f8a79f | ||
|
|
27d3c3abf5 | ||
|
|
7a355c400a | ||
|
|
c6fc30b121 | ||
|
|
8109e8bba9 | ||
|
|
5bf7deb045 | ||
|
|
0042464eaf | ||
|
|
e4397295e8 | ||
|
|
7fbe16ecef | ||
|
|
1ea1b1ccbd | ||
|
|
e82748cb2e | ||
|
|
bb54f11b1b | ||
|
|
2593e2faa3 | ||
|
|
60405632ab | ||
|
|
fbdd822ceb | ||
|
|
2cd2e9da48 | ||
|
|
3ec81d963a | ||
|
|
4280bfa352 | ||
|
|
2852c8254a |
0
e2e/support/commands/embedding-sdk/utils.ts
Normal file
0
e2e/support/commands/embedding-sdk/utils.ts
Normal file
@@ -8,6 +8,7 @@ import {
|
||||
verifyDownloadTasks,
|
||||
} from "./commands/downloads/downloadUtils";
|
||||
import * as dbTasks from "./db_tasks";
|
||||
import { signJwt } from "./helpers/e2e-jwt-tasks";
|
||||
|
||||
const createBundler = require("@bahmutov/cypress-esbuild-preprocessor"); // This function is called when a project is opened or re-opened (e.g. due to the project's config changing)
|
||||
const {
|
||||
@@ -27,6 +28,8 @@ const targetVersion = process.env["CROSS_VERSION_TARGET"];
|
||||
|
||||
const feHealthcheckEnabled = process.env["CYPRESS_FE_HEALTHCHECK"] === "true";
|
||||
|
||||
const isEmbeddingSdk = process.env.CYPRESS_IS_EMBEDDING_SDK === "true";
|
||||
|
||||
// docs say that tsconfig paths should handle aliases, but they don't
|
||||
const assetsResolverPlugin = {
|
||||
name: "assetsResolver",
|
||||
@@ -104,6 +107,7 @@ const defaultConfig = {
|
||||
...dbTasks,
|
||||
...verifyDownloadTasks,
|
||||
removeDirectory,
|
||||
signJwt,
|
||||
});
|
||||
|
||||
// this is an official workaround to keep recordings of the failed specs only
|
||||
@@ -169,6 +173,14 @@ const defaultConfig = {
|
||||
|
||||
const mainConfig = {
|
||||
...defaultConfig,
|
||||
...(isEmbeddingSdk
|
||||
? {
|
||||
chromeWebSecurity: true,
|
||||
hosts: {
|
||||
"my-site.local": "127.0.0.1",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
projectId: "ywjy9z",
|
||||
numTestsKeptInMemory: process.env["CI"] ? 1 : 50,
|
||||
reporter: "cypress-multi-reporters",
|
||||
|
||||
11
e2e/support/helpers/e2e-jwt-tasks.ts
Normal file
11
e2e/support/helpers/e2e-jwt-tasks.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export function signJwt({
|
||||
payload,
|
||||
secret,
|
||||
}: {
|
||||
payload: Record<string, string | number>;
|
||||
secret: string;
|
||||
}): string {
|
||||
return jwt.sign(payload, secret);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { USERS } from "e2e/support/cypress_data";
|
||||
import {
|
||||
ORDERS_DASHBOARD_DASHCARD_ID,
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import {
|
||||
getTextCardDetails,
|
||||
restore,
|
||||
setTokenFeatures,
|
||||
visitFullAppEmbeddingUrl,
|
||||
} from "e2e/support/helpers";
|
||||
import {
|
||||
EMBEDDING_SDK_STORY_HOST,
|
||||
describeSDK,
|
||||
} from "e2e/support/helpers/e2e-embedding-sdk-helpers";
|
||||
import {
|
||||
JWT_SHARED_SECRET,
|
||||
setupJwt,
|
||||
} from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
const STORYBOOK_ID = "embeddingsdk-cypressstaticdashboardwithcors--default";
|
||||
describeSDK("scenarios > embedding-sdk > static-dashboard", () => {
|
||||
beforeEach(() => {
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
setTokenFeatures("all");
|
||||
setupJwt();
|
||||
|
||||
const textCard = getTextCardDetails({ col: 16, text: "Text text card" });
|
||||
const questionCard = {
|
||||
id: ORDERS_DASHBOARD_DASHCARD_ID,
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
row: 0,
|
||||
col: 0,
|
||||
size_x: 16,
|
||||
size_y: 8,
|
||||
visualization_settings: {
|
||||
"card.title": "Test question card",
|
||||
},
|
||||
};
|
||||
|
||||
cy.createDashboard(
|
||||
{
|
||||
name: "Embedding Sdk Test Dashboard",
|
||||
dashcards: [questionCard, textCard],
|
||||
},
|
||||
{ wrapId: true },
|
||||
);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/*").as("getDashboard");
|
||||
cy.intercept("GET", "/api/user/current").as("getUser");
|
||||
cy.intercept("POST", "/api/dashboard/*/dashcard/*/card/*/query").as(
|
||||
"dashcardQuery",
|
||||
);
|
||||
|
||||
cy.task("signJwt", {
|
||||
payload: {
|
||||
email: USERS.normal.email,
|
||||
exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration
|
||||
},
|
||||
secret: JWT_SHARED_SECRET,
|
||||
}).then(jwtToken => {
|
||||
const ssoUrl = new URL("/auth/sso", Cypress.config().baseUrl);
|
||||
ssoUrl.searchParams.set("jwt", jwtToken);
|
||||
ssoUrl.searchParams.set("token", "true");
|
||||
cy.request(ssoUrl.toString()).then(({ body }) => {
|
||||
cy.wrap(body).as("metabaseSsoResponse");
|
||||
});
|
||||
});
|
||||
cy.get("@metabaseSsoResponse").then(ssoResponse => {
|
||||
cy.intercept("GET", "/sso/metabase", ssoResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render dashboard when embedding SDK is not enabled", () => {
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-sdk": false,
|
||||
});
|
||||
cy.signOut();
|
||||
|
||||
cy.get("@dashboardId").then(dashboardId => {
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: EMBEDDING_SDK_STORY_HOST,
|
||||
qs: { id: STORYBOOK_ID, viewMode: "story" },
|
||||
onBeforeLoad: window => {
|
||||
window.METABASE_INSTANCE_URL = Cypress.config().baseUrl;
|
||||
window.DASHBOARD_ID = dashboardId;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
cy.get("#metabase-sdk-root").within(() => {
|
||||
cy.findByText("Error").should("be.visible");
|
||||
cy.findByText(
|
||||
"Could not authenticate: invalid JWT URI or JWT provider did not return a valid JWT token",
|
||||
).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show dashboard content", () => {
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-sdk": true,
|
||||
});
|
||||
cy.signOut();
|
||||
cy.get("@dashboardId").then(dashboardId => {
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: EMBEDDING_SDK_STORY_HOST,
|
||||
qs: { id: STORYBOOK_ID, viewMode: "story" },
|
||||
onBeforeLoad: window => {
|
||||
window.METABASE_INSTANCE_URL = Cypress.config().baseUrl;
|
||||
window.DASHBOARD_ID = dashboardId;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait("@getUser").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
cy.wait("@getDashboard").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
cy.get("#metabase-sdk-root")
|
||||
.should("be.visible")
|
||||
.within(() => {
|
||||
cy.findByText("Embedding Sdk Test Dashboard").should("be.visible"); // dashboard title
|
||||
|
||||
cy.findByText("Text text card").should("be.visible"); // text card content
|
||||
|
||||
cy.wait("@dashcardQuery");
|
||||
cy.findByText("Test question card").should("be.visible"); // question card content
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render the SDK on non localhost sites when embedding SDK origins is not set", () => {
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-sdk": true,
|
||||
});
|
||||
cy.signOut();
|
||||
cy.get("@dashboardId").then(dashboardId => {
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: "http://my-site.local:6006/iframe.html",
|
||||
qs: {
|
||||
id: STORYBOOK_ID,
|
||||
viewMode: "story",
|
||||
},
|
||||
onBeforeLoad: window => {
|
||||
window.METABASE_INSTANCE_URL = Cypress.config().baseUrl;
|
||||
window.DASHBOARD_ID = dashboardId;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
cy.get("#metabase-sdk-root").within(() => {
|
||||
cy.findByText("Error").should("be.visible");
|
||||
cy.findByText(
|
||||
"Could not authenticate: invalid JWT URI or JWT provider did not return a valid JWT token",
|
||||
).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show dashboard content", () => {
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-sdk": true,
|
||||
"embedding-app-origins-sdk": "my-site.local:6006",
|
||||
});
|
||||
cy.signOut();
|
||||
cy.get("@dashboardId").then(dashboardId => {
|
||||
visitFullAppEmbeddingUrl({
|
||||
url: "http://my-site.local:6006/iframe.html",
|
||||
qs: { id: STORYBOOK_ID, viewMode: "story" },
|
||||
onBeforeLoad: window => {
|
||||
window.METABASE_INSTANCE_URL = Cypress.config().baseUrl;
|
||||
window.DASHBOARD_ID = dashboardId;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait("@getUser").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
cy.wait("@getDashboard").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
cy.get("#metabase-sdk-root")
|
||||
.should("be.visible")
|
||||
.within(() => {
|
||||
cy.findByText("Embedding Sdk Test Dashboard").should("be.visible"); // dashboard title
|
||||
|
||||
cy.findByText("Text text card").should("be.visible"); // text card content
|
||||
|
||||
cy.wait("@dashcardQuery");
|
||||
cy.findByText("Test question card").should("be.visible"); // question card content
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,9 @@ describeSDK("scenarios > embedding-sdk > static-dashboard", () => {
|
||||
cy.signInAsAdmin();
|
||||
setTokenFeatures("all");
|
||||
setupJwt();
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-sdk": true,
|
||||
});
|
||||
|
||||
const textCard = getTextCardDetails({ col: 16, text: "Text text card" });
|
||||
const questionCard = {
|
||||
|
||||
@@ -58,23 +58,12 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
cy.location("pathname").should("eq", embeddingPage);
|
||||
cy.findByTestId("enable-embedding-setting").within(() => {
|
||||
cy.findByText(embeddingDescription);
|
||||
|
||||
cy.findByLabelText("Enable Embedding").click({ force: true });
|
||||
});
|
||||
// The URL should stay the same
|
||||
cy.location("pathname").should("eq", embeddingPage);
|
||||
|
||||
cy.findByTestId("enable-embedding-setting").within(() => {
|
||||
cy.findByRole("checkbox").should("be.checked");
|
||||
});
|
||||
|
||||
mainPage().findByText(embeddingDescription).should("be.visible");
|
||||
cy.log(
|
||||
"With the embedding enabled, we should now see two new sections on the main page",
|
||||
);
|
||||
cy.log("The first section: 'Static embedding'");
|
||||
cy.findByTestId("-static-embedding-setting").within(() => {
|
||||
cy.findByRole("article", { name: "Static embedding" }).within(() => {
|
||||
// FE unit tests are making sure this section doesn't exist when a valid token is provided,
|
||||
// so we don't have to do it here usign a conditional logic
|
||||
assertLinkMatchesUrl("upgrade to a paid plan", upgradeUrl);
|
||||
@@ -88,7 +77,12 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
cy.log("Standalone embeds page");
|
||||
|
||||
mainPage().within(() => {
|
||||
cy.findByLabelText("Enable Static embedding")
|
||||
.click({ force: true })
|
||||
.should("be.checked");
|
||||
|
||||
cy.findByTestId("embedding-secret-key-setting").within(() => {
|
||||
cy.findByText("Embedding secret key");
|
||||
cy.findByText(
|
||||
@@ -98,7 +92,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
cy.button("Regenerate key");
|
||||
});
|
||||
|
||||
cy.findByTestId("-embedded-resources-setting").within(() => {
|
||||
cy.findByTestId("embedded-resources").within(() => {
|
||||
cy.findByText("Embedded Dashboards");
|
||||
cy.findByText("No dashboards have been embedded yet.");
|
||||
|
||||
@@ -111,7 +105,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
cy.location("pathname").should("eq", embeddingPage);
|
||||
|
||||
cy.log("The second section: 'Interactive embedding'");
|
||||
cy.findByTestId("-interactive-embedding-setting").within(() => {
|
||||
cy.findByRole("article", { name: "Interactive embedding" }).within(() => {
|
||||
cy.findByText("Interactive embedding");
|
||||
|
||||
cy.findByRole("link", { name: "Learn More" })
|
||||
@@ -310,7 +304,9 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
function resetEmbedding() {
|
||||
cy.request("PUT", "/api/setting/enable-embedding", { value: false });
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: false,
|
||||
});
|
||||
cy.request("PUT", "/api/setting/embedding-secret-key", {
|
||||
value: null,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
(ns metabase.embed.settings-test
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer :all]
|
||||
[metabase.analytics.snowplow-test :as snowplow-test]
|
||||
[metabase.embed.settings :as embed.settings]
|
||||
[metabase.test :as mt]
|
||||
[metabase.util.log.capture :as log.capture]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(deftest enable-embedding-test
|
||||
(testing "A snowplow event is sent whenever embedding is enabled or disabled"
|
||||
(testing "A snowplow event is sent whenever embedding is toggled"
|
||||
(mt/with-test-user :crowberto
|
||||
(mt/with-premium-features #{:embedding}
|
||||
(mt/with-temporary-setting-values [enable-embedding false
|
||||
embedding-app-origin "https://example.com"]
|
||||
(mt/with-temporary-setting-values [embedding-app-origin "https://example.com"
|
||||
enable-embedding false]
|
||||
(let [embedded-dash-count (t2/count :model/Dashboard :enable_embedding true)
|
||||
embedded-card-count (t2/count :model/Card :enable_embedding true)
|
||||
expected-payload {"embedding_app_origin_set" true
|
||||
@@ -19,13 +21,45 @@
|
||||
"number_embedded_dashboards" embedded-dash-count}]
|
||||
(snowplow-test/with-fake-snowplow-collector
|
||||
(embed.settings/enable-embedding! true)
|
||||
(is (= [{:data
|
||||
(merge expected-payload {"event" "embedding_enabled"})
|
||||
(is (= [{:data (merge expected-payload {"event" "embedding_enabled"})
|
||||
:user-id (str (mt/user->id :crowberto))}]
|
||||
(-> (snowplow-test/pop-event-data-and-user-id!))))
|
||||
(snowplow-test/pop-event-data-and-user-id!)))
|
||||
|
||||
(embed.settings/enable-embedding! false)
|
||||
(is (= [{:data
|
||||
(merge expected-payload {"event" "embedding_disabled"})
|
||||
:user-id (str (mt/user->id :crowberto))}]
|
||||
(-> (snowplow-test/pop-event-data-and-user-id!)))))))))))
|
||||
(mt/with-temporary-setting-values [enable-embedding false]
|
||||
(is (= [{:data
|
||||
(merge expected-payload {"event" "embedding_disabled"})
|
||||
:user-id (str (mt/user->id :crowberto))}]
|
||||
(snowplow-test/pop-event-data-and-user-id!)))))))))))
|
||||
|
||||
(deftest deprecation-warning-for-enable-embedding-test
|
||||
(mt/with-temporary-setting-values [enable-embedding "false"]
|
||||
(log.capture/with-log-messages-for-level [warnings :warn]
|
||||
(embed.settings/enable-embedding! true)
|
||||
(is (re-find
|
||||
#"Setting enable-embedding is deprecated as of Metabase 0.51.0 and may be removed in a future version."
|
||||
(str/join " " (map :message (warnings))))))))
|
||||
|
||||
(def ^:private other-ip "1.2.3.4:1234")
|
||||
|
||||
(deftest enable-embedding-SDK-true-ignores-localhosts
|
||||
(mt/with-premium-features #{:embedding :embedding-sdk}
|
||||
(mt/with-temporary-setting-values [enable-embedding-sdk true]
|
||||
(let [origin-value "localhost:*"]
|
||||
(embed.settings/embedding-app-origins-sdk! origin-value)
|
||||
(testing "All localhosty origins should be ignored, so the result should be \"localhost:*\""
|
||||
(embed.settings/embedding-app-origins-sdk! (str origin-value " localhost:8080"))
|
||||
(is (= "localhost:*" (embed.settings/embedding-app-origins-sdk))))
|
||||
(testing "Normal ips are added to the list"
|
||||
(embed.settings/embedding-app-origins-sdk! (str origin-value " " other-ip))
|
||||
(is (= (str "localhost:* " other-ip) (embed.settings/embedding-app-origins-sdk))))))))
|
||||
|
||||
(deftest enable-embedding-SDK-false-returns-nothing
|
||||
(mt/with-premium-features #{:embedding :embedding-sdk}
|
||||
(mt/with-temporary-setting-values [enable-embedding-sdk false]
|
||||
(embed.settings/embedding-app-origins-sdk! "")
|
||||
(let [origin-value (str "localhost:* " other-ip " "
|
||||
(str/join " " (map #(str "localhost:" %) (range 1000 2000))))]
|
||||
(embed.settings/embedding-app-origins-sdk! origin-value)
|
||||
(is (not (and
|
||||
(embed.settings/enable-embedding-sdk)
|
||||
(embed.settings/embedding-app-origins-sdk))))))))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ComponentStory } from "@storybook/react";
|
||||
|
||||
import { StaticDashboard } from "embedding-sdk";
|
||||
import { CommonSdkStoryCorsWrapper } from "embedding-sdk/test/CommonSdkCorsStoryWrapper";
|
||||
|
||||
const DASHBOARD_ID = (window as any).DASHBOARD_ID || "1";
|
||||
|
||||
export default {
|
||||
title: "EmbeddingSDK/CypressStaticDashboardWithCors",
|
||||
component: StaticDashboard,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [CommonSdkStoryCorsWrapper],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof StaticDashboard> = args => {
|
||||
return <StaticDashboard {...args} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Story } from "@storybook/react";
|
||||
|
||||
import { MetabaseProvider, type SDKConfig } from "embedding-sdk";
|
||||
|
||||
const METABASE_INSTANCE_URL =
|
||||
(window as any).METABASE_INSTANCE_URL || "http://localhost:3000";
|
||||
|
||||
const DEFAULT_CONFIG: SDKConfig = {
|
||||
metabaseInstanceUrl: METABASE_INSTANCE_URL,
|
||||
jwtProviderUri: `${METABASE_INSTANCE_URL}/sso/metabase`,
|
||||
};
|
||||
|
||||
export const CommonSdkStoryCorsWrapper = (Story: Story) => (
|
||||
<MetabaseProvider config={DEFAULT_CONFIG}>
|
||||
<Story />
|
||||
</MetabaseProvider>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
if (hasPremiumFeature("embedding_sdk")) {
|
||||
PLUGIN_EMBEDDING_SDK.isEnabled = () => true;
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Button, Group, Icon, Menu, Text } from "metabase/ui";
|
||||
import type { SessionCookieSameSite } from "metabase-types/api";
|
||||
|
||||
const SAME_SITE_OPTIONS: Options[] = [
|
||||
{
|
||||
value: "lax",
|
||||
name: t`Lax (default)`,
|
||||
description: t`Allows cookies to be sent when a user is navigating to the origin site from an external site (like when following a link).`,
|
||||
},
|
||||
{
|
||||
value: "strict",
|
||||
name: t`Strict (not recommended)`,
|
||||
// eslint-disable-next-line no-literal-metabase-strings -- Metabase settings
|
||||
description: t`Never allows cookies to be sent on a cross-site request. Warning: this will prevent users from following external links to Metabase.`,
|
||||
},
|
||||
{
|
||||
value: "none",
|
||||
name: t`None`,
|
||||
description: t`Allows all cross-site requests. Incompatible with most Safari and iOS-based browsers.`,
|
||||
},
|
||||
];
|
||||
|
||||
interface Options {
|
||||
value: SessionCookieSameSite;
|
||||
name: string;
|
||||
@@ -13,20 +33,19 @@ interface SameSiteSelectWidgetProps {
|
||||
onChange: (value: SessionCookieSameSite) => void;
|
||||
setting: {
|
||||
key: "session-cookie-samesite";
|
||||
value?: SessionCookieSameSite;
|
||||
defaultValue: SessionCookieSameSite;
|
||||
options: Options[];
|
||||
value: SessionCookieSameSite | null;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_SAME_SITE_VALUE = "lax";
|
||||
export function SameSiteSelectWidget({
|
||||
setting,
|
||||
onChange,
|
||||
}: SameSiteSelectWidgetProps) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const selectedValue = setting.value ?? setting.defaultValue;
|
||||
const selectedOption = setting.options.find(
|
||||
const selectedValue = setting.value ?? DEFAULT_SAME_SITE_VALUE;
|
||||
const selectedOption = SAME_SITE_OPTIONS.find(
|
||||
({ value }) => value === selectedValue,
|
||||
);
|
||||
|
||||
@@ -47,7 +66,7 @@ export function SameSiteSelectWidget({
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown maw={"21rem"}>
|
||||
{setting.options.map(({ value, name, description }) => (
|
||||
{SAME_SITE_OPTIONS.map(({ value, name, description }) => (
|
||||
<Menu.Item key="value" onClick={() => onChange(value)}>
|
||||
<Text>{name}</Text>
|
||||
<Text c="text-light">{description}</Text>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useEmbeddingSetting } from "metabase/admin/settings/components/EmbeddingSettings/hooks";
|
||||
import SettingHeader from "metabase/admin/settings/components/SettingHeader";
|
||||
import { SetByEnvVarWrapper } from "metabase/admin/settings/components/SettingsSetting";
|
||||
import { SettingTextInput } from "metabase/admin/settings/components/widgets/SettingTextInput";
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
import { EmbeddingAppOriginDescription } from "../EmbeddingAppOriginDescription";
|
||||
|
||||
const INTERACTIVE_EMBEDDING_ORIGINS_SETTING = {
|
||||
key: "embedding-app-origins-interactive",
|
||||
display_name: t`Authorized origins`,
|
||||
description: <EmbeddingAppOriginDescription />,
|
||||
placeholder: "https://*.example.com",
|
||||
} as const;
|
||||
|
||||
export const AuthorizedOrigins = () => {
|
||||
const [originsSetting, handleOriginsChange] = useEmbeddingSetting(
|
||||
INTERACTIVE_EMBEDDING_ORIGINS_SETTING,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SetByEnvVarWrapper setting={originsSetting}>
|
||||
<SettingHeader id={originsSetting.key} setting={originsSetting} />
|
||||
<SettingTextInput
|
||||
id={originsSetting.key}
|
||||
setting={originsSetting}
|
||||
onChange={handleOriginsChange}
|
||||
type="text"
|
||||
/>
|
||||
</SetByEnvVarWrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import {
|
||||
EmbeddingSettingsPageView,
|
||||
EmbeddingSettingsSwitch,
|
||||
} from "metabase/admin/settings/components/EmbeddingSettings";
|
||||
|
||||
import { AuthorizedOrigins } from "./AuthorizedOrigins";
|
||||
import { SameSiteCookieSetting } from "./SameSiteCookieSetting";
|
||||
|
||||
export const InteractiveEmbeddingSettings = () => (
|
||||
<EmbeddingSettingsPageView
|
||||
breadcrumbs={[
|
||||
[t`Embedding`, "/admin/settings/embedding-in-other-applications"],
|
||||
[t`Interactive embedding`],
|
||||
]}
|
||||
>
|
||||
<EmbeddingSettingsSwitch
|
||||
settingKey={"enable-embedding-interactive"}
|
||||
switchLabel={t`Enable Interactive embedding`}
|
||||
/>
|
||||
<AuthorizedOrigins />
|
||||
<SameSiteCookieSetting />
|
||||
</EmbeddingSettingsPageView>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useEmbeddingSetting } from "metabase/admin/settings/components/EmbeddingSettings/hooks";
|
||||
import SettingHeader from "metabase/admin/settings/components/SettingHeader";
|
||||
import { SetByEnvVarWrapper } from "metabase/admin/settings/components/SettingsSetting";
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
import {
|
||||
EmbeddingAppSameSiteCookieDescription,
|
||||
SameSiteSelectWidget,
|
||||
} from "../EmbeddingAppSameSiteCookieDescription";
|
||||
|
||||
const SAME_SITE_SETTING = {
|
||||
key: "session-cookie-samesite",
|
||||
display_name: t`SameSite cookie setting`,
|
||||
description: <EmbeddingAppSameSiteCookieDescription />,
|
||||
widget: SameSiteSelectWidget,
|
||||
} as const;
|
||||
|
||||
export const SameSiteCookieSetting = () => {
|
||||
const [sameSiteSetting, handleChangeSameSite] =
|
||||
useEmbeddingSetting(SAME_SITE_SETTING);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SetByEnvVarWrapper setting={sameSiteSetting}>
|
||||
<SettingHeader id={sameSiteSetting.key} setting={sameSiteSetting} />
|
||||
<SameSiteSelectWidget
|
||||
setting={sameSiteSetting}
|
||||
onChange={handleChangeSameSite}
|
||||
/>
|
||||
</SetByEnvVarWrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./InteractiveEmbeddingSettings";
|
||||
@@ -1,71 +1,14 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import {
|
||||
PLUGIN_ADMIN_SETTINGS_UPDATES,
|
||||
PLUGIN_EMBEDDING,
|
||||
} from "metabase/plugins";
|
||||
import { PLUGIN_ADMIN_SETTINGS, PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import { isInteractiveEmbeddingEnabled } from "metabase-enterprise/embedding/selectors";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
import { EmbeddingAppOriginDescription } from "./components/EmbeddingAppOriginDescription";
|
||||
import {
|
||||
EmbeddingAppSameSiteCookieDescription,
|
||||
SameSiteSelectWidget,
|
||||
} from "./components/EmbeddingAppSameSiteCookieDescription";
|
||||
|
||||
const SLUG = "embedding-in-other-applications/full-app";
|
||||
import { InteractiveEmbeddingSettings } from "./components/InteractiveEmbeddingSettings";
|
||||
|
||||
if (hasPremiumFeature("embedding")) {
|
||||
PLUGIN_EMBEDDING.isEnabled = () => true;
|
||||
PLUGIN_EMBEDDING.isInteractiveEmbeddingEnabled =
|
||||
isInteractiveEmbeddingEnabled;
|
||||
|
||||
PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections => {
|
||||
return {
|
||||
...sections,
|
||||
[SLUG]: {
|
||||
...sections[SLUG],
|
||||
settings: [
|
||||
...sections[SLUG]["settings"],
|
||||
{
|
||||
key: "embedding-app-origin",
|
||||
display_name: t`Authorized origins`,
|
||||
description: <EmbeddingAppOriginDescription />,
|
||||
placeholder: "https://*.example.com",
|
||||
type: "string",
|
||||
getHidden: (_, derivedSettings) =>
|
||||
!derivedSettings["enable-embedding"],
|
||||
},
|
||||
{
|
||||
key: "session-cookie-samesite",
|
||||
display_name: t`SameSite cookie setting`,
|
||||
description: <EmbeddingAppSameSiteCookieDescription />,
|
||||
type: "select",
|
||||
options: [
|
||||
{
|
||||
value: "lax",
|
||||
name: t`Lax (default)`,
|
||||
description: t`Allows cookies to be sent when a user is navigating to the origin site from an external site (like when following a link).`,
|
||||
},
|
||||
{
|
||||
value: "strict",
|
||||
name: t`Strict (not recommended)`,
|
||||
// eslint-disable-next-line no-literal-metabase-strings -- Metabase settings
|
||||
description: t`Never allows cookies to be sent on a cross-site request. Warning: this will prevent users from following external links to Metabase.`,
|
||||
},
|
||||
{
|
||||
value: "none",
|
||||
name: t`None`,
|
||||
description: t`Allows all cross-site requests. Incompatible with most Safari and iOS-based browsers.`,
|
||||
},
|
||||
],
|
||||
defaultValue: "lax",
|
||||
widget: SameSiteSelectWidget,
|
||||
getHidden: (_, derivedSettings) =>
|
||||
!derivedSettings["enable-embedding"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
PLUGIN_ADMIN_SETTINGS.InteractiveEmbeddingSettings =
|
||||
InteractiveEmbeddingSettings;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import "./collections";
|
||||
import "./content_verification";
|
||||
import "./whitelabel";
|
||||
import "./embedding";
|
||||
import "./embedding-sdk";
|
||||
import "./snippets";
|
||||
import "./sharing";
|
||||
import "./moderation";
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface EnterpriseSettings extends Settings {
|
||||
"ee-openai-api-key"?: string;
|
||||
"ee-openai-model"?: string;
|
||||
"saml-user-provisioning-enabled?"?: boolean;
|
||||
"saml-identity-provider-uri"?: boolean;
|
||||
"scim-enabled"?: boolean | null;
|
||||
"scim-base-url"?: string;
|
||||
"send-new-sso-user-admin-email?"?: boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import { AuthTabs } from "metabase/admin/settings/components/AuthTabs";
|
||||
import SettingToggle from "metabase/admin/settings/components/widgets/SettingToggle";
|
||||
import { SettingToggle } from "metabase/admin/settings/components/widgets/SettingToggle";
|
||||
import type { SettingElement } from "metabase/admin/settings/types";
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
|
||||
|
||||
@@ -131,7 +131,7 @@ export const createMockTokenFeatures = (
|
||||
export const createMockSettingDefinition = (
|
||||
opts?: Partial<SettingDefinition>,
|
||||
): SettingDefinition => ({
|
||||
key: "key",
|
||||
key: "active-users-count",
|
||||
env_name: "",
|
||||
is_env_setting: false,
|
||||
value: null,
|
||||
@@ -168,7 +168,12 @@ export const createMockSettings = (
|
||||
"email-smtp-username": null,
|
||||
"email-smtp-password": null,
|
||||
"embedding-app-origin": "",
|
||||
"embedding-app-origins-sdk": "",
|
||||
"embedding-app-origins-interactive": "",
|
||||
"enable-embedding": false,
|
||||
"enable-embedding-static": false,
|
||||
"enable-embedding-sdk": false,
|
||||
"enable-embedding-interactive": false,
|
||||
"enable-enhancements?": false,
|
||||
"enable-nested-queries": true,
|
||||
"expand-browse-in-nav": true,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type {
|
||||
EnterpriseSettingKey,
|
||||
EnterpriseSettings,
|
||||
} from "metabase-enterprise/settings/types";
|
||||
|
||||
export interface FormattingSettings {
|
||||
"type/Temporal"?: DateFormattingSettings;
|
||||
"type/Number"?: NumberFormattingSettings;
|
||||
@@ -181,12 +188,17 @@ export type PasswordComplexity = {
|
||||
|
||||
export type SessionCookieSameSite = "lax" | "strict" | "none";
|
||||
|
||||
export interface SettingDefinition {
|
||||
key: string;
|
||||
export interface SettingDefinition<
|
||||
Key extends EnterpriseSettingKey = EnterpriseSettingKey,
|
||||
> {
|
||||
key: Key;
|
||||
env_name?: string;
|
||||
is_env_setting: boolean;
|
||||
value?: unknown;
|
||||
default?: unknown;
|
||||
value: EnterpriseSettings[Key] | null;
|
||||
default?: EnterpriseSettings[Key];
|
||||
display_name?: string;
|
||||
description?: string | ReactNode;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface OpenAiModel {
|
||||
@@ -210,6 +222,9 @@ interface InstanceSettings {
|
||||
"email-smtp-username": string | null;
|
||||
"email-smtp-password": string | null;
|
||||
"enable-embedding": boolean;
|
||||
"enable-embedding-static": boolean;
|
||||
"enable-embedding-sdk": boolean;
|
||||
"enable-embedding-interactive": boolean;
|
||||
"enable-nested-queries": boolean;
|
||||
"enable-public-sharing": boolean;
|
||||
"enable-xrays": boolean;
|
||||
@@ -290,6 +305,8 @@ interface PublicSettings {
|
||||
"ee-ai-features-enabled"?: boolean;
|
||||
"email-configured?": boolean;
|
||||
"embedding-app-origin": string;
|
||||
"embedding-app-origins-sdk": string | null;
|
||||
"embedding-app-origins-interactive": string | null;
|
||||
"enable-enhancements?": boolean;
|
||||
"enable-password-login": boolean;
|
||||
engines: Record<string, Engine>;
|
||||
|
||||
@@ -1,43 +1,12 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { screen } from "__support__/ui";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
} from "metabase-types/api/mocks";
|
||||
|
||||
import { EMAIL_URL, FULL_APP_EMBEDDING_URL, setup } from "./setup";
|
||||
import { EMAIL_URL, setup } from "./setup";
|
||||
|
||||
describe("SettingsEditor", () => {
|
||||
describe("full-app embedding", () => {
|
||||
it("should show info about interactive embedding", async () => {
|
||||
await setup({
|
||||
settings: [createMockSettingDefinition({ key: "enable-embedding" })],
|
||||
settingValues: createMockSettings({ "enable-embedding": true }),
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Embedding"));
|
||||
await userEvent.click(screen.getByText("Interactive embedding"));
|
||||
expect(screen.queryByText("Authorized origins")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("SameSite cookie setting"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should allow visiting the full-app embedding page even if embedding is not enabled", async () => {
|
||||
await setup({
|
||||
settings: [createMockSettingDefinition({ key: "enable-embedding" })],
|
||||
settingValues: createMockSettings({ "enable-embedding": false }),
|
||||
initialRoute: FULL_APP_EMBEDDING_URL,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/Embed dashboards, questions/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Interactive embedding")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscription allowed domains", () => {
|
||||
it("should not be visible", async () => {
|
||||
await setup({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { screen } from "__support__/ui";
|
||||
import { screen, within } from "__support__/ui";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
@@ -115,5 +115,11 @@ describe("SettingsEditor", () => {
|
||||
});
|
||||
|
||||
const goToInteractiveEmbeddingSettings = async () => {
|
||||
await userEvent.click(screen.getByText("Configure"));
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).getByText("Configure"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,54 +1,439 @@
|
||||
import { act, screen } from "__support__/ui";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { act, screen, within } from "__support__/ui";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
createMockTokenFeatures,
|
||||
} from "metabase-types/api/mocks";
|
||||
|
||||
import { FULL_APP_EMBEDDING_URL, setup } from "../setup";
|
||||
|
||||
import {
|
||||
embeddingSettingsUrl,
|
||||
getQuickStartLink,
|
||||
goToStaticEmbeddingSettings,
|
||||
getInteractiveEmbeddingQuickStartLink,
|
||||
interactiveEmbeddingSettingsUrl,
|
||||
setupEmbedding,
|
||||
staticEmbeddingSettingsUrl,
|
||||
} from "./setup";
|
||||
import type { History } from "./types";
|
||||
|
||||
describe("[OSS] embedding settings", () => {
|
||||
describe("when the embedding is disabled", () => {
|
||||
describe("when static embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding-static": false },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("static embedding", () => {
|
||||
it("should not allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupEmbedding({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Manage" })).toBeDisabled();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText("upgrade to a paid plan"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("link", {
|
||||
name: "upgrade to a paid plan",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embed-settings&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).not.toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is disabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding-sdk": false },
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Disabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEmbedding({
|
||||
settingValues: {
|
||||
"enable-embedding-interactive": false,
|
||||
},
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeDisabled();
|
||||
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
|
||||
// should have a learn more button for interactive embedding
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not allow access to interactive embedding settings page", async () => {
|
||||
act(() => {
|
||||
history.push(staticEmbeddingSettingsUrl);
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
describe("when static embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
settingValues: { "enable-embedding-static": true },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
expect(screen.getByText("upgrade to a paid plan")).toBeInTheDocument();
|
||||
describe("static embedding", () => {
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "upgrade to a paid plan" }),
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText("upgrade to a paid plan"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("link", {
|
||||
name: "upgrade to a paid plan",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embed-settings&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is enabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding-sdk": true },
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding-interactive": true },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should not allow going to interactive settings page", async () => {
|
||||
const { history } = await setupEmbedding({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeDisabled();
|
||||
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
|
||||
// should have a learn more button for interactive embedding
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not allow access to interactive embedding settings page", async () => {
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Configure" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
@@ -58,79 +443,157 @@ describe("[OSS] embedding settings", () => {
|
||||
embeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
|
||||
it("should have a learn more button for interactive embedding", async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Learn More" })).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v0.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getQuickStartLink()).toBeInTheDocument();
|
||||
expect(getQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings", async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("offer multi-tenant, self-service analytics"),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("when the embedding is enabled", () => {
|
||||
it("should allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupEmbedding({
|
||||
settingValues: { "enable-embedding": true },
|
||||
});
|
||||
|
||||
await goToStaticEmbeddingSettings();
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupEmbedding({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v0.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toBeInTheDocument();
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
it("should redirect users back to embedding settings page when visiting the full-app embedding page when embedding is not enabled", async () => {
|
||||
await setup({
|
||||
settings: [createMockSettingDefinition({ key: "enable-embedding" })],
|
||||
settingValues: createMockSettings({ "enable-embedding": false }),
|
||||
tokenFeatures: createMockTokenFeatures({
|
||||
embedding: false,
|
||||
embedding_sdk: false,
|
||||
}),
|
||||
hasEnterprisePlugins: false,
|
||||
initialRoute: FULL_APP_EMBEDDING_URL,
|
||||
});
|
||||
|
||||
it("should not allow going to interactive embedding settings page", async () => {
|
||||
const { history } = await setupEmbedding({
|
||||
settingValues: { "enable-embedding": true },
|
||||
expect(screen.getByText("Static embedding")).toBeInTheDocument();
|
||||
expect(screen.getByText("Embedded analytics SDK")).toBeInTheDocument();
|
||||
expect(screen.getByText("Interactive embedding")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("self-hosted (OSS)", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEmbedding({
|
||||
isHosted: false,
|
||||
settingValues: { "is-hosted?": false },
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Configure" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Embedding SDK settings page", () => {
|
||||
it("should show API key banner", () => {
|
||||
const apiKeyBanner = screen.getByText(
|
||||
/You can test Embedded analytics SDK/,
|
||||
);
|
||||
expect(apiKeyBanner).toHaveTextContent(
|
||||
"You can test Embedded analytics SDK on localhost quickly by using API keys. To use the SDK on other sites, switch Metabase binaries, upgrade to Metabase Pro and implement JWT SSO.",
|
||||
);
|
||||
|
||||
const withinApiKeyBanner = within(apiKeyBanner);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "switch Metabase binaries",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/latest/paid-features/activating-the-enterprise-edition.html?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "upgrade to Metabase Pro",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "implement JWT SSO",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/learn/metabase-basics/embedding/securing-embeds?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show quick start section", () => {
|
||||
expect(
|
||||
screen.getByText("Try Embedded analytics SDK"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Use the SDK with API keys for development."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Check out the Quick Start" }),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-quick-start?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show CORS settings", () => {
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeInTheDocument();
|
||||
const corsSettingDescription = screen.getByText(
|
||||
/Try out the SDK on localhost. To enable other sites/,
|
||||
);
|
||||
expect(corsSettingDescription).toHaveTextContent(
|
||||
"Try out the SDK on localhost. To enable other sites, upgrade to Metabase Pro and Enter the origins for the websites or apps where you want to allow SDK embedding.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(corsSettingDescription).getByRole("link", {
|
||||
name: "upgrade to Metabase Pro",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show documentation link", () => {
|
||||
const documentationText = screen.getByTestId("sdk-documentation");
|
||||
expect(documentationText).toHaveTextContent(
|
||||
"Check out the documentation for more.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(documentationText).getByRole("link", {
|
||||
name: "documentation",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-docs?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show version pinning section", () => {
|
||||
expect(screen.queryByText("Version pinning")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Metabase Cloud instances are automatically upgraded to new releases. SDK packages are strictly compatible with specific version of Metabase. You can request to pin your Metabase to a major version and upgrade your Metabase and SDK dependency in a coordinated fashion.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Request version pinning" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { act, screen } from "__support__/ui";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { act, screen, within } from "__support__/ui";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
createMockTokenFeatures,
|
||||
} from "metabase-types/api/mocks";
|
||||
|
||||
import { FULL_APP_EMBEDDING_URL, setup } from "../setup";
|
||||
|
||||
import type { SetupOpts } from "./setup";
|
||||
import {
|
||||
embeddingSettingsUrl,
|
||||
getQuickStartLink,
|
||||
goToStaticEmbeddingSettings,
|
||||
getInteractiveEmbeddingQuickStartLink,
|
||||
interactiveEmbeddingSettingsUrl,
|
||||
setupEmbedding,
|
||||
staticEmbeddingSettingsUrl,
|
||||
} from "./setup";
|
||||
import type { History } from "./types";
|
||||
|
||||
const setupEnterprise = (opts?: SetupOpts) => {
|
||||
return setupEmbedding({
|
||||
@@ -19,45 +28,419 @@ const setupEnterprise = (opts?: SetupOpts) => {
|
||||
};
|
||||
|
||||
describe("[EE, no token] embedding settings", () => {
|
||||
describe("when the embedding is disabled", () => {
|
||||
describe("when static embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEmbedding({
|
||||
settingValues: { "enable-embedding-static": false },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("static embedding", () => {
|
||||
it("should not allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupEnterprise({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Manage" })).toBeDisabled();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText("upgrade to a paid plan"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("link", {
|
||||
name: "upgrade to a paid plan",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embed-settings&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).not.toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is disabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding-sdk": false },
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Disabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding-interactive": false },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeDisabled();
|
||||
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
|
||||
// should have a learn more button for interactive embedding
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not allow access to interactive embedding settings page", async () => {
|
||||
act(() => {
|
||||
history.push(staticEmbeddingSettingsUrl);
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
describe("when static embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
settingValues: { "enable-embedding-static": true },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
expect(screen.getByText("upgrade to a paid plan")).toBeInTheDocument();
|
||||
describe("static embedding", () => {
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "upgrade to a paid plan" }),
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should prompt to upgrade to remove the Powered by text", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText("upgrade to a paid plan"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("link", {
|
||||
name: "upgrade to a paid plan",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embed-settings&source_plan=oss",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is enabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding-sdk": true },
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding-interactive": true },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should not allow going to interactive settings page", async () => {
|
||||
const { history } = await setupEnterprise({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeDisabled();
|
||||
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
|
||||
// should have a learn more button for interactive embedding
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not allow access to interactive embedding settings page", async () => {
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Configure" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
@@ -67,80 +450,149 @@ describe("[EE, no token] embedding settings", () => {
|
||||
embeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
|
||||
it("should have a learn more button for interactive embedding", async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Learn More" })).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v1.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getQuickStartLink()).toBeInTheDocument();
|
||||
expect(getQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should link to https://www.metabase.com/blog/why-full-app-embedding", async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("offer multi-tenant, self-service analytics"),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("when the embedding is enabled", () => {
|
||||
it("should allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupEnterprise({
|
||||
settingValues: { "enable-embedding": true },
|
||||
});
|
||||
|
||||
await goToStaticEmbeddingSettings();
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupEnterprise({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v0.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toBeInTheDocument();
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=oss&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
it("should redirect users back to embedding settings page when visiting the full-app embedding page when embedding is not enabled", async () => {
|
||||
await setup({
|
||||
settings: [createMockSettingDefinition({ key: "enable-embedding" })],
|
||||
settingValues: createMockSettings({ "enable-embedding": false }),
|
||||
tokenFeatures: createMockTokenFeatures({
|
||||
embedding: false,
|
||||
embedding_sdk: false,
|
||||
}),
|
||||
hasEnterprisePlugins: true,
|
||||
initialRoute: FULL_APP_EMBEDDING_URL,
|
||||
});
|
||||
|
||||
it("should not allow going to interactive embedding settings page", async () => {
|
||||
const { history } = await setupEnterprise({
|
||||
settingValues: { "enable-embedding": true },
|
||||
expect(screen.getByText("Static embedding")).toBeInTheDocument();
|
||||
expect(screen.getByText("Embedded analytics SDK")).toBeInTheDocument();
|
||||
expect(screen.getByText("Interactive embedding")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("cloud (Cloud starter)", () => {
|
||||
beforeEach(async () => {
|
||||
await setupEnterprise({
|
||||
isHosted: true,
|
||||
settingValues: { "is-hosted?": true },
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Configure" }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Try it out" }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Embedding SDK settings page", () => {
|
||||
it("should show API key banner", () => {
|
||||
const apiKeyBanner = screen.getByText(
|
||||
/You can test Embedded analytics SDK/,
|
||||
);
|
||||
expect(apiKeyBanner).toHaveTextContent(
|
||||
"You can test Embedded analytics SDK on localhost quickly by using API keys. To use the SDK on other sites, upgrade to Metabase Pro and implement JWT SSO.",
|
||||
);
|
||||
|
||||
const withinApiKeyBanner = within(apiKeyBanner);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "upgrade to Metabase Pro",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=starter",
|
||||
);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "implement JWT SSO",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/learn/metabase-basics/embedding/securing-embeds?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=starter",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show quick start section", () => {
|
||||
expect(
|
||||
screen.getByText("Try Embedded analytics SDK"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Use the SDK with API keys for development."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Check out the Quick Start" }),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-quick-start?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=starter",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show CORS settings", () => {
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeInTheDocument();
|
||||
const corsSettingDescription = screen.getByText(
|
||||
/Try out the SDK on localhost. To enable other sites/,
|
||||
);
|
||||
expect(corsSettingDescription).toHaveTextContent(
|
||||
"Try out the SDK on localhost. To enable other sites, upgrade to Metabase Pro and Enter the origins for the websites or apps where you want to allow SDK embedding.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(corsSettingDescription).getByRole("link", {
|
||||
name: "upgrade to Metabase Pro",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=starter",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show documentation link", () => {
|
||||
const documentationText = screen.getByTestId("sdk-documentation");
|
||||
expect(documentationText).toHaveTextContent(
|
||||
"Check out the documentation for more.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(documentationText).getByRole("link", {
|
||||
name: "documentation",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-docs?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=starter",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show version pinning section", () => {
|
||||
expect(screen.queryByText("Version pinning")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Metabase Cloud instances are automatically upgraded to new releases. SDK packages are strictly compatible with specific version of Metabase. You can request to pin your Metabase to a major version and upgrade your Metabase and SDK dependency in a coordinated fashion.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Request version pinning" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { act, screen } from "__support__/ui";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { screen, within } from "__support__/ui";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
createMockTokenFeatures,
|
||||
} from "metabase-types/api/mocks";
|
||||
|
||||
import { FULL_APP_EMBEDDING_URL, setup } from "../setup";
|
||||
|
||||
import type { SetupOpts } from "./setup";
|
||||
import {
|
||||
embeddingSettingsUrl,
|
||||
getQuickStartLink,
|
||||
goToInteractiveEmbeddingSettings,
|
||||
goToStaticEmbeddingSettings,
|
||||
getInteractiveEmbeddingQuickStartLink,
|
||||
interactiveEmbeddingSettingsUrl,
|
||||
setupEmbedding,
|
||||
staticEmbeddingSettingsUrl,
|
||||
} from "./setup";
|
||||
import type { History } from "./types";
|
||||
|
||||
const setupPremium = (opts?: SetupOpts) => {
|
||||
fetchMock.put("path:/api/setting/enable-embedding-interactive", 204);
|
||||
return setupEmbedding({
|
||||
...opts,
|
||||
hasEnterprisePlugins: true,
|
||||
@@ -20,92 +29,677 @@ const setupPremium = (opts?: SetupOpts) => {
|
||||
};
|
||||
|
||||
describe("[EE, with token] embedding settings", () => {
|
||||
describe("when the embedding is disabled", () => {
|
||||
describe("when static embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupPremium({
|
||||
settingValues: { "enable-embedding-static": false },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("static embedding", () => {
|
||||
it("should not allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupPremium({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Manage" }),
|
||||
).toBeDisabled();
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
history.push(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should not prompt to upgrade to remove the Powered by text", async () => {
|
||||
await setupPremium({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("upgrade to a paid plan"),
|
||||
withinStaticEmbeddingCard.queryByText("upgrade to a paid plan"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).not.toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is disabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding-sdk": false,
|
||||
"embedding-app-origins-sdk": "metabase-sdk.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Disabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Configure" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toHaveValue("metabase-sdk.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is disabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding-interactive": false,
|
||||
"embedding-app-origins-interactive": "localhost:9999",
|
||||
"session-cookie-samesite": "strict",
|
||||
},
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should not allow going to interactive settings page", async () => {
|
||||
const { history } = await setupPremium({
|
||||
settingValues: { "enable-embedding": false },
|
||||
});
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Configure" }),
|
||||
).toBeDisabled();
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Disabled"),
|
||||
).toBeEnabled();
|
||||
|
||||
act(() => {
|
||||
history.push(interactiveEmbeddingSettingsUrl);
|
||||
});
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
embeddingSettingsUrl,
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=pro-self-hosted&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=pro-self-hosted&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v1.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getQuickStartLink()).toBeInTheDocument();
|
||||
expect(getQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=pro-self-hosted&utm_media=embed-settings",
|
||||
it("should allow access to interactive embedding settings page", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.queryByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
withinInteractiveEmbeddingCard.getByRole("button", {
|
||||
name: "Configure",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
).not.toBeChecked();
|
||||
await userEvent.click(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Authorized origins")).toBeEnabled();
|
||||
expect(screen.getByLabelText("Authorized origins")).toHaveValue(
|
||||
"localhost:9999",
|
||||
);
|
||||
|
||||
expect(screen.getByText("SameSite cookie setting")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Strict (not recommended)"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
interactiveEmbeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("when the embedding is enabled", () => {
|
||||
it("should allow going to static embedding settings page", async () => {
|
||||
const { history } = await setupPremium({
|
||||
settingValues: { "enable-embedding": true },
|
||||
});
|
||||
|
||||
await goToStaticEmbeddingSettings();
|
||||
describe("when static embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupPremium({
|
||||
settingValues: { "enable-embedding-static": true },
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
it("should allow going to interactive embedding settings page", async () => {
|
||||
const { history } = await setupPremium({
|
||||
settingValues: { "enable-embedding": true },
|
||||
describe("static embedding", () => {
|
||||
it("should show info about static embedding", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByRole("heading", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByText(/Use static embedding when/),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinStaticEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
await goToInteractiveEmbeddingSettings();
|
||||
it("should not prompt to upgrade to remove the Powered by text", async () => {
|
||||
const withinStaticEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinStaticEmbeddingCard.queryByText("upgrade to a paid plan"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(interactiveEmbeddingSettingsUrl);
|
||||
it("should allow access to static embedding settings page", async () => {
|
||||
// Go to static embedding settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Static embedding",
|
||||
}),
|
||||
).getByRole("button", { name: "Manage" }),
|
||||
);
|
||||
|
||||
const staticEmbeddingToggle = screen.getByLabelText(
|
||||
"Enable Static embedding",
|
||||
);
|
||||
expect(staticEmbeddingToggle).toBeEnabled();
|
||||
expect(staticEmbeddingToggle).toBeChecked();
|
||||
|
||||
expect(screen.getByText("Embedding secret key")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage embeds")).toBeInTheDocument();
|
||||
|
||||
const location = history.getCurrentLocation();
|
||||
expect(location.pathname).toEqual(staticEmbeddingSettingsUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when embedding SDK is enabled", () => {
|
||||
beforeEach(async () => {
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding-sdk": true,
|
||||
"embedding-app-origins-sdk": "metabase-sdk.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding SDK", () => {
|
||||
it("should show info about embedding SDK", async () => {
|
||||
const withinEmbeddingSdkCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByRole("heading", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByText("Beta")).toBeInTheDocument();
|
||||
expect(
|
||||
withinEmbeddingSdkCard.getByText(
|
||||
/Interactive embedding with full, granular control./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeChecked();
|
||||
expect(withinEmbeddingSdkCard.getByLabelText("Enabled")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should allow access to embedding SDK settings page", async () => {
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Configure" }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Embedded analytics SDK"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toHaveValue("metabase-sdk.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when interactive embedding is enabled", () => {
|
||||
let history: History;
|
||||
|
||||
beforeEach(async () => {
|
||||
history = (
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding-interactive": true,
|
||||
"embedding-app-origins-interactive": "localhost:9999",
|
||||
"session-cookie-samesite": "strict",
|
||||
},
|
||||
})
|
||||
).history;
|
||||
});
|
||||
|
||||
describe("interactive embedding", () => {
|
||||
it("should show info about interactive embedding", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByRole("heading", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
/Use interactive embedding when/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByLabelText("Enabled"),
|
||||
).toBeEnabled();
|
||||
|
||||
// should link to https://www.metabase.com/blog/why-full-app-embedding?utm_source=pro-self-hosted&utm_media=embed-settings
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.getByText(
|
||||
"offer multi-tenant, self-service analytics",
|
||||
),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/blog/why-full-app-embedding?utm_source=pro-self-hosted&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow access to interactive embedding settings page", async () => {
|
||||
const withinInteractiveEmbeddingCard = within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
withinInteractiveEmbeddingCard.queryByRole("link", {
|
||||
name: "Learn More",
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
withinInteractiveEmbeddingCard.getByRole("button", {
|
||||
name: "Configure",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
).toBeChecked();
|
||||
await userEvent.click(
|
||||
screen.getByLabelText("Enable Interactive embedding"),
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Authorized origins")).toBeEnabled();
|
||||
expect(screen.getByLabelText("Authorized origins")).toHaveValue(
|
||||
"localhost:9999",
|
||||
);
|
||||
|
||||
expect(screen.getByText("SameSite cookie setting")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Strict (not recommended)"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(history.getCurrentLocation().pathname).toEqual(
|
||||
interactiveEmbeddingSettingsUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should link to quickstart for interactive embedding", async () => {
|
||||
await setupPremium({
|
||||
settingValues: {
|
||||
"enable-embedding": false,
|
||||
version: { tag: "v1.49.3" },
|
||||
},
|
||||
});
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toBeInTheDocument();
|
||||
expect(getInteractiveEmbeddingQuickStartLink()).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/docs/v0.49/embedding/interactive-embedding-quick-start-guide.html?utm_source=pro-self-hosted&utm_media=embed-settings",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not redirect users back to embedding settings page when visiting the full-app embedding page when embedding is not enabled", async () => {
|
||||
await setup({
|
||||
settings: [createMockSettingDefinition({ key: "enable-embedding" })],
|
||||
settingValues: createMockSettings({ "enable-embedding": false }),
|
||||
tokenFeatures: createMockTokenFeatures({
|
||||
embedding: false,
|
||||
embedding_sdk: false,
|
||||
}),
|
||||
hasEnterprisePlugins: true,
|
||||
initialRoute: FULL_APP_EMBEDDING_URL,
|
||||
});
|
||||
|
||||
expect(screen.queryByText("Static embedding")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Embedded analytics SDK"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("self-hosted (pro)", () => {
|
||||
beforeEach(async () => {
|
||||
await setupPremium({
|
||||
isHosted: false,
|
||||
settingValues: { "is-hosted?": false },
|
||||
});
|
||||
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Configure" }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Embedding SDK settings page", () => {
|
||||
it("should show API key banner", () => {
|
||||
const apiKeyBanner = screen.getByText(
|
||||
/You can test Embedded analytics SDK/,
|
||||
);
|
||||
expect(apiKeyBanner).toHaveTextContent(
|
||||
"You can test Embedded analytics SDK on localhost quickly by using API keys. To use the SDK on other sites, implement JWT SSO.",
|
||||
);
|
||||
|
||||
const withinApiKeyBanner = within(apiKeyBanner);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "implement JWT SSO",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/learn/metabase-basics/embedding/securing-embeds?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-self-hosted",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show quick start section", () => {
|
||||
expect(screen.getByText("Get started")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Use the SDK with API keys for development."),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Check out the Quick Start" }),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-quick-start?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-self-hosted",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show CORS settings", () => {
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Enter the origins for the websites or apps where you want to allow SDK embedding, separated by a space. Localhost is automatically included.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show documentation link", () => {
|
||||
const documentationText = screen.getByTestId("sdk-documentation");
|
||||
expect(documentationText).toHaveTextContent(
|
||||
"Check out the documentation for more.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(documentationText).getByRole("link", {
|
||||
name: "documentation",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-docs?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-self-hosted",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show version pinning section", () => {
|
||||
expect(screen.queryByText("Version pinning")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Metabase Cloud instances are automatically upgraded to new releases. SDK packages are strictly compatible with specific version of Metabase. You can request to pin your Metabase to a major version and upgrade your Metabase and SDK dependency in a coordinated fashion.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Request version pinning" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cloud (Pro)", () => {
|
||||
beforeEach(async () => {
|
||||
await setupPremium({
|
||||
isHosted: true,
|
||||
settingValues: { "is-hosted?": true },
|
||||
});
|
||||
|
||||
// Go to embedding SDK settings page
|
||||
await userEvent.click(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Embedded analytics SDK",
|
||||
}),
|
||||
).getByRole("button", { name: "Configure" }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Embedding SDK settings page", () => {
|
||||
it("should show API key banner", () => {
|
||||
const apiKeyBanner = screen.getByText(
|
||||
/You can test Embedded analytics SDK/,
|
||||
);
|
||||
expect(apiKeyBanner).toHaveTextContent(
|
||||
"You can test Embedded analytics SDK on localhost quickly by using API keys. To use the SDK on other sites, implement JWT SSO.",
|
||||
);
|
||||
|
||||
const withinApiKeyBanner = within(apiKeyBanner);
|
||||
expect(
|
||||
withinApiKeyBanner.getByRole("link", {
|
||||
name: "implement JWT SSO",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://www.metabase.com/learn/metabase-basics/embedding/securing-embeds?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-cloud",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show quick start section", () => {
|
||||
expect(screen.getByText("Get started")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Use the SDK with API keys for development."),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Check out the Quick Start" }),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-quick-start?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-cloud",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show CORS settings", () => {
|
||||
expect(
|
||||
screen.getByLabelText("Cross-Origin Resource Sharing (CORS)"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Enter the origins for the websites or apps where you want to allow SDK embedding, separated by a space. Localhost is automatically included.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show documentation link", () => {
|
||||
const documentationText = screen.getByTestId("sdk-documentation");
|
||||
expect(documentationText).toHaveTextContent(
|
||||
"Check out the documentation for more.",
|
||||
);
|
||||
|
||||
expect(
|
||||
within(documentationText).getByRole("link", {
|
||||
name: "documentation",
|
||||
}),
|
||||
).toHaveProperty(
|
||||
"href",
|
||||
"https://metaba.se/sdk-docs?utm_source=product&utm_medium=docs&utm_campaign=embedding-sdk&utm_content=embedding-sdk-admin&source_plan=pro-cloud",
|
||||
);
|
||||
});
|
||||
|
||||
it("should show version pinning section", () => {
|
||||
expect(screen.getByText("Version pinning")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Metabase Cloud instances are automatically upgraded to new releases. SDK packages are strictly compatible with specific version of Metabase. You can request to pin your Metabase to a major version and upgrade your Metabase and SDK dependency in a coordinated fashion.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Request version pinning" }),
|
||||
).toHaveProperty("href", "mailto:help@metabase.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { screen } from "__support__/ui";
|
||||
import { screen, within } from "__support__/ui";
|
||||
import { checkNotNull } from "metabase/lib/types";
|
||||
import type { Settings } from "metabase-types/api";
|
||||
import type { SettingKey, Settings } from "metabase-types/api";
|
||||
import {
|
||||
createMockSettingDefinition,
|
||||
createMockSettings,
|
||||
createMockTokenFeatures,
|
||||
} from "metabase-types/api/mocks";
|
||||
@@ -13,19 +14,29 @@ import { setup } from "../setup";
|
||||
|
||||
export type SetupOpts = {
|
||||
settingValues?: Partial<Settings>;
|
||||
isHosted?: boolean;
|
||||
hasEmbeddingFeature?: boolean;
|
||||
hasEnterprisePlugins?: boolean;
|
||||
};
|
||||
|
||||
export const setupEmbedding = async ({
|
||||
settingValues,
|
||||
isHosted = false,
|
||||
hasEmbeddingFeature = false,
|
||||
hasEnterprisePlugins = false,
|
||||
}: SetupOpts) => {
|
||||
const returnedValue = await setup({
|
||||
settings: Object.entries(settingValues ?? {}).map(([key, value]) => {
|
||||
return createMockSettingDefinition({
|
||||
key: key as SettingKey,
|
||||
value,
|
||||
});
|
||||
}),
|
||||
settingValues: createMockSettings(settingValues),
|
||||
tokenFeatures: createMockTokenFeatures({
|
||||
hosting: isHosted,
|
||||
embedding: hasEmbeddingFeature,
|
||||
embedding_sdk: hasEmbeddingFeature,
|
||||
}),
|
||||
hasEnterprisePlugins,
|
||||
});
|
||||
@@ -38,16 +49,12 @@ export const setupEmbedding = async ({
|
||||
return { ...returnedValue, history: checkNotNull(returnedValue.history) };
|
||||
};
|
||||
|
||||
export const goToStaticEmbeddingSettings = async () => {
|
||||
await userEvent.click(screen.getByText("Manage"));
|
||||
};
|
||||
|
||||
export const goToInteractiveEmbeddingSettings = async () => {
|
||||
await userEvent.click(screen.getByText("Configure"));
|
||||
};
|
||||
|
||||
export const getQuickStartLink = () => {
|
||||
return screen.getByRole("link", { name: "Check out our Quick Start" });
|
||||
export const getInteractiveEmbeddingQuickStartLink = () => {
|
||||
return within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).getByRole("link", { name: "Check out our Quick Start" });
|
||||
};
|
||||
|
||||
export const embeddingSettingsUrl =
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { setupEmbedding } from "./setup";
|
||||
|
||||
export type History = Awaited<ReturnType<typeof setupEmbedding>>["history"];
|
||||
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { setupGroupsEndpoint } from "__support__/server-mocks";
|
||||
import { screen } from "__support__/ui";
|
||||
import { screen, within } from "__support__/ui";
|
||||
import {
|
||||
createMockGroup,
|
||||
createMockSettingDefinition,
|
||||
@@ -48,7 +48,14 @@ describe("SettingsEditor", () => {
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Embedding"));
|
||||
await userEvent.click(screen.getByText("Interactive embedding"));
|
||||
expect(
|
||||
within(
|
||||
screen.getByRole("article", {
|
||||
name: "Interactive embedding",
|
||||
}),
|
||||
).getByRole("link", { name: "Learn More" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText("Authorized origins")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("SameSite cookie setting"),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
import SettingHeader from "../../SettingHeader";
|
||||
import { SetByEnvVarWrapper } from "../../SettingsSetting";
|
||||
import { SettingTextInput } from "../../widgets/SettingTextInput";
|
||||
import { useEmbeddingSetting, useMergeSetting } from "../hooks";
|
||||
|
||||
import { useEmbeddingSettingsLinks } from "./sdk";
|
||||
|
||||
export const CorsOriginInput = () => {
|
||||
const isEE = PLUGIN_EMBEDDING_SDK.isEnabled();
|
||||
const enableEmbeddingSdkSetting = useMergeSetting({
|
||||
key: "enable-embedding-sdk",
|
||||
});
|
||||
const isEmbeddingSdkEnabled = Boolean(enableEmbeddingSdkSetting.value);
|
||||
const canEditSdkOrigins = isEE && isEmbeddingSdkEnabled;
|
||||
|
||||
const { upgradeUrl } = useEmbeddingSettingsLinks();
|
||||
|
||||
const [sdkOriginsSetting, handleChangeSdkOrigins] = useEmbeddingSetting(
|
||||
!isEE
|
||||
? {
|
||||
key: "embedding-app-origins-sdk",
|
||||
placeholder: "https://*.example.com",
|
||||
display_name: t`Cross-Origin Resource Sharing (CORS)`,
|
||||
description: jt`Try out the SDK on localhost. To enable other sites, ${(
|
||||
<ExternalLink key="upgrade-url" href={upgradeUrl}>
|
||||
{t`upgrade to Metabase Pro`}
|
||||
</ExternalLink>
|
||||
)} and Enter the origins for the websites or apps where you want to allow SDK embedding.`,
|
||||
}
|
||||
: {
|
||||
key: "embedding-app-origins-sdk",
|
||||
placeholder: "https://*.example.com",
|
||||
display_name: t`Cross-Origin Resource Sharing (CORS)`,
|
||||
description: t`Enter the origins for the websites or apps where you want to allow SDK embedding, separated by a space. Localhost is automatically included.`,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SetByEnvVarWrapper setting={sdkOriginsSetting}>
|
||||
<SettingHeader id={sdkOriginsSetting.key} setting={sdkOriginsSetting} />
|
||||
<SettingTextInput
|
||||
id={sdkOriginsSetting.key}
|
||||
setting={sdkOriginsSetting}
|
||||
onChange={handleChangeSdkOrigins}
|
||||
type="text"
|
||||
disabled={!canEditSdkOrigins}
|
||||
/>
|
||||
</SetByEnvVarWrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { Text } from "metabase/ui";
|
||||
|
||||
import { useEmbeddingSettingsLinks } from "./sdk";
|
||||
|
||||
export const DocumentationCta = () => {
|
||||
const { documentationUrl } = useEmbeddingSettingsLinks();
|
||||
|
||||
return (
|
||||
<Text data-testid="sdk-documentation">
|
||||
{jt`Check out the ${(
|
||||
<ExternalLink key="sdk-doc" href={documentationUrl}>
|
||||
{t`documentation`}
|
||||
</ExternalLink>
|
||||
)} for more.`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
|
||||
|
||||
import { EmbeddingSettingsPageView } from "../EmbeddingSettingsPageView";
|
||||
import { EmbeddingSettingsSwitch } from "../EmbeddingSettingsSwitch";
|
||||
|
||||
import { CorsOriginInput } from "./CorsOriginInput";
|
||||
import { DocumentationCta } from "./DocumentationCta";
|
||||
import { GetStarted } from "./GetStarted";
|
||||
import { SdkInfoAlert } from "./SdkInfoAlert";
|
||||
import { VersionPinning } from "./VersionPinning";
|
||||
|
||||
export const EmbeddingSdkSettings = () => {
|
||||
const isEE = PLUGIN_EMBEDDING_SDK.isEnabled();
|
||||
const isHosted = useSetting("is-hosted?");
|
||||
|
||||
return (
|
||||
<EmbeddingSettingsPageView
|
||||
breadcrumbs={[
|
||||
[t`Embedding`, "/admin/settings/embedding-in-other-applications"],
|
||||
[t`Embedding SDK for React`],
|
||||
]}
|
||||
>
|
||||
<EmbeddingSettingsSwitch
|
||||
settingKey={"enable-embedding-sdk"}
|
||||
switchLabel={t`Enable Embedded analytics SDK`}
|
||||
/>
|
||||
<SdkInfoAlert />
|
||||
<GetStarted />
|
||||
<CorsOriginInput />
|
||||
{isEE && isHosted && <VersionPinning />}
|
||||
<DocumentationCta />
|
||||
</EmbeddingSettingsPageView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
|
||||
import { Box, Button } from "metabase/ui";
|
||||
|
||||
import SettingHeader from "../../SettingHeader";
|
||||
|
||||
import { useEmbeddingSettingsLinks } from "./sdk";
|
||||
|
||||
export const GetStarted = () => {
|
||||
const isEE = PLUGIN_EMBEDDING_SDK.isEnabled();
|
||||
|
||||
const { quickStartUrl } = useEmbeddingSettingsLinks();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SettingHeader
|
||||
id="get-started"
|
||||
setting={
|
||||
isEE
|
||||
? {
|
||||
display_name: t`Get started`,
|
||||
}
|
||||
: {
|
||||
display_name: t`Try Embedded analytics SDK`,
|
||||
description: t`Use the SDK with API keys for development.`,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
component={ExternalLink}
|
||||
href={quickStartUrl}
|
||||
>{t`Check out the Quick Start`}</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { match } from "ts-pattern";
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
|
||||
import { Alert, Icon, Text } from "metabase/ui";
|
||||
|
||||
import { useEmbeddingSettingsLinks } from "./sdk";
|
||||
|
||||
export const SdkInfoAlert = () => {
|
||||
const { implementJwtUrl, switchMetabaseBinariesUrl, upgradeUrl } =
|
||||
useEmbeddingSettingsLinks();
|
||||
const isHosted = useSetting("is-hosted?");
|
||||
const isEE = PLUGIN_EMBEDDING_SDK.isEnabled();
|
||||
|
||||
const baseText = t`You can test Embedded analytics SDK on localhost quickly by using API keys. To use the SDK on other sites, `;
|
||||
const upgradeLink = (
|
||||
<ExternalLink href={upgradeUrl}>{t`upgrade to Metabase Pro`}</ExternalLink>
|
||||
);
|
||||
const jwtLink = (
|
||||
<ExternalLink href={implementJwtUrl}>{t`implement JWT SSO`}</ExternalLink>
|
||||
);
|
||||
|
||||
const alertText = match({ isEE, isHosted })
|
||||
.with({ isEE: false, isHosted: false }, () => {
|
||||
const switchLink = (
|
||||
<ExternalLink
|
||||
href={switchMetabaseBinariesUrl}
|
||||
>{t`switch Metabase binaries`}</ExternalLink>
|
||||
);
|
||||
return jt`${baseText}${switchLink}, ${upgradeLink} and ${jwtLink}.`;
|
||||
})
|
||||
.with(
|
||||
{ isEE: false, isHosted: true },
|
||||
() => jt`${baseText}${upgradeLink} and ${jwtLink}.`,
|
||||
)
|
||||
.otherwise(() => jt`${baseText}${jwtLink}.`);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
icon={<Icon color="var(--mb-color-text-secondary)" name="info_filled" />}
|
||||
bg="var(--mb-color-background-info)"
|
||||
style={{ borderColor: "var(--mb-color-border)" }}
|
||||
variant="outline"
|
||||
px="lg"
|
||||
py="md"
|
||||
maw={620}
|
||||
>
|
||||
<Text size="sm">{alertText}</Text>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { Box, Button, Icon } from "metabase/ui";
|
||||
|
||||
import SettingHeader from "../../SettingHeader";
|
||||
|
||||
export const VersionPinning = () => (
|
||||
<Box>
|
||||
<SettingHeader
|
||||
id="version-pinning"
|
||||
setting={{
|
||||
display_name: t`Version pinning`,
|
||||
description: t`Metabase Cloud instances are automatically upgraded to new releases. SDK packages are strictly compatible with specific version of Metabase. You can request to pin your Metabase to a major version and upgrade your Metabase and SDK dependency in a coordinated fashion.`,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
compact
|
||||
variant="outline"
|
||||
leftIcon={<Icon size={12} name="mail" aria-hidden />}
|
||||
component={ExternalLink}
|
||||
fz="0.75rem"
|
||||
href="mailto:help@metabase.com"
|
||||
>{t`Request version pinning`}</Button>
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./EmbeddingSdkSettings";
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useDocsUrl } from "metabase/common/hooks";
|
||||
import { getPlan } from "metabase/common/utils/plan";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import {
|
||||
getLearnUrl,
|
||||
getSetting,
|
||||
getUpgradeUrl,
|
||||
} from "metabase/selectors/settings";
|
||||
|
||||
export const useEmbeddingSettingsLinks = () => {
|
||||
const plan = useSelector(state =>
|
||||
getPlan(getSetting(state, "token-features")),
|
||||
);
|
||||
|
||||
const addUtmToLink = (url: string) =>
|
||||
`${url}?${new URLSearchParams({
|
||||
utm_source: "product",
|
||||
utm_medium: "docs",
|
||||
utm_campaign: "embedding-sdk",
|
||||
utm_content: "embedding-sdk-admin",
|
||||
source_plan: plan,
|
||||
})}`;
|
||||
|
||||
const quickStartUrl = addUtmToLink("https://metaba.se/sdk-quick-start");
|
||||
|
||||
const { url: activationUrl } = useDocsUrl(
|
||||
"paid-features/activating-the-enterprise-edition",
|
||||
);
|
||||
|
||||
const switchMetabaseBinariesUrl = addUtmToLink(activationUrl);
|
||||
|
||||
const implementJwtUrl = addUtmToLink(
|
||||
getLearnUrl("metabase-basics/embedding/securing-embeds"),
|
||||
);
|
||||
|
||||
const documentationUrl = addUtmToLink("https://metaba.se/sdk-docs");
|
||||
|
||||
const upgradeUrl = useSelector(state =>
|
||||
getUpgradeUrl(state, {
|
||||
utm_campaign: "embedding-sdk",
|
||||
utm_content: "embedding-sdk-admin",
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
quickStartUrl,
|
||||
switchMetabaseBinariesUrl,
|
||||
implementJwtUrl,
|
||||
documentationUrl,
|
||||
upgradeUrl,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Box, Stack, Text } from "metabase/ui";
|
||||
|
||||
import SettingHeader from "../SettingHeader";
|
||||
import {
|
||||
EmbeddingSdkOptionCard,
|
||||
InteractiveEmbeddingOptionCard,
|
||||
StaticEmbeddingOptionCard,
|
||||
} from "../widgets/EmbeddingOption";
|
||||
|
||||
export const EmbeddingSettings = () => (
|
||||
<Stack spacing="2.5rem" px="md" pt="sm">
|
||||
<Box data-testid="enable-embedding-setting">
|
||||
<SettingHeader
|
||||
id="enable-embedding"
|
||||
setting={{
|
||||
display_name: t`Embedding`,
|
||||
}}
|
||||
/>
|
||||
<Stack spacing={"md"} className={CS.textMeasure}>
|
||||
<Text lh={1.5}>
|
||||
{t`Embed dashboards, questions, or the entire Metabase app into your application. Integrate with your server code to create a secure environment, limited to specific users or organizations.`}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<StaticEmbeddingOptionCard />
|
||||
<EmbeddingSdkOptionCard />
|
||||
<InteractiveEmbeddingOptionCard />
|
||||
</Stack>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import Breadcrumbs from "metabase/components/Breadcrumbs";
|
||||
import { Stack } from "metabase/ui";
|
||||
|
||||
export const EmbeddingSettingsPageView = ({
|
||||
children,
|
||||
breadcrumbs,
|
||||
}: PropsWithChildren<{
|
||||
breadcrumbs: string[][];
|
||||
}>) => (
|
||||
<Stack spacing="2.5rem" px="md" pt="sm">
|
||||
<Breadcrumbs size="large" crumbs={breadcrumbs} />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Switch, Text } from "metabase/ui";
|
||||
import type { SettingKey } from "metabase-types/api";
|
||||
|
||||
import { useEmbeddingSetting } from "../hooks";
|
||||
|
||||
export const EmbeddingSettingsSwitch = ({
|
||||
settingKey,
|
||||
switchLabel,
|
||||
}: {
|
||||
settingKey: SettingKey;
|
||||
switchLabel: string;
|
||||
}) => {
|
||||
const [setting, handleChange] = useEmbeddingSetting({ key: settingKey });
|
||||
const isSettingEnabled = Boolean(setting.value);
|
||||
|
||||
return setting.is_env_setting ? (
|
||||
<Text color="var(--mb-color-text-secondary)">{t`Set via environment variable`}</Text>
|
||||
) : (
|
||||
<Switch
|
||||
label={switchLabel}
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
checked={isSettingEnabled}
|
||||
onChange={event => handleChange(event.currentTarget.checked)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./EmbeddingSettingsSwitch";
|
||||
@@ -0,0 +1,5 @@
|
||||
import RedirectWidget from "../../widgets/RedirectWidget";
|
||||
|
||||
export const InteractiveEmbeddingSettings = () => (
|
||||
<RedirectWidget to="/admin/settings/embedding-in-other-applications" />
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./InteractiveEmbeddingSettings";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
import { SettingTitle } from "../../SettingHeader/SettingHeader.styled";
|
||||
import { EmbeddedResources } from "../../widgets/PublicLinksListing";
|
||||
|
||||
export const ManageEmbeds = () => (
|
||||
<Box data-testid="embedded-resources">
|
||||
<SettingTitle>{t`Manage embeds`}</SettingTitle>
|
||||
<EmbeddedResources />
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
import SettingHeader from "../../SettingHeader";
|
||||
import { SetByEnvVarWrapper } from "../../SettingsSetting";
|
||||
import SecretKeyWidget from "../../widgets/SecretKeyWidget";
|
||||
import { useEmbeddingSetting } from "../hooks";
|
||||
|
||||
const EMBEDDING_SECRET_KEY_SETTING = {
|
||||
key: "embedding-secret-key",
|
||||
display_name: t`Embedding secret key`,
|
||||
description: t`Standalone Embed Secret Key used to sign JSON Web Tokens for requests to /api/embed endpoints. This lets you create a secure environment limited to specific users or organizations.`,
|
||||
} as const;
|
||||
|
||||
export const ManageSecretKey = () => {
|
||||
const [embeddingSecretKeySetting, handleChangeEmbeddingSecretKey] =
|
||||
useEmbeddingSetting(EMBEDDING_SECRET_KEY_SETTING);
|
||||
|
||||
return (
|
||||
<Box data-testid="embedding-secret-key-setting">
|
||||
<SetByEnvVarWrapper setting={embeddingSecretKeySetting}>
|
||||
<SettingHeader
|
||||
id="setting-embedding-secret-key"
|
||||
setting={embeddingSecretKeySetting}
|
||||
/>
|
||||
<SecretKeyWidget
|
||||
id="setting-embedding-secret-key"
|
||||
onChange={handleChangeEmbeddingSecretKey}
|
||||
setting={embeddingSecretKeySetting}
|
||||
confirmation={{
|
||||
header: t`Regenerate embedding key?`,
|
||||
dialog: t`This will cause existing embeds to stop working until they are updated with the new key.`,
|
||||
}}
|
||||
/>
|
||||
</SetByEnvVarWrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { EmbeddingSettingsPageView } from "../EmbeddingSettingsPageView";
|
||||
import { EmbeddingSettingsSwitch } from "../EmbeddingSettingsSwitch";
|
||||
|
||||
import { ManageEmbeds } from "./ManageEmbeds";
|
||||
import { ManageSecretKey } from "./ManageSecretKey";
|
||||
|
||||
export function StaticEmbeddingSettings() {
|
||||
return (
|
||||
<EmbeddingSettingsPageView
|
||||
breadcrumbs={[
|
||||
[t`Embedding`, "/admin/settings/embedding-in-other-applications"],
|
||||
[t`Static embedding`],
|
||||
]}
|
||||
>
|
||||
<EmbeddingSettingsSwitch
|
||||
settingKey="enable-embedding-static"
|
||||
switchLabel={t`Enable Static embedding`}
|
||||
/>
|
||||
<ManageSecretKey />
|
||||
<ManageEmbeds />
|
||||
</EmbeddingSettingsPageView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./StaticEmbeddingSettings";
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useDispatch, useSelector } from "metabase/lib/redux";
|
||||
import type {
|
||||
SettingDefinition,
|
||||
SettingKey,
|
||||
Settings,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import { updateSetting } from "../../settings";
|
||||
|
||||
type DisplaySetting<Key extends SettingKey> = {
|
||||
key: Key;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type SettingWithValue<Key extends SettingKey> = {
|
||||
key: Key;
|
||||
value: Settings[Key];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function useMergeSetting<Key extends SettingKey>(
|
||||
displaySetting: DisplaySetting<Key>,
|
||||
): SettingDefinition<Key> {
|
||||
const apiSetting = useSelector(state => state.admin.settings.settings).find(
|
||||
setting => setting.key === displaySetting.key,
|
||||
) as SettingDefinition<Key>;
|
||||
const mergedSetting = useMemo(() => {
|
||||
return {
|
||||
...apiSetting,
|
||||
...displaySetting,
|
||||
};
|
||||
}, [apiSetting, displaySetting]);
|
||||
|
||||
return mergedSetting;
|
||||
}
|
||||
|
||||
export const useEmbeddingSetting = <Key extends SettingKey>(
|
||||
displaySetting: DisplaySetting<Key>,
|
||||
): [
|
||||
SettingDefinition<Key>,
|
||||
(value: SettingWithValue<Key>["value"]) => void,
|
||||
] => {
|
||||
const mergedSetting = useMergeSetting(displaySetting);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSettingChange = (value: SettingWithValue<Key>["value"]) => {
|
||||
dispatch(updateSetting({ key: displaySetting.key, value }));
|
||||
};
|
||||
|
||||
// used to match useState's get/set pattern with array values
|
||||
return [mergedSetting, handleSettingChange];
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export { EmbeddingSettings } from "./EmbeddingSettings";
|
||||
export { StaticEmbeddingSettings } from "./StaticEmbeddingSettings";
|
||||
export { EmbeddingSdkSettings } from "./EmbeddingSdkSettings";
|
||||
export { InteractiveEmbeddingSettings } from "./InteractiveEmbeddingSettings";
|
||||
export { EmbeddingSettingsSwitch } from "./EmbeddingSettingsSwitch";
|
||||
export { EmbeddingSettingsPageView } from "./EmbeddingSettingsPageView";
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { SettingKey, Settings } from "metabase-types/api";
|
||||
|
||||
type Setting<K extends SettingKey> = { key: K };
|
||||
|
||||
export interface AdminSettingComponentProps {
|
||||
updateSetting: <K extends SettingKey>(
|
||||
setting: Setting<K>,
|
||||
value: Settings[K] | null,
|
||||
) => Promise<void>;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import SettingNumber from "./widgets/SettingNumber";
|
||||
import SettingPassword from "./widgets/SettingPassword";
|
||||
import SettingRadio from "./widgets/SettingRadio";
|
||||
import SettingText from "./widgets/SettingText";
|
||||
import SettingToggle from "./widgets/SettingToggle";
|
||||
import { SettingToggle } from "./widgets/SettingToggle";
|
||||
import SettingSelect from "./widgets/deprecated/SettingSelect";
|
||||
|
||||
const SETTING_WIDGET_MAP = {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
import Card from "metabase/components/Card";
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { space } from "metabase/styled-components/theme";
|
||||
|
||||
export const StyledCard = styled(Card)`
|
||||
padding: 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 40rem;
|
||||
`;
|
||||
|
||||
export const Label = styled.span`
|
||||
padding: ${space(0)} ${space(1)};
|
||||
display: inline-block;
|
||||
line-height: 1.3;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--mb-color-text-white);
|
||||
background: var(--mb-color-brand);
|
||||
`;
|
||||
|
||||
export const BoldExternalLink = styled(ExternalLink)`
|
||||
color: var(--mb-color-brand);
|
||||
font-weight: bold;
|
||||
`;
|
||||
@@ -1,45 +1,39 @@
|
||||
import { Link } from "react-router";
|
||||
import { jt, t } from "ttag";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import { getPlan } from "metabase/common/utils/plan";
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import {
|
||||
getDocsUrl,
|
||||
getSetting,
|
||||
getUpgradeUrl,
|
||||
} from "metabase/selectors/settings";
|
||||
import type { ButtonProps } from "metabase/ui";
|
||||
import { Button, Flex, Text, Title } from "metabase/ui";
|
||||
import { useUniqueId } from "metabase/hooks/use-unique-id";
|
||||
import { Flex, Paper, Text, Title } from "metabase/ui";
|
||||
|
||||
import { BoldExternalLink, Label, StyledCard } from "./EmbeddingOption.styled";
|
||||
import InteractiveEmbeddingOff from "./InteractiveEmbeddingOff.svg?component";
|
||||
import InteractiveEmbeddingOn from "./InteractiveEmbeddingOn.svg?component";
|
||||
import StaticEmbeddingOff from "./StaticEmbeddingOff.svg?component";
|
||||
import StaticEmbeddingOn from "./StaticEmbeddingOn.svg?component";
|
||||
interface EmbeddingOptionProps {
|
||||
type EmbeddingOptionProps = {
|
||||
title: string;
|
||||
label?: string;
|
||||
children?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
label?: ReactNode;
|
||||
children?: ReactNode;
|
||||
description: ReactNode;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
function EmbeddingOption({
|
||||
export function EmbeddingOption({
|
||||
title,
|
||||
label,
|
||||
description,
|
||||
children,
|
||||
icon,
|
||||
}: EmbeddingOptionProps) {
|
||||
const titleId = useUniqueId();
|
||||
return (
|
||||
<StyledCard compact>
|
||||
<Paper
|
||||
maw="40rem"
|
||||
w="100%"
|
||||
shadow="sm"
|
||||
p="2.5rem"
|
||||
component="article"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
{icon}
|
||||
<Flex gap="md" mt="md" mb="sm" direction={"row"}>
|
||||
<Title order={2}>{title}</Title>
|
||||
{label && <Label>{label}</Label>}
|
||||
<Flex gap="md" mt="md" mb="sm" direction={"row"} align="center">
|
||||
<Title id={titleId} order={2}>
|
||||
{title}
|
||||
</Title>
|
||||
{label}
|
||||
</Flex>
|
||||
<Text lh={"1.25rem"} mb={"lg"}>
|
||||
{description}
|
||||
@@ -47,114 +41,6 @@ function EmbeddingOption({
|
||||
<Flex gap="lg" direction="column" align="flex-start">
|
||||
{children}
|
||||
</Flex>
|
||||
</StyledCard>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export const StaticEmbeddingOptionCard = () => {
|
||||
const enabled = useSetting("enable-embedding");
|
||||
const upgradeUrl = useSelector(state =>
|
||||
getUpgradeUrl(state, { utm_content: "embed-settings" }),
|
||||
);
|
||||
const shouldPromptToUpgrade = !PLUGIN_EMBEDDING.isEnabled();
|
||||
|
||||
const upgradeText = jt`A "powered by Metabase" banner appears on static embeds. You can ${(
|
||||
<ExternalLink key="upgrade-link" href={upgradeUrl}>
|
||||
{t`upgrade to a paid plan`}
|
||||
</ExternalLink>
|
||||
)} to remove it.`;
|
||||
|
||||
return (
|
||||
<EmbeddingOption
|
||||
icon={enabled ? <StaticEmbeddingOn /> : <StaticEmbeddingOff />}
|
||||
title={t`Static embedding`}
|
||||
description={jt`Use static embedding when you don’t want to give people ad hoc query access to their data for whatever reason, or you want to present data that applies to all of your tenants at once.${
|
||||
shouldPromptToUpgrade && (
|
||||
<Text size="sm" mt="xs" key="upgrade-text">
|
||||
{upgradeText}
|
||||
</Text>
|
||||
)
|
||||
}`}
|
||||
>
|
||||
<LinkButton
|
||||
variant="default"
|
||||
disabled={!enabled}
|
||||
to={"/admin/settings/embedding-in-other-applications/standalone"}
|
||||
>
|
||||
{t`Manage`}
|
||||
</LinkButton>
|
||||
</EmbeddingOption>
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveEmbeddingOptionCard = () => {
|
||||
const isEE = PLUGIN_EMBEDDING.isEnabled();
|
||||
const plan = useSelector(state =>
|
||||
getPlan(getSetting(state, "token-features")),
|
||||
);
|
||||
const enabled = useSetting("enable-embedding");
|
||||
const quickStartUrl = useSelector(state =>
|
||||
getDocsUrl(state, {
|
||||
page: "embedding/interactive-embedding-quick-start-guide",
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<EmbeddingOption
|
||||
icon={
|
||||
!isEE || enabled ? (
|
||||
<InteractiveEmbeddingOn />
|
||||
) : (
|
||||
<InteractiveEmbeddingOff />
|
||||
)
|
||||
}
|
||||
title={t`Interactive embedding`}
|
||||
label={t`PRO & ENTERPRISE`}
|
||||
description={jt`Use interactive embedding when you want to ${(
|
||||
<ExternalLink
|
||||
href={`https://www.metabase.com/blog/why-full-app-embedding?utm_source=${plan}&utm_media=embed-settings`}
|
||||
key="why-full-app-embedding"
|
||||
>
|
||||
{t`offer multi-tenant, self-service analytics`}
|
||||
</ExternalLink>
|
||||
)} and people want to create their own questions, dashboards, models, and more, all in their own data sandbox.`}
|
||||
>
|
||||
<BoldExternalLink
|
||||
href={`${quickStartUrl}?utm_source=${plan}&utm_media=embed-settings`}
|
||||
>
|
||||
{t`Check out our Quick Start`}
|
||||
</BoldExternalLink>
|
||||
{isEE ? (
|
||||
<LinkButton
|
||||
to={"/admin/settings/embedding-in-other-applications/full-app"}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{t`Configure`}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button
|
||||
component={ExternalLink}
|
||||
href={`https://www.metabase.com/product/embedded-analytics?utm_source=${plan}&utm_media=embed-settings`}
|
||||
>
|
||||
{t`Learn More`}
|
||||
</Button>
|
||||
)}
|
||||
</EmbeddingOption>
|
||||
);
|
||||
};
|
||||
|
||||
// component={Link} breaks the styling when the button is disabled
|
||||
// disabling a link button doesn't look like a common enough scenario to make an exported component
|
||||
const LinkButton = ({
|
||||
to,
|
||||
disabled,
|
||||
...buttonProps
|
||||
}: { to: string; disabled?: boolean } & ButtonProps) => {
|
||||
return disabled ? (
|
||||
<Button disabled={disabled} {...buttonProps} />
|
||||
) : (
|
||||
<Link to={to}>
|
||||
<Button {...buttonProps} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import { Badge } from "metabase/home/components/EmbedHomepage/Badge";
|
||||
import { PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import { Flex, Group } from "metabase/ui";
|
||||
|
||||
import { EmbeddingOption } from "../EmbeddingOption";
|
||||
import { LinkButton } from "../LinkButton";
|
||||
import { SwitchWithSetByEnvVar } from "../SwitchWithSetByEnvVar";
|
||||
|
||||
import { SdkIcon } from "./SdkIcon";
|
||||
|
||||
export function EmbeddingSdkOptionCard() {
|
||||
const isEmbeddingSdkEnabled = useSetting("enable-embedding-sdk");
|
||||
const isEE = PLUGIN_EMBEDDING.isEnabled();
|
||||
|
||||
return (
|
||||
<EmbeddingOption
|
||||
icon={<SdkIcon disabled={!isEmbeddingSdkEnabled} />}
|
||||
title={t`Embedded analytics SDK`}
|
||||
label={
|
||||
<Group spacing="sm">
|
||||
<Badge
|
||||
color="brand"
|
||||
fz="sm"
|
||||
px="sm"
|
||||
py="xs"
|
||||
uppercase
|
||||
>{t`Pro and Enterprise`}</Badge>
|
||||
<Badge
|
||||
color="gray"
|
||||
fz="sm"
|
||||
px="sm"
|
||||
py="xs"
|
||||
uppercase
|
||||
>{t`Beta`}</Badge>
|
||||
</Group>
|
||||
}
|
||||
description={t`Interactive embedding with full, granular control. Embed and style individual Metabase components in your app, and tailor the experience to each person. Allows for CSS styling, custom user flows, event subscriptions, and more. Only available with SSO via JWT.`}
|
||||
>
|
||||
<Flex align="center" w="100%">
|
||||
<LinkButton to={"/admin/settings/embedding-in-other-applications/sdk"}>
|
||||
{!isEE ? t`Try it out` : t`Configure`}
|
||||
</LinkButton>
|
||||
<SwitchWithSetByEnvVar settingKey="enable-embedding-sdk" />
|
||||
</Flex>
|
||||
</EmbeddingOption>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
type UseEmbeddingSettingsIconColorsProps,
|
||||
useEmbeddingSettingsIconColors,
|
||||
} from "../use-embedding-settings-icon-colors";
|
||||
|
||||
export const SdkIcon = ({ disabled }: UseEmbeddingSettingsIconColorsProps) => {
|
||||
const { primary, secondary } = useEmbeddingSettingsIconColors({ disabled });
|
||||
|
||||
return (
|
||||
<svg width="40" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#a)">
|
||||
<rect
|
||||
x="1.25"
|
||||
y="1.25"
|
||||
width="37.5"
|
||||
height="29.5"
|
||||
rx="2.75"
|
||||
stroke={secondary}
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M20 14.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89ZM15.37 24c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.965 22.965 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96Zm.71-5.74-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51Zm6.54-.76.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C21.17 13 20.6 13 20 13c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47l-.81 1.5.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47ZM20 10.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72Zm0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72ZM24.62 8c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96Zm-.7 5.74.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51Zm1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95ZM25.08 16c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26Zm-10.16 0c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26Zm9 2.26-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51Zm-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9Zm-4.95-8.56.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51Zm2.89-4.04C17.38 8.2 16 7.62 15.37 8c-.63.35-.82 1.82-.31 3.96a23.018 23.018 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9Z"
|
||||
fill={primary}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./EmbeddingSdkOptionCard";
|
||||
@@ -1,22 +0,0 @@
|
||||
<svg width="40" height="32" viewBox="0 0 40 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1030_4496)">
|
||||
<rect x="1.25" y="1.25" width="37.5" height="29.5" rx="2.75" stroke="#F0F0F0" stroke-width="2.5"/>
|
||||
<g opacity="0.5">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5267 17.4733L13 16L18.5267 14.5267L20 9L21.4733 14.5267L27 16L21.4733 17.4733L20 23L18.5267 17.4733Z" fill="#949AAB"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.3686 8.6314L27 8L29.3686 7.3686L30 5L30.6314 7.3686L33 8L30.6314 8.6314L30 11L29.3686 8.6314Z" fill="#949AAB"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.9738 24.0262L28 23.5L29.9738 22.9738L30.5 21L31.0262 22.9738L33 23.5L31.0262 24.0262L30.5 26L29.9738 24.0262Z" fill="#949AAB"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.97383 11.0262L7 10.5L8.97383 9.97383L9.5 8L10.0262 9.97383L12 10.5L10.0262 11.0262L9.5 13L8.97383 11.0262Z" fill="#949AAB"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1030_4496">
|
||||
<rect width="40" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg width="40" height="32" viewBox="0 0 40 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1030_4461)">
|
||||
<rect x="1.25" y="1.25" width="37.5" height="29.5" rx="2.75" stroke="#CBE2F7" stroke-width="2.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5267 17.4733L13 16L18.5267 14.5267L20 9L21.4733 14.5267L27 16L21.4733 17.4733L20 23L18.5267 17.4733Z" fill="#509EE3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.3686 8.6314L27 8L29.3686 7.3686L30 5L30.6314 7.3686L33 8L30.6314 8.6314L30 11L29.3686 8.6314Z" fill="#509EE3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.9738 24.0262L28 23.5L29.9738 22.9738L30.5 21L31.0262 22.9738L33 23.5L31.0262 24.0262L30.5 26L29.9738 24.0262Z" fill="#509EE3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.97383 11.0262L7 10.5L8.97383 9.97383L9.5 8L10.0262 9.97383L12 10.5L10.0262 11.0262L9.5 13L8.97383 11.0262Z" fill="#509EE3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1030_4461">
|
||||
<rect width="40" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
type UseEmbeddingSettingsIconColorsProps,
|
||||
useEmbeddingSettingsIconColors,
|
||||
} from "../use-embedding-settings-icon-colors";
|
||||
|
||||
export const InteractiveEmbeddingIcon = ({
|
||||
disabled,
|
||||
}: UseEmbeddingSettingsIconColorsProps) => {
|
||||
const { primary, secondary } = useEmbeddingSettingsIconColors({ disabled });
|
||||
|
||||
return (
|
||||
<svg width="40" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_1030_4461)">
|
||||
<rect
|
||||
x="1.25"
|
||||
y="1.25"
|
||||
width="37.5"
|
||||
height="29.5"
|
||||
rx="2.75"
|
||||
stroke={secondary}
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.527 17.473 13 16l5.527-1.473L20 9l1.473 5.527L27 16l-5.527 1.473L20 23l-1.473-5.527ZM29.369 8.631 27 8l2.369-.631L30 5l.631 2.369L33 8l-2.369.631L30 11l-.631-2.369ZM29.974 24.026 28 23.5l1.974-.526L30.5 21l.526 1.974L33 23.5l-1.974.526L30.5 26l-.526-1.974ZM8.974 11.026 7 10.5l1.974-.526L9.5 8l.526 1.974L12 10.5l-1.974.526L9.5 13l-.526-1.974Z"
|
||||
fill={primary}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1030_4461">
|
||||
<path fill="#fff" d="M0 0h40v32H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import { getPlan } from "metabase/common/utils/plan";
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { Badge } from "metabase/home/components/EmbedHomepage/Badge";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import { getDocsUrl, getSetting } from "metabase/selectors/settings";
|
||||
import { Box, Button, Flex, Icon, Text } from "metabase/ui";
|
||||
|
||||
import { EmbeddingOption } from "../EmbeddingOption";
|
||||
import { LinkButton } from "../LinkButton";
|
||||
import { SwitchWithSetByEnvVar } from "../SwitchWithSetByEnvVar";
|
||||
|
||||
import { InteractiveEmbeddingIcon } from "./InteractiveEmbeddingIcon";
|
||||
|
||||
export const InteractiveEmbeddingOptionCard = () => {
|
||||
const isEE = PLUGIN_EMBEDDING.isEnabled();
|
||||
const plan = useSelector(state =>
|
||||
getPlan(getSetting(state, "token-features")),
|
||||
);
|
||||
const isInteractiveEmbeddingEnabled = useSetting(
|
||||
"enable-embedding-interactive",
|
||||
);
|
||||
const quickStartUrl = useSelector(state =>
|
||||
getDocsUrl(state, {
|
||||
page: "embedding/interactive-embedding-quick-start-guide",
|
||||
}),
|
||||
);
|
||||
|
||||
const quickStartLinkWithUtm = `${quickStartUrl}?utm_source=${plan}&utm_media=embed-settings`;
|
||||
|
||||
return (
|
||||
<EmbeddingOption
|
||||
icon={
|
||||
<InteractiveEmbeddingIcon
|
||||
disabled={isEE && !isInteractiveEmbeddingEnabled}
|
||||
/>
|
||||
}
|
||||
title={t`Interactive embedding`}
|
||||
label={
|
||||
<Badge
|
||||
fz="sm"
|
||||
px="sm"
|
||||
py="xs"
|
||||
color="brand"
|
||||
uppercase
|
||||
>{t`Pro and Enterprise`}</Badge>
|
||||
}
|
||||
description={jt`Use interactive embedding when you want to ${(
|
||||
<ExternalLink
|
||||
href={`https://www.metabase.com/blog/why-full-app-embedding?utm_source=${plan}&utm_media=embed-settings`}
|
||||
key="why-full-app-embedding"
|
||||
>
|
||||
{t`offer multi-tenant, self-service analytics`}
|
||||
</ExternalLink>
|
||||
)} and people want to create their own questions, dashboards, models, and more, all in their own data sandbox.`}
|
||||
>
|
||||
<Text
|
||||
fw="bold"
|
||||
color="brand"
|
||||
component="a"
|
||||
pos="relative"
|
||||
href={quickStartLinkWithUtm}
|
||||
target="_blank"
|
||||
>
|
||||
{t`Check out our Quick Start`}{" "}
|
||||
<Box ml="sm" top="2.5px" pos="absolute" component="span">
|
||||
<Icon name="share" aria-hidden />
|
||||
</Box>
|
||||
</Text>
|
||||
<Flex align="center" w="100%">
|
||||
{isEE ? (
|
||||
<LinkButton
|
||||
to={"/admin/settings/embedding-in-other-applications/full-app"}
|
||||
>
|
||||
{t`Configure`}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button
|
||||
component={ExternalLink}
|
||||
href={`https://www.metabase.com/product/embedded-analytics?utm_source=${plan}&utm_media=embed-settings`}
|
||||
>
|
||||
{t`Learn More`}
|
||||
</Button>
|
||||
)}
|
||||
<SwitchWithSetByEnvVar
|
||||
settingKey="enable-embedding-interactive"
|
||||
disabled={!isEE}
|
||||
/>
|
||||
</Flex>
|
||||
</EmbeddingOption>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./InteractiveEmbeddingOptionCard";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { Button, type ButtonProps } from "metabase/ui";
|
||||
|
||||
// component={Link} breaks the styling when the button is disabled
|
||||
// disabling a link button doesn't look like a common enough scenario to make an exported component
|
||||
export const LinkButton = ({
|
||||
to,
|
||||
disabled,
|
||||
...buttonProps
|
||||
}: { to: string; disabled?: boolean } & ButtonProps) => {
|
||||
return disabled ? (
|
||||
<Button disabled={disabled} {...buttonProps} />
|
||||
) : (
|
||||
<Link to={to}>
|
||||
<Button {...buttonProps} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./LinkButton";
|
||||
@@ -1,8 +0,0 @@
|
||||
<svg width="42" height="34" viewBox="0 0 42 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.25" y="1.25" width="37.5" height="29.5" rx="2.75" stroke="#F0F0F0" stroke-width="2.5"/>
|
||||
<path opacity="0.5" d="M14 12C14 10.8954 14.8954 10 16 10H33C34.1046 10 35 10.8954 35 12V17H14V12Z" fill="#949AAB"/>
|
||||
<path opacity="0.5" d="M14 19H23V28H16C14.8954 28 14 27.1046 14 26V19Z" fill="#949AAB"/>
|
||||
<path opacity="0.5" d="M25 19H35V26C35 27.1046 34.1046 28 33 28H25V19Z" fill="#949AAB"/>
|
||||
<path d="M5 10H11V28H7C5.89543 28 5 27.1046 5 26V10Z" fill="#F0F0F0"/>
|
||||
<path d="M5 6C5 4.89543 5.89543 4 7 4H33C34.1046 4 35 4.89543 35 6V8H5V6Z" fill="#F0F0F0"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 674 B |
@@ -1,8 +0,0 @@
|
||||
<svg width="42" height="34" viewBox="0 0 42 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.25" y="1.25" width="37.5" height="29.5" rx="2.75" stroke="#CBE2F7" stroke-width="2.5"/>
|
||||
<path d="M14 12C14 10.8954 14.8954 10 16 10H33C34.1046 10 35 10.8954 35 12V17H14V12Z" fill="#509EE3"/>
|
||||
<path d="M14 19H23V28H16C14.8954 28 14 27.1046 14 26V19Z" fill="#509EE3"/>
|
||||
<path d="M25 19H35V26C35 27.1046 34.1046 28 33 28H25V19Z" fill="#509EE3"/>
|
||||
<path d="M5 10H11V28H7C5.89543 28 5 27.1046 5 26V10Z" fill="#CBE2F7"/>
|
||||
<path d="M5 6C5 4.89543 5.89543 4 7 4H33C34.1046 4 35 4.89543 35 6V8H5V6Z" fill="#CBE2F7"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 632 B |
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type UseEmbeddingSettingsIconColorsProps,
|
||||
useEmbeddingSettingsIconColors,
|
||||
} from "../use-embedding-settings-icon-colors";
|
||||
|
||||
export const StaticEmbeddingIcon = ({
|
||||
disabled,
|
||||
}: UseEmbeddingSettingsIconColorsProps) => {
|
||||
const { primary, secondary } = useEmbeddingSettingsIconColors({ disabled });
|
||||
|
||||
return (
|
||||
<svg width="42" height="34" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect
|
||||
x="1.25"
|
||||
y="1.25"
|
||||
width="37.5"
|
||||
height="29.5"
|
||||
rx="2.75"
|
||||
stroke={secondary}
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M14 12a2 2 0 0 1 2-2h17a2 2 0 0 1 2 2v5H14v-5ZM14 19h9v9h-7a2 2 0 0 1-2-2v-7ZM25 19h10v7a2 2 0 0 1-2 2h-8v-9Z"
|
||||
fill={primary}
|
||||
/>
|
||||
<path
|
||||
d="M5 10h6v18H7a2 2 0 0 1-2-2V10ZM5 6a2 2 0 0 1 2-2h26a2 2 0 0 1 2 2v2H5V6Z"
|
||||
fill={secondary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import { useSetting } from "metabase/common/hooks";
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import { getUpgradeUrl } from "metabase/selectors/settings";
|
||||
import { Flex, Text } from "metabase/ui";
|
||||
|
||||
import { EmbeddingOption } from "../EmbeddingOption";
|
||||
import { LinkButton } from "../LinkButton";
|
||||
import { SwitchWithSetByEnvVar } from "../SwitchWithSetByEnvVar";
|
||||
|
||||
import { StaticEmbeddingIcon } from "./StaticEmbeddingIcon";
|
||||
|
||||
export const StaticEmbeddingOptionCard = () => {
|
||||
const isStaticEmbeddingEnabled = useSetting("enable-embedding-static");
|
||||
const upgradeUrl = useSelector(state =>
|
||||
getUpgradeUrl(state, { utm_content: "embed-settings" }),
|
||||
);
|
||||
const shouldPromptToUpgrade = !PLUGIN_EMBEDDING.isEnabled();
|
||||
|
||||
const upgradeText = jt`A "powered by Metabase" banner appears on static embeds. You can ${(
|
||||
<ExternalLink key="upgrade-link" href={upgradeUrl}>
|
||||
{t`upgrade to a paid plan`}
|
||||
</ExternalLink>
|
||||
)} to remove it.`;
|
||||
|
||||
return (
|
||||
<EmbeddingOption
|
||||
icon={<StaticEmbeddingIcon disabled={!isStaticEmbeddingEnabled} />}
|
||||
title={t`Static embedding`}
|
||||
description={jt`Use static embedding when you don’t want to give people ad hoc query access to their data for whatever reason, or you want to present data that applies to all of your tenants at once.${
|
||||
shouldPromptToUpgrade && (
|
||||
<Text size="sm" mt="xs" key="upgrade-text">
|
||||
{upgradeText}
|
||||
</Text>
|
||||
)
|
||||
}`}
|
||||
>
|
||||
<Flex align="center" w="100%">
|
||||
<LinkButton
|
||||
variant="default"
|
||||
to={"/admin/settings/embedding-in-other-applications/standalone"}
|
||||
>
|
||||
{t`Manage`}
|
||||
</LinkButton>
|
||||
<SwitchWithSetByEnvVar settingKey="enable-embedding-static" />
|
||||
</Flex>
|
||||
</EmbeddingOption>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./StaticEmbeddingOptionCard";
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Switch, Text } from "metabase/ui";
|
||||
|
||||
import { useEmbeddingSetting } from "../../../EmbeddingSettings/hooks";
|
||||
|
||||
interface SwitchWithSetByEnvVarProps extends ComponentProps<typeof Switch> {
|
||||
settingKey:
|
||||
| "enable-embedding-static"
|
||||
| "enable-embedding-sdk"
|
||||
| "enable-embedding-interactive";
|
||||
}
|
||||
|
||||
export function SwitchWithSetByEnvVar({
|
||||
settingKey,
|
||||
...switchProps
|
||||
}: SwitchWithSetByEnvVarProps) {
|
||||
const [setting, handleChange] = useEmbeddingSetting({ key: settingKey });
|
||||
|
||||
if (setting.is_env_setting) {
|
||||
return (
|
||||
<Text
|
||||
ml="auto"
|
||||
color="var(--mb-color-text-secondary)"
|
||||
>{t`Set via environment variable`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const isEnabled = Boolean(setting.value);
|
||||
return (
|
||||
<Switch
|
||||
{...switchProps}
|
||||
label={isEnabled ? t`Enabled` : t`Disabled`}
|
||||
size="sm"
|
||||
ml="auto"
|
||||
labelPosition="left"
|
||||
checked={isEnabled}
|
||||
disabled={setting.is_env_setting ? true : switchProps.disabled}
|
||||
onChange={event => handleChange(event.currentTarget.checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SwitchWithSetByEnvVar";
|
||||
@@ -1,4 +1,3 @@
|
||||
export {
|
||||
InteractiveEmbeddingOptionCard,
|
||||
StaticEmbeddingOptionCard,
|
||||
} from "./EmbeddingOption";
|
||||
export { StaticEmbeddingOptionCard } from "./StaticEmbeddingOptionCard";
|
||||
export { EmbeddingSdkOptionCard } from "./EmbeddingSdkOptionCard";
|
||||
export { InteractiveEmbeddingOptionCard } from "./InteractiveEmbeddingOptionCard";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export type UseEmbeddingSettingsIconColorsProps = {
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const useEmbeddingSettingsIconColors = ({
|
||||
disabled,
|
||||
}: UseEmbeddingSettingsIconColorsProps) => {
|
||||
if (disabled) {
|
||||
return {
|
||||
primary: "var(--mb-base-color-gray-30)",
|
||||
secondary: "var(--mb-base-color-gray-10)",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
primary: "var(--mb-base-color-brand-40)",
|
||||
secondary: "var(--mb-base-color-brand-20)",
|
||||
};
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Stack, Switch, Text } from "metabase/ui";
|
||||
|
||||
interface EmbeddingSwitchWidgetProps {
|
||||
setting: { value: boolean | null };
|
||||
onChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const EmbeddingSwitchWidget = ({
|
||||
setting,
|
||||
onChange,
|
||||
}: EmbeddingSwitchWidgetProps) => (
|
||||
<Stack spacing={"md"} className={CS.textMeasure}>
|
||||
<Text lh={1.5}>
|
||||
{t`Embed dashboards, questions, or the entire Metabase app into your application. Integrate with your server code to create a secure environment, limited to specific users or organizations.`}
|
||||
</Text>
|
||||
<Switch
|
||||
labelPosition="left"
|
||||
checked={Boolean(setting.value)}
|
||||
label={<strong>{t`Enable Embedding`}</strong>}
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { EmbeddingSwitchWidget } from "./EmbeddingSwitchWidget";
|
||||
@@ -11,7 +11,7 @@ import { FormSwitch } from "metabase/forms";
|
||||
import { isDefaultGroup } from "metabase/lib/groups";
|
||||
import { Icon } from "metabase/ui";
|
||||
|
||||
import SettingToggle from "../SettingToggle";
|
||||
import { SettingToggle } from "../SettingToggle";
|
||||
|
||||
import AddMappingRow from "./AddMappingRow";
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Component } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import SettingToggle from "./SettingToggle";
|
||||
import { SettingToggle } from "./SettingToggle";
|
||||
|
||||
const VERIFIED = "verified";
|
||||
const CHECKING = "checking";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import Confirm from "metabase/components/Confirm";
|
||||
@@ -9,6 +10,7 @@ import { SettingInput } from "../SettingInput";
|
||||
import { GenerateButton, SecretKeyWidgetRoot } from "./SecretKeyWidget.styled";
|
||||
|
||||
interface SecretKeyWidgetProps {
|
||||
id?: string;
|
||||
onChange: (token: string) => void;
|
||||
setting: any;
|
||||
confirmation: {
|
||||
@@ -23,14 +25,22 @@ const SecretKeyWidget = ({
|
||||
confirmation,
|
||||
...rest
|
||||
}: SecretKeyWidgetProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const generateToken = async () => {
|
||||
setLoading(true);
|
||||
const result = await UtilApi.random_token();
|
||||
onChange(result.token);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SecretKeyWidgetRoot>
|
||||
<SettingInput setting={setting} onChange={onChange} {...(rest as any)} />
|
||||
<SettingInput
|
||||
loading={loading}
|
||||
setting={setting}
|
||||
onChange={onChange}
|
||||
{...(rest as any)}
|
||||
/>
|
||||
{setting.value ? (
|
||||
<Confirm
|
||||
triggerClasses={CS.fullHeight}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface SettingInputProps {
|
||||
setting: {
|
||||
key: string;
|
||||
value: string | null;
|
||||
default?: string;
|
||||
default?: string | null;
|
||||
placeholder?: string;
|
||||
};
|
||||
onChange: (value: Value) => void;
|
||||
@@ -20,6 +20,7 @@ export interface SettingInputProps {
|
||||
id?: string;
|
||||
type: "text" | "password";
|
||||
normalize?: (value: Value) => Value;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const identity = (value: Value) => value;
|
||||
@@ -32,6 +33,7 @@ export const SettingTextInput = ({
|
||||
id,
|
||||
type = "text",
|
||||
normalize = identity,
|
||||
disabled,
|
||||
}: SettingInputProps) => {
|
||||
const [valueState, setValueState] = useState<string>(setting.value ?? "");
|
||||
const changeHandler = (e: { target: HTMLInputElement }) => {
|
||||
@@ -59,6 +61,7 @@ export const SettingTextInput = ({
|
||||
placeholder={setting.placeholder}
|
||||
onBlur={changeHandler}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import cx from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import Toggle from "metabase/core/components/Toggle";
|
||||
import Tooltip from "metabase/core/components/Tooltip";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
|
||||
const SettingToggle = ({
|
||||
import type { SettingElement } from "../../types";
|
||||
|
||||
type SettingToggleProps = {
|
||||
disabled: boolean;
|
||||
hideLabel: boolean;
|
||||
id: string;
|
||||
setting: SettingElement;
|
||||
tooltip: ReactNode;
|
||||
onChange: ((value: boolean) => void) | undefined;
|
||||
onChangeSetting?: unknown;
|
||||
reloadSettings?: unknown;
|
||||
settingValues?: unknown;
|
||||
};
|
||||
|
||||
export const SettingToggle = ({
|
||||
disabled,
|
||||
hideLabel,
|
||||
id,
|
||||
setting,
|
||||
tooltip,
|
||||
onChange,
|
||||
// the following three props were being spread into the 'div', causing
|
||||
// unknown prop errors so we're just keeping them here
|
||||
onChangeSetting,
|
||||
reloadSettings,
|
||||
settingValues,
|
||||
...props
|
||||
}) => {
|
||||
}: SettingToggleProps) => {
|
||||
const value = setting.value == null ? setting.default : setting.value;
|
||||
const on = value === true || value === "true";
|
||||
return (
|
||||
@@ -23,7 +42,7 @@ const SettingToggle = ({
|
||||
<Toggle
|
||||
id={id}
|
||||
value={on}
|
||||
onChange={!disabled ? () => onChange(!on) : null}
|
||||
onChange={!disabled ? () => onChange?.(!on) : undefined}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -35,5 +54,3 @@ const SettingToggle = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingToggle;
|
||||
@@ -3,14 +3,13 @@ import { t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import { SMTPConnectionForm } from "metabase/admin/settings/components/Email/SMTPConnectionForm";
|
||||
import Breadcrumbs from "metabase/components/Breadcrumbs";
|
||||
import { DashboardSelector } from "metabase/components/DashboardSelector";
|
||||
import MetabaseSettings from "metabase/lib/settings";
|
||||
import { newVersionAvailable } from "metabase/lib/utils";
|
||||
import {
|
||||
PLUGIN_ADMIN_SETTINGS,
|
||||
PLUGIN_ADMIN_SETTINGS_AUTH_TABS,
|
||||
PLUGIN_ADMIN_SETTINGS_UPDATES,
|
||||
PLUGIN_EMBEDDING,
|
||||
PLUGIN_LLM_AUTODESCRIPTION,
|
||||
} from "metabase/plugins";
|
||||
import { refreshCurrentUser } from "metabase/redux/user";
|
||||
@@ -23,25 +22,22 @@ import {
|
||||
import { CloudPanel } from "./components/CloudPanel";
|
||||
import { BccToggleWidget } from "./components/Email/BccToggleWidget";
|
||||
import { SettingsEmailForm } from "./components/Email/SettingsEmailForm";
|
||||
import {
|
||||
EmbeddingSdkSettings,
|
||||
EmbeddingSettings,
|
||||
StaticEmbeddingSettings,
|
||||
} from "./components/EmbeddingSettings";
|
||||
import SettingsLicense from "./components/SettingsLicense";
|
||||
import SettingsUpdatesForm from "./components/SettingsUpdatesForm/SettingsUpdatesForm";
|
||||
import { UploadSettings } from "./components/UploadSettings";
|
||||
import CustomGeoJSONWidget from "./components/widgets/CustomGeoJSONWidget";
|
||||
import {
|
||||
InteractiveEmbeddingOptionCard,
|
||||
StaticEmbeddingOptionCard,
|
||||
} from "./components/widgets/EmbeddingOption";
|
||||
import { EmbeddingSwitchWidget } from "./components/widgets/EmbeddingSwitchWidget";
|
||||
import FormattingWidget from "./components/widgets/FormattingWidget";
|
||||
import HttpsOnlyWidget from "./components/widgets/HttpsOnlyWidget";
|
||||
import {
|
||||
EmbeddedResources,
|
||||
PublicLinksActionListing,
|
||||
PublicLinksDashboardListing,
|
||||
PublicLinksQuestionListing,
|
||||
} from "./components/widgets/PublicLinksListing";
|
||||
import RedirectWidget from "./components/widgets/RedirectWidget";
|
||||
import SecretKeyWidget from "./components/widgets/SecretKeyWidget";
|
||||
import SettingCommaDelimitedInput from "./components/widgets/SettingCommaDelimitedInput";
|
||||
import SiteUrlWidget from "./components/widgets/SiteUrlWidget";
|
||||
import { NotificationSettings } from "./notifications/NotificationSettings";
|
||||
@@ -428,100 +424,25 @@ export const ADMIN_SETTINGS_SECTIONS = {
|
||||
key: "enable-embedding",
|
||||
name: t`Embedding`,
|
||||
order: 100,
|
||||
settings: [
|
||||
{
|
||||
key: "enable-embedding",
|
||||
display_name: t`Embedding`,
|
||||
description: null,
|
||||
widget: EmbeddingSwitchWidget,
|
||||
},
|
||||
{
|
||||
key: "-static-embedding",
|
||||
widget: StaticEmbeddingOptionCard,
|
||||
},
|
||||
{
|
||||
key: "-interactive-embedding",
|
||||
widget: InteractiveEmbeddingOptionCard,
|
||||
},
|
||||
],
|
||||
component: EmbeddingSettings,
|
||||
settings: [],
|
||||
},
|
||||
"embedding-in-other-applications/standalone": {
|
||||
settings: [
|
||||
{
|
||||
key: "-breadcrumb",
|
||||
widget: () => {
|
||||
return (
|
||||
<Breadcrumbs
|
||||
size="large"
|
||||
crumbs={[
|
||||
[
|
||||
t`Embedding`,
|
||||
"/admin/settings/embedding-in-other-applications",
|
||||
],
|
||||
[t`Static embedding`],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "embedding-secret-key",
|
||||
display_name: t`Embedding secret key`,
|
||||
description: t`Standalone Embed Secret Key used to sign JSON Web Tokens for requests to /api/embed endpoints. This lets you create a secure environment limited to specific users or organizations.`,
|
||||
widget: SecretKeyWidget,
|
||||
getHidden: (_, derivedSettings) => !derivedSettings["enable-embedding"],
|
||||
props: {
|
||||
confirmation: {
|
||||
header: t`Regenerate embedding key?`,
|
||||
dialog: t`This will cause existing embeds to stop working until they are updated with the new key.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: "-embedded-resources",
|
||||
display_name: t`Manage embeds`,
|
||||
|
||||
widget: EmbeddedResources,
|
||||
getHidden: (_, derivedSettings) => !derivedSettings["enable-embedding"],
|
||||
},
|
||||
{
|
||||
key: "-redirect-widget",
|
||||
widget: () => (
|
||||
<RedirectWidget to="/admin/settings/embedding-in-other-applications" />
|
||||
),
|
||||
getHidden: (_, derivedSettings) => derivedSettings["enable-embedding"],
|
||||
},
|
||||
],
|
||||
component: StaticEmbeddingSettings,
|
||||
settings: [],
|
||||
},
|
||||
"embedding-in-other-applications/sdk": {
|
||||
component: EmbeddingSdkSettings,
|
||||
settings: [],
|
||||
},
|
||||
"embedding-in-other-applications/full-app": {
|
||||
settings: [
|
||||
{
|
||||
key: "-breadcrumbs",
|
||||
widget: () => {
|
||||
return (
|
||||
<Breadcrumbs
|
||||
size="large"
|
||||
crumbs={[
|
||||
[
|
||||
t`Embedding`,
|
||||
"/admin/settings/embedding-in-other-applications",
|
||||
],
|
||||
[t`Interactive embedding`],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "-redirect-widget",
|
||||
widget: () => (
|
||||
<RedirectWidget to="/admin/settings/embedding-in-other-applications" />
|
||||
),
|
||||
getHidden: (_, derivedSettings) =>
|
||||
PLUGIN_EMBEDDING.isEnabled() && derivedSettings["enable-embedding"],
|
||||
},
|
||||
],
|
||||
// We need to do this because EE plugins would load after this file in unit tests
|
||||
component: ({ updateSetting }) => (
|
||||
<PLUGIN_ADMIN_SETTINGS.InteractiveEmbeddingSettings
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
),
|
||||
settings: [],
|
||||
},
|
||||
license: {
|
||||
name: t`License`,
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
--mb-color-bg-black: #2e353b;
|
||||
--mb-color-bg-dark: #93a1ab;
|
||||
--mb-color-bg-medium: #edf2f5;
|
||||
--mb-color-bg-light: #f9fbfc;
|
||||
--mb-color-bg-light: var(--mb-base-color-gray-5);
|
||||
--mb-color-bg-white: var(--mb-base-color-white);
|
||||
--mb-color-bg-error: #ed6e6e55;
|
||||
--mb-color-bg-night: #42484e;
|
||||
@@ -63,9 +63,11 @@
|
||||
--mb-color-text-tertiary: var(--mb-color-text-light);
|
||||
--mb-color-text-selected: var(--mb-base-color-white);
|
||||
--mb-color-text-hover: var(--mb-base-color-brand-40);
|
||||
--mb-color-text-disabled: var(--mb-base-color-gray-50);
|
||||
--mb-color-text-brand: var(--mb-base-color-brand-40);
|
||||
--mb-color-text-white: var(--mb-base-color-white);
|
||||
--mb-color-background: var(--mb-base-color-white);
|
||||
--mb-color-background-info: var(--mb-base-color-gray-5);
|
||||
--mb-color-background-selected: var(--mb-base-color-brand-40);
|
||||
--mb-color-background-hover: var(--mb-base-color-brand-10);
|
||||
--mb-color-background-disabled: var(--mb-base-color-gray-10);
|
||||
@@ -76,6 +78,8 @@
|
||||
/**
|
||||
* Base colors
|
||||
*
|
||||
* @see https://www.figma.com/design/YxiDXi1DzmXJCTMCuqXaaY/Applying-the-updated-colors?node-id=4001-16486&node-type=FRAME&t=Iwv4nAfH8QF1J5QG-0
|
||||
*
|
||||
* These are the base colors that should never change and never be used
|
||||
* directly in any components or CSS properties. They should only be used
|
||||
* to define the other semantic colors in this file.
|
||||
@@ -116,6 +120,11 @@
|
||||
var(--mb-color-brand),
|
||||
white 45%
|
||||
);
|
||||
--mb-base-color-brand-20: color-mix(
|
||||
in srgb,
|
||||
var(--mb-color-brand),
|
||||
white 70%
|
||||
);
|
||||
--mb-base-color-brand-10: color-mix(
|
||||
in srgb,
|
||||
var(--mb-color-brand),
|
||||
|
||||
38
frontend/src/metabase/home/components/EmbedHomepage/Badge.ts
Normal file
38
frontend/src/metabase/home/components/EmbedHomepage/Badge.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import styled from "@emotion/styled";
|
||||
import type { MantineSize } from "@mantine/styles";
|
||||
|
||||
type BadgeColor = "brand" | "gray";
|
||||
|
||||
const COLOR_VARIANTS = {
|
||||
brand: {
|
||||
color: "var(--mb-color-text-white)",
|
||||
background: "var(--mb-color-brand)",
|
||||
},
|
||||
gray: {
|
||||
color: "var(--mb-color-text-black)",
|
||||
background: "var(--mb-base-color-gray-20)",
|
||||
},
|
||||
};
|
||||
|
||||
export const Badge = styled.span<{
|
||||
color: BadgeColor;
|
||||
uppercase?: boolean;
|
||||
px?: MantineSize;
|
||||
py?: MantineSize;
|
||||
fz?: MantineSize;
|
||||
}>`
|
||||
padding: ${({ px, py, theme }) => {
|
||||
const paddingY = py ? theme.spacing[py] : "0";
|
||||
const paddingX = px ? theme.spacing[px] : "4px";
|
||||
return `${paddingY} ${paddingX}`;
|
||||
}};
|
||||
display: inline-block;
|
||||
line-height: 1rem;
|
||||
font-size: ${({ fz, theme }) => (fz ? theme.fontSizes[fz] : "0.625rem")};
|
||||
font-weight: 700;
|
||||
border-radius: 4px;
|
||||
text-transform: ${props =>
|
||||
(props.uppercase ?? true) ? "uppercase" : "none"};
|
||||
color: ${({ color }) => COLOR_VARIANTS[color].color};
|
||||
background: ${({ color }) => COLOR_VARIANTS[color].background};
|
||||
`;
|
||||
@@ -106,6 +106,7 @@ export const EmbedHomepage = () => {
|
||||
}
|
||||
learnMoreInteractiveEmbedUrl={learnMoreInteractiveEmbedding + utmTags}
|
||||
learnMoreStaticEmbedUrl={learnMoreStaticEmbedding + utmTags}
|
||||
sdkUrl={"https://metaba.se/sdk" + utmTags}
|
||||
/>
|
||||
<FeedbackModal
|
||||
opened={feedbackModalOpened}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "metabase/ui";
|
||||
import type { EmbeddingHomepageDismissReason } from "metabase-types/api";
|
||||
|
||||
import { Badge } from "./Badge";
|
||||
import { InteractiveTabContent } from "./InteractiveTabContent";
|
||||
import { StaticTabContent } from "./StaticTabContent";
|
||||
import type { EmbeddingHomepageInitialTab } from "./types";
|
||||
@@ -31,6 +32,7 @@ export type EmbedHomepageViewProps = {
|
||||
analyticsDocsUrl: string;
|
||||
learnMoreStaticEmbedUrl: string;
|
||||
learnMoreInteractiveEmbedUrl: string;
|
||||
sdkUrl: string;
|
||||
};
|
||||
|
||||
export const EmbedHomepageView = (props: EmbedHomepageViewProps) => {
|
||||
@@ -39,6 +41,7 @@ export const EmbedHomepageView = (props: EmbedHomepageViewProps) => {
|
||||
initialTab,
|
||||
embeddingDocsUrl,
|
||||
analyticsDocsUrl,
|
||||
sdkUrl,
|
||||
onDismiss,
|
||||
} = props;
|
||||
return (
|
||||
@@ -86,6 +89,26 @@ export const EmbedHomepageView = (props: EmbedHomepageViewProps) => {
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text color="text-medium" fw="bold" size="sm">
|
||||
{/* eslint-disable-next-line no-literal-metabase-strings -- only visible to admins */}
|
||||
{t`New in Metabase 0.51`}
|
||||
</Text>
|
||||
<Group spacing="sm">
|
||||
<Text color="text-dark" fw="bold">{t`Embedded analytics SDK`}</Text>
|
||||
<Badge color="brand">{t`PRO & ENTERPRISE`}</Badge>
|
||||
<Badge color="gray">{t`Beta`}</Badge>
|
||||
</Group>
|
||||
|
||||
<Text color="text-light" size="sm">
|
||||
{/* eslint-disable-next-line no-literal-metabase-strings -- only visible to admins */}
|
||||
{t`Interactive embedding with full, granular control. Embed and style individual Metabase components in your app, and tailor the experience to each person. Allows for CSS styling, custom user flows, event subscriptions, and more. Only available with SSO via JWT.`}{" "}
|
||||
<Anchor size="sm" href={sdkUrl}>
|
||||
{t`Read more in the docs.`}
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
{embeddingAutoEnabled && (
|
||||
<Card>
|
||||
<Text
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type EntityId,
|
||||
type PermissionSubject,
|
||||
} from "metabase/admin/permissions/types";
|
||||
import { InteractiveEmbeddingSettings } from "metabase/admin/settings/components/EmbeddingSettings/InteractiveEmbeddingSettings";
|
||||
import type { ADMIN_SETTINGS_SECTIONS } from "metabase/admin/settings/selectors";
|
||||
import type {
|
||||
ActualModelFilters,
|
||||
@@ -95,6 +96,10 @@ export const PLUGIN_ADMIN_TROUBLESHOOTING = {
|
||||
GET_EXTRA_NAV: (): ReactNode[] => [],
|
||||
};
|
||||
|
||||
export const PLUGIN_ADMIN_SETTINGS = {
|
||||
InteractiveEmbeddingSettings: InteractiveEmbeddingSettings,
|
||||
};
|
||||
|
||||
// functions that update the sections
|
||||
export const PLUGIN_ADMIN_SETTINGS_UPDATES: ((
|
||||
sections: typeof ADMIN_SETTINGS_SECTIONS,
|
||||
@@ -505,6 +510,10 @@ export const PLUGIN_EMBEDDING = {
|
||||
isInteractiveEmbeddingEnabled: (_state: State) => false,
|
||||
};
|
||||
|
||||
export const PLUGIN_EMBEDDING_SDK = {
|
||||
isEnabled: () => false,
|
||||
};
|
||||
|
||||
export const PLUGIN_CONTENT_VERIFICATION = {
|
||||
VerifiedFilter: {} as SearchFilterComponent<"verified">,
|
||||
availableModelFilters: {} as AvailableModelFilters,
|
||||
|
||||
@@ -7,7 +7,8 @@ import { getUserIsAdmin } from "metabase/selectors/user";
|
||||
import { Center, Icon, Menu, Stack, Text, Title } from "metabase/ui";
|
||||
|
||||
export function EmbedMenuItem({ onClick }: { onClick: () => void }) {
|
||||
const isEmbeddingEnabled = useSetting("enable-embedding");
|
||||
// TODO: Change this to `enable-embedding-static` once the BE is implemented.
|
||||
const isStaticEmbeddingEnabled = useSetting("enable-embedding");
|
||||
const isAdmin = useSelector(getUserIsAdmin);
|
||||
|
||||
if (!isAdmin) {
|
||||
@@ -23,9 +24,9 @@ export function EmbedMenuItem({ onClick }: { onClick: () => void }) {
|
||||
<Icon name="embed" />
|
||||
</Center>
|
||||
}
|
||||
onClick={isEmbeddingEnabled ? onClick : undefined}
|
||||
onClick={isStaticEmbeddingEnabled ? onClick : undefined}
|
||||
>
|
||||
{isEmbeddingEnabled ? (
|
||||
{isStaticEmbeddingEnabled ? (
|
||||
<Title order={4}>{t`Embed`}</Title>
|
||||
) : (
|
||||
<Link
|
||||
|
||||
@@ -28,10 +28,14 @@ export const getInputOverrides = (): MantineThemeOverride["components"] => ({
|
||||
minHeight: getSize({ size, sizes: SIZES }),
|
||||
background: theme.fn.themeColor("bg-white"),
|
||||
"&::placeholder": {
|
||||
color: theme.fn.themeColor("text-light"),
|
||||
color: "var(--mb-color-text-tertiary)",
|
||||
},
|
||||
"&:disabled": {
|
||||
backgroundColor: theme.fn.themeColor("bg-light"),
|
||||
color: "var(--mb-color-text-disabled)",
|
||||
backgroundColor: "var(--mb-color-background-disabled)",
|
||||
"&::placeholder": {
|
||||
color: "var(--mb-color-text-disabled)",
|
||||
},
|
||||
},
|
||||
"&[data-invalid]": {
|
||||
color: theme.fn.themeColor("error"),
|
||||
|
||||
@@ -5,18 +5,30 @@
|
||||
[crypto.random :as crypto-random]
|
||||
[metabase.analytics.snowplow :as snowplow]
|
||||
[metabase.models.setting :as setting :refer [defsetting]]
|
||||
[metabase.public-settings :as public-settings]
|
||||
[metabase.public-settings.premium-features :as premium-features]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.embed :as embed]
|
||||
[metabase.util.i18n :as i18n :refer [deferred-tru]]
|
||||
[metabase.util.malli :as mu]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(defsetting embedding-app-origin
|
||||
(deferred-tru "Allow this origin to embed the full {0} application"
|
||||
(public-settings/application-name-for-setting-descriptions))
|
||||
:feature :embedding
|
||||
:visibility :public
|
||||
:audit :getter
|
||||
:encryption :no)
|
||||
(mu/defn- make-embedding-toggle-setter
|
||||
"Creates a boolean setter for various boolean embedding-enabled flavors, all tracked by snowplow."
|
||||
[setting-key :- :keyword event-name :- :string]
|
||||
(fn [new-value]
|
||||
(u/prog1 new-value
|
||||
(let [old-value (setting/get-value-of-type :boolean setting-key)]
|
||||
(when (not= new-value old-value)
|
||||
(setting/set-value-of-type! :boolean setting-key new-value)
|
||||
(when (and new-value (str/blank? (embed/embedding-secret-key)))
|
||||
(embed/embedding-secret-key! (crypto-random/hex 32)))
|
||||
(snowplow/track-event! ::snowplow/embed_share
|
||||
{:event (keyword (str event-name (if new-value "-enabled" "-disabled")))
|
||||
:embedding-app-origin-set (boolean (setting/get-value-of-type :string :embedding-app-origin))
|
||||
:number-embedded-questions (t2/count :model/Card :enable_embedding true)
|
||||
:number-embedded-dashboards (t2/count :model/Dashboard :enable_embedding true)}))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Embed Settings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defsetting enable-embedding
|
||||
(deferred-tru "Allow admins to securely embed questions and dashboards within other applications?")
|
||||
@@ -25,18 +37,82 @@
|
||||
:visibility :authenticated
|
||||
:export? true
|
||||
:audit :getter
|
||||
:setter (fn [new-value]
|
||||
(when (not= new-value (setting/get-value-of-type :boolean :enable-embedding))
|
||||
(setting/set-value-of-type! :boolean :enable-embedding new-value)
|
||||
(when (and new-value (str/blank? (embed/embedding-secret-key)))
|
||||
(embed/embedding-secret-key! (crypto-random/hex 32)))
|
||||
(snowplow/track-event! ::snowplow/embed_share
|
||||
{:event (if new-value
|
||||
:embedding-enabled
|
||||
:embedding-disabled)
|
||||
:embedding-app-origin-set (boolean (embedding-app-origin))
|
||||
:number-embedded-questions (t2/count :model/Card :enable_embedding true)
|
||||
:number-embedded-dashboards (t2/count :model/Dashboard :enable_embedding true)}))))
|
||||
:deprecated "0.51.0"
|
||||
:setter (make-embedding-toggle-setter :enable-embedding "embedding"))
|
||||
|
||||
(defsetting embedding-app-origin
|
||||
(deferred-tru "Allow this origin to embed the full Metabase application.")
|
||||
;; This value is usually gated by [[enable-embedding]]
|
||||
:feature :embedding
|
||||
:type :string
|
||||
:export? false
|
||||
:visibility :public
|
||||
:audit :getter
|
||||
:encryption :no)
|
||||
|
||||
(defsetting enable-embedding-sdk
|
||||
(deferred-tru "Allow admins to embed Metabase via the SDK?")
|
||||
:type :boolean
|
||||
:default false
|
||||
:visibility :authenticated
|
||||
:export? false
|
||||
:audit :getter
|
||||
:setter (make-embedding-toggle-setter :enable-embedding-sdk "sdk-embedding"))
|
||||
|
||||
(mu/defn- ignore-localhost :- :string
|
||||
"Remove localhost:* or localhost:<port> from the list of origins."
|
||||
[s :- [:maybe :string]]
|
||||
(->> (str/split (or s "") #"\s+")
|
||||
(remove #(re-matches #"localhost:(\*|\d+)" %))
|
||||
distinct
|
||||
(str/join " ")
|
||||
str/trim))
|
||||
|
||||
(mu/defn- add-localhost :- :string [s :- [:maybe :string]]
|
||||
(->> s ignore-localhost (str "localhost:* ") str/trim))
|
||||
|
||||
(defn embedding-app-origins-sdk-setter
|
||||
"The setter for [[embedding-app-origins-sdk]].
|
||||
|
||||
Checks that we have SDK embedding feature and that it's enabled, then sets the value accordingly."
|
||||
[new-value]
|
||||
(add-localhost ;; return the same value that is returned from the getter
|
||||
(when (and (premium-features/has-feature? :embedding-sdk)
|
||||
;; Cannot set the SDK origins if the SDK embedding is disabled. so it will remain localhost:*.
|
||||
(setting/get-value-of-type :boolean :enable-embedding-sdk))
|
||||
(->> new-value
|
||||
ignore-localhost
|
||||
(setting/set-value-of-type! :string :embedding-app-origins-sdk)))))
|
||||
|
||||
(defsetting embedding-app-origins-sdk
|
||||
(deferred-tru "Allow this origin to embed Metabase SDK")
|
||||
:type :string
|
||||
:export? false
|
||||
:visibility :public
|
||||
:encryption :no
|
||||
:audit :getter
|
||||
:getter (fn embedding-app-origins-sdk-getter []
|
||||
(add-localhost (setting/get-value-of-type :string :embedding-app-origins-sdk)))
|
||||
:setter embedding-app-origins-sdk-setter)
|
||||
|
||||
(defsetting enable-embedding-static
|
||||
(deferred-tru "Allow admins to embed Metabase via static embedding?")
|
||||
:type :boolean
|
||||
:default false
|
||||
:visibility :authenticated
|
||||
:export? false
|
||||
:audit :getter
|
||||
:setter (make-embedding-toggle-setter :enable-embedding-static "static-embedding"))
|
||||
|
||||
(defsetting enable-embedding-interactive
|
||||
(deferred-tru "Allow admins to embed Metabase via interactive embedding?")
|
||||
:feature :embedding
|
||||
:type :boolean
|
||||
:default false
|
||||
:visibility :authenticated
|
||||
:export? false
|
||||
:audit :getter
|
||||
:setter (make-embedding-toggle-setter :enable-embedding-interactive "interactive-embedding"))
|
||||
|
||||
;; settings for the embedding homepage
|
||||
(defsetting embedding-homepage
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[metabase.public-settings :as public-settings]
|
||||
[metabase.server.request.util :as req.util]
|
||||
[metabase.util.log :as log]
|
||||
[metabase.util.malli :as mu]
|
||||
[ring.util.codec :refer [base64-encode]])
|
||||
(:import
|
||||
(java.security MessageDigest SecureRandom)))
|
||||
@@ -45,9 +46,8 @@
|
||||
"Expires" "Tue, 03 Jul 2001 06:00:00 GMT"
|
||||
"Last-Modified" (t/format :rfc-1123-date-time (t/zoned-date-time))})
|
||||
|
||||
(defn- cache-far-future-headers
|
||||
(def cache-far-future-headers
|
||||
"Headers that tell browsers to cache a static resource for a long time."
|
||||
[]
|
||||
{"Cache-Control" "public, max-age=31536000"})
|
||||
|
||||
(def ^:private ^:const strict-transport-security-header
|
||||
@@ -113,21 +113,15 @@
|
||||
:manifest-src ["'self'"]}]
|
||||
(format "%s %s; " (name k) (str/join " " vs))))})
|
||||
|
||||
(defn- embedding-app-origin
|
||||
[]
|
||||
(when (and (embed.settings/enable-embedding) (embed.settings/embedding-app-origin))
|
||||
(embed.settings/embedding-app-origin)))
|
||||
|
||||
(defn- embedding-app-origin-sdk
|
||||
[]
|
||||
(when (embed.settings/enable-embedding)
|
||||
(str "localhost:* " (embed.settings/embedding-app-origin))))
|
||||
|
||||
(defn- content-security-policy-header-with-frame-ancestors
|
||||
[allow-iframes? nonce]
|
||||
(update (content-security-policy-header nonce)
|
||||
"Content-Security-Policy"
|
||||
#(format "%s frame-ancestors %s;" % (if allow-iframes? "*" (or (embedding-app-origin) "'none'")))))
|
||||
#(format "%s frame-ancestors %s;" % (if allow-iframes? "*"
|
||||
(if-let [eao (and (embed.settings/enable-embedding)
|
||||
(embed.settings/embedding-app-origin))]
|
||||
eao
|
||||
"'none'")))))
|
||||
|
||||
(defn parse-url
|
||||
"Returns an object with protocol, domain and port for the given url"
|
||||
@@ -170,9 +164,10 @@
|
||||
(let [urls (str/split approved-origins-raw #" +")]
|
||||
(keep parse-url urls)))
|
||||
|
||||
(defn approved-origin?
|
||||
(mu/defn approved-origin?
|
||||
"Returns true if `origin` should be allowed for CORS based on the `approved-origins`"
|
||||
[raw-origin approved-origins-raw]
|
||||
[raw-origin :- [:maybe :string]
|
||||
approved-origins-raw :- [:maybe :string]]
|
||||
(boolean
|
||||
(when (and (seq raw-origin) (seq approved-origins-raw))
|
||||
(let [approved-list (parse-approved-origins approved-origins-raw)
|
||||
@@ -188,37 +183,28 @@
|
||||
"Returns headers for CORS requests"
|
||||
[origin]
|
||||
(merge
|
||||
(when
|
||||
(approved-origin? origin (embedding-app-origin-sdk))
|
||||
(when (approved-origin? origin (embed.settings/embedding-app-origins-sdk))
|
||||
{"Access-Control-Allow-Origin" origin
|
||||
"Vary" "Origin"})
|
||||
|
||||
{"Access-Control-Allow-Headers" "*"
|
||||
"Access-Control-Allow-Methods" "*"
|
||||
"Access-Control-Expose-Headers" "X-Metabase-Anti-CSRF-Token"}))
|
||||
|
||||
(defn- first-embedding-app-origin
|
||||
"Return only the first embedding app origin."
|
||||
[]
|
||||
(some-> (embedding-app-origin)
|
||||
(str/split #" ")
|
||||
first))
|
||||
{"Access-Control-Allow-Headers" "*"
|
||||
"Access-Control-Allow-Methods" "*"
|
||||
"Access-Control-Expose-Headers" "X-Metabase-Anti-CSRF-Token"}))
|
||||
|
||||
(defn security-headers
|
||||
"Fetch a map of security headers that should be added to a response based on the passed options."
|
||||
[& {:keys [origin nonce allow-iframes? allow-cache?]
|
||||
:or {allow-iframes? false, allow-cache? false}}]
|
||||
(merge
|
||||
(if allow-cache?
|
||||
(cache-far-future-headers)
|
||||
(cache-prevention-headers))
|
||||
(if allow-cache? cache-far-future-headers (cache-prevention-headers))
|
||||
strict-transport-security-header
|
||||
(content-security-policy-header-with-frame-ancestors allow-iframes? nonce)
|
||||
(when (embedding-app-origin-sdk) (access-control-headers origin))
|
||||
(when (embed.settings/enable-embedding-sdk)
|
||||
(access-control-headers origin))
|
||||
(when-not allow-iframes?
|
||||
;; Tell browsers not to render our site as an iframe (prevent clickjacking)
|
||||
{"X-Frame-Options" (if (embedding-app-origin)
|
||||
(format "ALLOW-FROM %s" (first-embedding-app-origin))
|
||||
{"X-Frame-Options" (if-let [eao (and (embed.settings/enable-embedding)
|
||||
(embed.settings/embedding-app-origin))]
|
||||
(format "ALLOW-FROM %s" (-> eao (str/split #" ") first))
|
||||
"DENY")})
|
||||
{;; Tell browser to block suspected XSS attacks
|
||||
"X-XSS-Protection" "1; mode=block"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns ^:mb/once metabase.api.setting-test
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer :all]
|
||||
[metabase.api.common.validation :as validation]
|
||||
[metabase.driver.h2 :as h2]
|
||||
@@ -7,7 +8,8 @@
|
||||
[metabase.models.setting-test :as models.setting-test]
|
||||
[metabase.test :as mt]
|
||||
[metabase.test.fixtures :as fixtures]
|
||||
[metabase.util.i18n :refer [deferred-tru]]))
|
||||
[metabase.util.i18n :refer [deferred-tru]]
|
||||
[metabase.util.log.capture :as log.capture]))
|
||||
|
||||
(comment h2/keep-me)
|
||||
|
||||
@@ -375,3 +377,14 @@
|
||||
(mt/user-http-request :crowberto :put 204 "setting" {:test_setting_1 "GHI", :test_setting_2 "JKL"})
|
||||
(is (= "GHI" (mt/user-http-request :crowberto :get 200 "setting/test_setting_1")))
|
||||
(is (= "JKL" (mt/user-http-request :crowberto :get 200 "setting/test_setting_2")))))))))
|
||||
|
||||
(defsetting test-deprecated-setting
|
||||
(deferred-tru "Setting to test deprecation warning.")
|
||||
:deprecated "0.51.0"
|
||||
:encryption :no)
|
||||
|
||||
(deftest deprecation-warning-for-deprecated-setting-test
|
||||
(log.capture/with-log-messages-for-level [warnings :warn]
|
||||
(test-deprecated-setting! "hello")
|
||||
(is (re-find #"Setting test-deprecated-setting is deprecated as of Metabase 0.51.0"
|
||||
(str/join " " (map :message (warnings)))))))
|
||||
|
||||
@@ -184,17 +184,22 @@
|
||||
|
||||
(deftest test-access-control-headers?
|
||||
(testing "Should always allow localhost:*"
|
||||
(tu/with-temporary-setting-values [enable-embedding true
|
||||
embedding-app-origin nil]
|
||||
(tu/with-temporary-setting-values [enable-embedding-sdk true
|
||||
embedding-app-origins-sdk nil]
|
||||
(is (= "http://localhost:8080" (get (mw.security/access-control-headers "http://localhost:8080") "Access-Control-Allow-Origin")))))
|
||||
|
||||
(testing "Should disable CORS when embedding is disabled"
|
||||
(tu/with-temporary-setting-values [enable-embedding false
|
||||
embedding-app-origin nil]
|
||||
(is (= nil (get (mw.security/access-control-headers "http://localhost:8080") "Access-Control-Allow-Origin")))))
|
||||
(testing "Should disable CORS when enable-embedding-sdk is disabled"
|
||||
(tu/with-temporary-setting-values [enable-embedding-sdk false
|
||||
embedding-app-origins-sdk nil]
|
||||
(is (= "http://localhost:8080"
|
||||
(get (mw.security/access-control-headers "http://localhost:8080")
|
||||
"Access-Control-Allow-Origin"))
|
||||
"Localhost is always permitted.")
|
||||
(is (= nil (get (mw.security/access-control-headers "http://1.2.3.4:5555")
|
||||
"Access-Control-Allow-Origin")))))
|
||||
|
||||
(testing "Should work with embedding-app-origin"
|
||||
(mt/with-premium-features #{:embedding}
|
||||
(tu/with-temporary-setting-values [enable-embedding true
|
||||
embedding-app-origin "example.com"]
|
||||
(mt/with-premium-features #{:embedding-sdk}
|
||||
(tu/with-temporary-setting-values [enable-embedding-sdk true
|
||||
embedding-app-origins-sdk "example.com"]
|
||||
(is (= "https://example.com" (get (mw.security/access-control-headers "https://example.com") "Access-Control-Allow-Origin")))))))
|
||||
|
||||
Reference in New Issue
Block a user