feat(sdk, embed-js): Add Guest Embeds via Embed JS and SDK (#64850)
This commit is contained in:
@@ -8,6 +8,7 @@ export interface DashboardDetails extends Omit<CreateDashboardRequest, "name"> {
|
||||
name?: string;
|
||||
auto_apply_filters?: Dashboard["auto_apply_filters"];
|
||||
enable_embedding?: Dashboard["enable_embedding"];
|
||||
embedding_type?: Dashboard["embedding_type"];
|
||||
embedding_params?: Dashboard["embedding_params"];
|
||||
dashcards?: Partial<DashboardCard>[];
|
||||
}
|
||||
@@ -33,6 +34,7 @@ export const createDashboard = (
|
||||
name = "Test Dashboard",
|
||||
auto_apply_filters,
|
||||
enable_embedding,
|
||||
embedding_type,
|
||||
embedding_params,
|
||||
dashcards,
|
||||
...restDashboardDetails
|
||||
@@ -59,6 +61,7 @@ export const createDashboard = (
|
||||
return cy.request<Dashboard>("PUT", `/api/dashboard/${body.id}`, {
|
||||
auto_apply_filters,
|
||||
enable_embedding,
|
||||
embedding_type,
|
||||
embedding_params,
|
||||
dashcards,
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ export type QuestionDetails = {
|
||||
* Defaults to false.
|
||||
*/
|
||||
enable_embedding?: Card["enable_embedding"];
|
||||
embedding_type?: Card["embedding_type"];
|
||||
};
|
||||
|
||||
export type StructuredQuestionDetails = Omit<
|
||||
|
||||
31
e2e/support/helpers/e2e-content-translation-helpers.ts
Normal file
31
e2e/support/helpers/e2e-content-translation-helpers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { DictionaryArray } from "metabase-types/api";
|
||||
|
||||
export const getCSVWithHeaderRow = (dictionary: DictionaryArray) => {
|
||||
const header = ["Language", "String", "Translation"];
|
||||
return [header, ...dictionary]
|
||||
.map((row) => Object.values(row).join(","))
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
export const uploadTranslationDictionaryViaAPI = (rows: DictionaryArray) => {
|
||||
cy.signInAsAdmin();
|
||||
const csvString = getCSVWithHeaderRow(rows);
|
||||
|
||||
const fileBlob = new Blob([csvString], { type: "text/csv" });
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("file", fileBlob, "dictionary.csv");
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: "POST",
|
||||
url: "/api/ee/content-translation/upload-dictionary",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,13 @@
|
||||
import * as jose from "jose";
|
||||
|
||||
import { METABASE_SECRET_KEY } from "e2e/support/cypress_data";
|
||||
import {
|
||||
embedModalContent,
|
||||
embedModalEnableEmbedding,
|
||||
legacyStaticEmbeddingButton,
|
||||
} from "e2e/support/helpers/e2e-embedding-iframe-sdk-setup-helpers";
|
||||
import { modal, popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
|
||||
import { JWT_SHARED_SECRET } from "e2e/support/helpers/embedding-sdk-helpers/constants";
|
||||
|
||||
import { openSharingMenu } from "./e2e-sharing-helpers";
|
||||
|
||||
@@ -188,49 +196,72 @@ export function getEmbedModalSharingPane() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Static Embedding setup modal
|
||||
* Open Legacy Static Embedding setup modal
|
||||
* @param {object} params
|
||||
* @param {("question"|"dashboard")} [params.resource] - resource type
|
||||
* @param {(string|number)} [params.resourceId] - resource id
|
||||
* @param {("overview"|"parameters"|"lookAndFeel")} [params.activeTab] - modal tab to open
|
||||
* @param {("code"|"preview")} [params.previewMode] - preview mode type to activate
|
||||
* @param {boolean} [params.acceptTerms] - whether we need to go through the legalese step
|
||||
* @param {boolean} [params.unpublishBeforeOpen] - either unpublish entity before legacy modal is opened or not
|
||||
*/
|
||||
export function openStaticEmbeddingModal({
|
||||
export function openLegacyStaticEmbeddingModal({
|
||||
resource,
|
||||
resourceId,
|
||||
activeTab,
|
||||
previewMode,
|
||||
acceptTerms = true,
|
||||
confirmSave,
|
||||
unpublishBeforeOpen = true,
|
||||
} = {}) {
|
||||
const apiPath = resource === "question" ? "card" : "dashboard";
|
||||
|
||||
cy.request("PUT", `/api/${apiPath}/${resourceId}`, {
|
||||
enable_embedding: true,
|
||||
embedding_type: "static-legacy",
|
||||
});
|
||||
|
||||
openSharingMenu("Embed");
|
||||
|
||||
if (confirmSave) {
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
}
|
||||
embedModalContent().should("exist");
|
||||
|
||||
cy.findByText("Static embedding").click();
|
||||
cy.get("body").then(($body) => {
|
||||
const isEmbeddingDisabled =
|
||||
$body.find('[data-testid="enable-embedding-card"]').length > 0;
|
||||
|
||||
if (acceptTerms) {
|
||||
cy.findByTestId("accept-legalese-terms-button").click();
|
||||
}
|
||||
|
||||
modal().within(() => {
|
||||
if (activeTab) {
|
||||
const tabKeyToNameMap = {
|
||||
overview: "Overview",
|
||||
parameters: "Parameters",
|
||||
lookAndFeel: "Look and Feel",
|
||||
};
|
||||
|
||||
cy.findByRole("tab", { name: tabKeyToNameMap[activeTab] }).click();
|
||||
if (isEmbeddingDisabled) {
|
||||
embedModalEnableEmbedding();
|
||||
}
|
||||
|
||||
if (previewMode) {
|
||||
const previewModeToKeyMap = {
|
||||
code: "Code",
|
||||
preview: "Preview",
|
||||
};
|
||||
embedModalContent().within(() => {
|
||||
legacyStaticEmbeddingButton().click();
|
||||
});
|
||||
|
||||
cy.findByText(previewModeToKeyMap[previewMode]).click();
|
||||
}
|
||||
modal().within(() => {
|
||||
cy.findByText("Static embedding").should("be.visible");
|
||||
|
||||
// Some tests after a modal is opened expect the entity to be unpublished;
|
||||
// Other tests want to keep it published.
|
||||
if (unpublishBeforeOpen) {
|
||||
unpublishChanges(apiPath);
|
||||
}
|
||||
|
||||
if (activeTab) {
|
||||
const tabKeyToNameMap = {
|
||||
overview: "Overview",
|
||||
parameters: "Parameters",
|
||||
lookAndFeel: "Look and Feel",
|
||||
};
|
||||
|
||||
cy.findByRole("tab", { name: tabKeyToNameMap[activeTab] }).click();
|
||||
}
|
||||
|
||||
if (previewMode) {
|
||||
const previewModeToKeyMap = {
|
||||
code: "Code",
|
||||
preview: "Preview",
|
||||
};
|
||||
|
||||
cy.findByText(previewModeToKeyMap[previewMode]).click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,7 +272,7 @@ export function closeStaticEmbeddingModal() {
|
||||
/**
|
||||
* Publish a static dashboard or question
|
||||
* @param {"card" | "dashboard"} apiPath
|
||||
* @param callback
|
||||
* @param [callback]
|
||||
*/
|
||||
export function publishChanges(apiPath, callback) {
|
||||
cy.intercept("PUT", `/api/${apiPath}/*`).as("publishChanges");
|
||||
@@ -265,7 +296,13 @@ export function publishChanges(apiPath, callback) {
|
||||
* @param callback
|
||||
*/
|
||||
export function unpublishChanges(apiPath, callback) {
|
||||
cy.intercept("PUT", `/api/${apiPath}/*`).as("unpublishChanges");
|
||||
cy.intercept("PUT", `/api/${apiPath}/*`, (req) => {
|
||||
const body = req.body;
|
||||
|
||||
if (body?.enable_embedding === false) {
|
||||
req.alias = "unpublishChanges";
|
||||
}
|
||||
});
|
||||
|
||||
cy.button("Unpublish").click();
|
||||
|
||||
@@ -336,3 +373,34 @@ export const visitFullAppEmbeddingUrl = ({ url, qs, onBeforeLoad }) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {number} options.resourceId
|
||||
* @param {'question' | 'dashboard'} options.resourceType
|
||||
* @param [options.params]
|
||||
* @param [options.expirationMinutes]
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export const getSignedJwtForResource = async ({
|
||||
resourceId,
|
||||
resourceType,
|
||||
params = {},
|
||||
expirationMinutes = 10,
|
||||
}) => {
|
||||
const secret = new TextEncoder().encode(JWT_SHARED_SECRET);
|
||||
|
||||
const iat = Math.round(new Date().getTime() / 1000);
|
||||
const exp = iat + 60 * expirationMinutes;
|
||||
|
||||
const payload = {
|
||||
resource: { [resourceType]: resourceId },
|
||||
params,
|
||||
iat,
|
||||
exp,
|
||||
};
|
||||
|
||||
return new jose.SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { openSharingMenu } from "e2e/support/helpers/e2e-sharing-helpers";
|
||||
import { JWT_SHARED_SECRET } from "e2e/support/helpers/embedding-sdk-helpers/constants";
|
||||
import type { MetabaseTheme } from "metabase/embedding-sdk/theme/MetabaseTheme";
|
||||
import type { CreateApiKeyResponse } from "metabase-types/api";
|
||||
|
||||
import { createApiKey } from "./api";
|
||||
import { createApiKey, updateSetting } from "./api";
|
||||
import { getIframeBody } from "./e2e-embedding-helpers";
|
||||
import { enableJwtAuth } from "./e2e-jwt-helpers";
|
||||
import { restore } from "./e2e-setup-helpers";
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
mockAuthProviderAndJwtSignIn,
|
||||
} from "./embedding-sdk-testing";
|
||||
|
||||
const { IS_ENTERPRISE } = Cypress.env();
|
||||
|
||||
const EMBED_JS_PATH = "http://localhost:4000/app/embed.js";
|
||||
|
||||
/**
|
||||
@@ -22,6 +25,7 @@ const EMBED_JS_PATH = "http://localhost:4000/app/embed.js";
|
||||
export interface BaseEmbedTestPageOptions {
|
||||
// Passed to defineMetabaseConfig
|
||||
metabaseConfig?: {
|
||||
isGuest?: boolean;
|
||||
instanceUrl?: string;
|
||||
apiKey?: string;
|
||||
useExistingUserSession?: boolean;
|
||||
@@ -217,6 +221,47 @@ export function prepareSdkIframeEmbedTest({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the testing environment for sdk iframe embedding tests in guest embed mode.
|
||||
*/
|
||||
export function prepareGuestEmbedSdkIframeEmbedTest({
|
||||
withTokenFeatures = true,
|
||||
onPrepare,
|
||||
}: {
|
||||
withTokenFeatures?: boolean;
|
||||
onPrepare?: () => void;
|
||||
} = {}) {
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
if (IS_ENTERPRISE) {
|
||||
if (withTokenFeatures) {
|
||||
activateToken("bleeding-edge");
|
||||
} else {
|
||||
activateToken("starter");
|
||||
}
|
||||
}
|
||||
|
||||
onPrepare?.();
|
||||
|
||||
cy.request("PUT", "/api/setting/enable-embedding-simple", {
|
||||
value: true,
|
||||
});
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
|
||||
cy.intercept("GET", "/api/embed/card/*").as("getCard");
|
||||
cy.intercept("GET", "/api/embed/card/*/query*").as("getCardQuery");
|
||||
cy.intercept("GET", "/api/embed/pivot/card/*/query*").as("getCardPivotQuery");
|
||||
|
||||
updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
|
||||
mockEmbedJsToDevServer();
|
||||
|
||||
cy.signOut();
|
||||
}
|
||||
|
||||
type EnabledAuthMethods = "jwt" | "saml" | "api-key";
|
||||
|
||||
function setupMockAuthProviders(enabledAuthMethods: EnabledAuthMethods[]) {
|
||||
@@ -265,6 +310,7 @@ export const getNewEmbedScriptTag = ({
|
||||
|
||||
export const getNewEmbedConfigurationScript = ({
|
||||
instanceUrl = "http://localhost:4000",
|
||||
isGuest,
|
||||
theme,
|
||||
apiKey,
|
||||
useExistingUserSession,
|
||||
@@ -273,6 +319,7 @@ export const getNewEmbedConfigurationScript = ({
|
||||
}: BaseEmbedTestPageOptions["metabaseConfig"] = {}) => {
|
||||
const config = {
|
||||
instanceUrl,
|
||||
isGuest,
|
||||
apiKey,
|
||||
useExistingUserSession,
|
||||
theme,
|
||||
@@ -343,6 +390,4 @@ export const mockEmbedJsToDevServer = () => {
|
||||
|
||||
export function openEmbedJsModal() {
|
||||
openSharingMenu("Embed");
|
||||
|
||||
cy.findByText("Embedded Analytics JS").click();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const embedModalContent = () =>
|
||||
cy.findByTestId("sdk-iframe-embed-setup-modal-content");
|
||||
|
||||
export const embedModalEnableEmbeddingCard = () =>
|
||||
cy.findByTestId("enable-embedding-card");
|
||||
|
||||
export const embedModalEnableEmbedding = () =>
|
||||
embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText(/(Agree and (continue|enable)|Enable)/).click();
|
||||
});
|
||||
|
||||
export const legacyStaticEmbeddingButton = () =>
|
||||
cy.findByTestId("legacy-static-embedding-button");
|
||||
@@ -105,11 +105,11 @@ export const cypressWaitAll = function (commands) {
|
||||
*/
|
||||
export function visitQuestion(questionIdOrAlias) {
|
||||
if (typeof questionIdOrAlias === "number") {
|
||||
visitQuestionById(questionIdOrAlias);
|
||||
return visitQuestionById(questionIdOrAlias);
|
||||
}
|
||||
|
||||
if (typeof questionIdOrAlias === "string") {
|
||||
cy.get(questionIdOrAlias).then((id) => visitQuestionById(id));
|
||||
return cy.get(questionIdOrAlias).then((id) => visitQuestionById(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ function visitQuestionById(id) {
|
||||
|
||||
cy.wait("@" + metadataAlias);
|
||||
cy.wait("@" + alias);
|
||||
|
||||
return cy.wrap(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { StaticDashboard, StaticQuestion } from "@metabase/embedding-sdk-react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { mountSdkContent } from "./component-embedding-sdk-helpers";
|
||||
|
||||
export function mountGuestEmbedQuestion(
|
||||
extraProps: Partial<ComponentProps<typeof StaticQuestion>> = {},
|
||||
{
|
||||
shouldAssertCardQuery,
|
||||
locale,
|
||||
}: { shouldAssertCardQuery?: boolean; locale?: string } = {
|
||||
shouldAssertCardQuery: true,
|
||||
},
|
||||
) {
|
||||
cy.intercept("GET", "/api/embed/card/*").as("getCard");
|
||||
cy.intercept("POST", "/api/embed/card/*/query").as("cardQuery");
|
||||
cy.intercept("GET", "/api/embed/pivot/card/*/query*").as("getCardPivotQuery");
|
||||
|
||||
mountSdkContent(<StaticQuestion {...extraProps} />, {
|
||||
sdkProviderProps: { authConfig: { isGuest: true }, locale },
|
||||
});
|
||||
|
||||
if (shouldAssertCardQuery) {
|
||||
cy.wait("@getCard").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function mountGuestEmbedDashboard(
|
||||
extraProps: Partial<ComponentProps<typeof StaticDashboard>> = {},
|
||||
) {
|
||||
cy.intercept("GET", "/api/embed/dashboard/*").as("getDashboard");
|
||||
|
||||
mountSdkContent(<StaticDashboard {...extraProps} />, {
|
||||
sdkProviderProps: { authConfig: { isGuest: true } },
|
||||
});
|
||||
|
||||
cy.wait("@getDashboard").then(({ response }) => {
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { JSX } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { METABASE_INSTANCE_URL } from "e2e/support/helpers";
|
||||
import type { DeepPartial } from "metabase/embedding-sdk/types/utils";
|
||||
import { ThemeProvider } from "metabase/ui";
|
||||
|
||||
export const DEFAULT_SDK_AUTH_PROVIDER_CONFIG = {
|
||||
@@ -26,7 +27,7 @@ export function mountSdk(
|
||||
}
|
||||
|
||||
export interface MountSdkContentOptions extends MountSdkOptions {
|
||||
sdkProviderProps?: Partial<MetabaseProviderProps>;
|
||||
sdkProviderProps?: DeepPartial<MetabaseProviderProps>;
|
||||
waitForUser?: boolean;
|
||||
}
|
||||
|
||||
@@ -38,7 +39,11 @@ export function mountSdkContent(
|
||||
waitForUser = true,
|
||||
}: MountSdkContentOptions = {},
|
||||
) {
|
||||
cy.intercept("GET", "/api/user/current").as("getUser");
|
||||
const isGuest = !!sdkProviderProps?.authConfig?.isGuest;
|
||||
|
||||
if (!isGuest) {
|
||||
cy.intercept("GET", "/api/user/current").as("getUser");
|
||||
}
|
||||
|
||||
const reactNode = (
|
||||
<ThemeProvider>
|
||||
@@ -60,7 +65,7 @@ export function mountSdkContent(
|
||||
cy.mount(reactNode);
|
||||
}
|
||||
|
||||
if (waitForUser) {
|
||||
if (!isGuest && waitForUser) {
|
||||
// When running stress tests with network throttling, the request can take longer to complete
|
||||
// as it first needs to fetch the bundle from the server
|
||||
cy.wait("@getUser", { timeout: 20_000 }).then(({ response }) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export function mountInteractiveQuestion(
|
||||
|
||||
export function mountStaticQuestion(
|
||||
extraProps: Partial<ComponentProps<typeof StaticQuestion>> = {},
|
||||
{ shouldAssertCardQuery }: MountQuestionOptions = {
|
||||
{ shouldAssertCardQuery, sdkProviderProps }: MountQuestionOptions = {
|
||||
shouldAssertCardQuery: true,
|
||||
},
|
||||
) {
|
||||
@@ -48,7 +48,10 @@ export function mountStaticQuestion(
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
|
||||
cy.get<number>("@questionId").then((questionId) => {
|
||||
mountSdkContent(<StaticQuestion questionId={questionId} {...extraProps} />);
|
||||
mountSdkContent(
|
||||
<StaticQuestion questionId={questionId} {...extraProps} />,
|
||||
{ sdkProviderProps },
|
||||
);
|
||||
});
|
||||
|
||||
if (shouldAssertCardQuery) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./component-embedding-sdk-helpers";
|
||||
export * from "./component-embedding-sdk-question-helpers";
|
||||
export * from "./component-embedding-sdk-console-helpers";
|
||||
export * from "./component-embedding-sdk-guest-embeds-helpers";
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
METABASE_INSTANCE_URL,
|
||||
activateToken,
|
||||
restore,
|
||||
updateSetting,
|
||||
} from "e2e/support/helpers";
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
@@ -77,6 +78,21 @@ export function signInAsAdminAndEnableEmbeddingSdk() {
|
||||
});
|
||||
}
|
||||
|
||||
export function signInAsAdminAndSetupGuestEmbedding({
|
||||
token,
|
||||
}: {
|
||||
token: "starter" | "pro-cloud";
|
||||
}) {
|
||||
Cypress.config("baseUrl", METABASE_INSTANCE_URL);
|
||||
|
||||
restore();
|
||||
|
||||
cy.signInAsAdmin();
|
||||
|
||||
activateToken(token);
|
||||
updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
}
|
||||
|
||||
const MOCK_SAML_IDP_URI = "https://example.test/saml";
|
||||
|
||||
export function enableSamlAuth() {
|
||||
|
||||
@@ -27,6 +27,7 @@ export * from "./e2e-email-helpers";
|
||||
export * from "./e2e-embedding-helpers";
|
||||
export * from "./e2e-embedding-iframe-interactive-helpers";
|
||||
export * from "./e2e-embedding-iframe-sdk-helpers";
|
||||
export * from "./e2e-embedding-iframe-sdk-setup-helpers";
|
||||
export * from "./e2e-embedding-sdk-assertion-helpers";
|
||||
export * from "./e2e-enterprise-helpers";
|
||||
export * from "./e2e-filter-helpers";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { createQuestion, getSignedJwtForResource } from "e2e/support/helpers";
|
||||
import { uploadTranslationDictionaryViaAPI } from "e2e/support/helpers/e2e-content-translation-helpers";
|
||||
import { getSdkRoot } from "e2e/support/helpers/e2e-embedding-sdk-helpers";
|
||||
import { mountGuestEmbedQuestion } from "e2e/support/helpers/embedding-sdk-component-testing";
|
||||
import { signInAsAdminAndSetupGuestEmbedding } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
import type { Card } from "metabase-types/api";
|
||||
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
describe("scenarios > embedding-sdk > guest-embed-content-translations", () => {
|
||||
describe("question", () => {
|
||||
const setup = ({ display }: { display?: Card["display"] } = {}) => {
|
||||
signInAsAdminAndSetupGuestEmbedding({
|
||||
token: "pro-cloud",
|
||||
});
|
||||
|
||||
createQuestion({
|
||||
name: "Question for Guest Embed SDK",
|
||||
enable_embedding: true,
|
||||
embedding_type: "guest-embed",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["max", ["field", ORDERS.QUANTITY, null]]],
|
||||
breakout: [["field", ORDERS.PRODUCT_ID, null]],
|
||||
limit: 2,
|
||||
},
|
||||
display,
|
||||
}).then(({ body: question }) => {
|
||||
cy.wrap(question.id).as("questionId");
|
||||
});
|
||||
|
||||
cy.signOut();
|
||||
};
|
||||
|
||||
describe("content translation", () => {
|
||||
it("should show question content with applied content translation", () => {
|
||||
setup();
|
||||
|
||||
cy.signInAsAdmin();
|
||||
|
||||
uploadTranslationDictionaryViaAPI([
|
||||
{
|
||||
locale: "de",
|
||||
msgid: "Question for Guest Embed SDK",
|
||||
msgstr: "Override title für Deutsch",
|
||||
},
|
||||
{
|
||||
locale: "de",
|
||||
msgid: "Product ID",
|
||||
msgstr: "Override Product ID für Deutsch",
|
||||
},
|
||||
]);
|
||||
|
||||
cy.signOut();
|
||||
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion(
|
||||
{ token, title: true },
|
||||
{
|
||||
locale: "de",
|
||||
},
|
||||
);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Override title für Deutsch").should("be.visible");
|
||||
cy.findByText("Override Product ID für Deutsch").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { createQuestion, getSignedJwtForResource } from "e2e/support/helpers";
|
||||
import { getSdkRoot } from "e2e/support/helpers/e2e-embedding-sdk-helpers";
|
||||
import { mountGuestEmbedQuestion } from "e2e/support/helpers/embedding-sdk-component-testing";
|
||||
import { signInAsAdminAndSetupGuestEmbedding } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
describe("scenarios > embedding-sdk > guest-embed-error-handling", () => {
|
||||
beforeEach(() => {
|
||||
signInAsAdminAndSetupGuestEmbedding({
|
||||
token: "starter",
|
||||
});
|
||||
|
||||
createQuestion({
|
||||
name: "47563",
|
||||
enable_embedding: true,
|
||||
embedding_type: "guest-embed",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["max", ["field", ORDERS.QUANTITY, null]]],
|
||||
breakout: [["field", ORDERS.PRODUCT_ID, null]],
|
||||
limit: 2,
|
||||
},
|
||||
}).then(({ body: question }) => {
|
||||
cy.wrap(question.id).as("questionId");
|
||||
});
|
||||
|
||||
cy.signOut();
|
||||
});
|
||||
|
||||
it("should show JWT token error for invalid token", () => {
|
||||
cy.get("@questionId").then(async () => {
|
||||
mountGuestEmbedQuestion(
|
||||
{ token: "foo" },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Passed token is not a valid JWT token.").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show an error for an unpublished entity", () => {
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
cy.signInAsAdmin();
|
||||
cy.request("PUT", `/api/card/${questionId}`, { enable_embedding: false });
|
||||
cy.signOut();
|
||||
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion(
|
||||
{ token },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Embedding is not enabled for this object.").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show an error when an id is passed instead of a token", () => {
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
mountGuestEmbedQuestion(
|
||||
{ questionId },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText(
|
||||
"A valid JWT token is required to be passed in guest embeds mode.",
|
||||
).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,519 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
type NativeQuestionDetails,
|
||||
type StructuredQuestionDetails,
|
||||
createNativeQuestion,
|
||||
createNativeQuestionAndDashboard,
|
||||
createQuestion,
|
||||
createQuestionAndDashboard,
|
||||
getSignedJwtForResource,
|
||||
} from "e2e/support/helpers";
|
||||
import { getSdkRoot } from "e2e/support/helpers/e2e-embedding-sdk-helpers";
|
||||
import {
|
||||
mountGuestEmbedDashboard,
|
||||
mountGuestEmbedQuestion,
|
||||
} from "e2e/support/helpers/embedding-sdk-component-testing";
|
||||
import { signInAsAdminAndSetupGuestEmbedding } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
import { questionAsPinMapWithTiles } from "e2e/test/scenarios/embedding/shared/embedding-questions";
|
||||
import type { Card } from "metabase-types/api";
|
||||
import { createMockParameter } from "metabase-types/api/mocks";
|
||||
|
||||
const { ORDERS, ORDERS_ID, PEOPLE_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const WAIT_FOR_INTERNAL_API_REQUESTS_MS = 1000;
|
||||
|
||||
const PARAMETERS = [
|
||||
createMockParameter({
|
||||
id: "quantity",
|
||||
name: "Internal",
|
||||
slug: "quantity",
|
||||
type: "number/=",
|
||||
target: ["dimension", ["template-tag", "quantity"]],
|
||||
}),
|
||||
createMockParameter({
|
||||
id: "product_id_fk",
|
||||
name: "FK",
|
||||
slug: "product_id_fk",
|
||||
type: "id",
|
||||
target: ["dimension", ["template-tag", "product_id_fk"]],
|
||||
}),
|
||||
createMockParameter({
|
||||
id: "user_id_pk",
|
||||
name: "PK->Name",
|
||||
slug: "user_id_pk",
|
||||
type: "id",
|
||||
target: ["dimension", ["template-tag", "user_id_pk"]],
|
||||
values_source_type: "static-list",
|
||||
values_source_config: {
|
||||
values: ["74"],
|
||||
},
|
||||
}),
|
||||
];
|
||||
const TEMPLATE_TAGS = {
|
||||
quantity: {
|
||||
id: "quantity",
|
||||
name: "quantity",
|
||||
"display-name": "Internal",
|
||||
type: "dimension",
|
||||
"widget-type": "number/=",
|
||||
dimension: ["field", ORDERS.QUANTITY, null],
|
||||
},
|
||||
product_id_fk: {
|
||||
id: "product_id_fk",
|
||||
name: "product_id_fk",
|
||||
"display-name": "FK",
|
||||
type: "dimension",
|
||||
"widget-type": "id",
|
||||
dimension: ["field", ORDERS.PRODUCT_ID, null],
|
||||
},
|
||||
user_id_pk: {
|
||||
id: "user_id_pk",
|
||||
name: "user_id_pk",
|
||||
"display-name": "PK->Name",
|
||||
type: "dimension",
|
||||
"widget-type": "id",
|
||||
dimension: ["field", PEOPLE_ID, null],
|
||||
},
|
||||
};
|
||||
|
||||
describe("scenarios > embedding-sdk > guest-embed-happy-path", () => {
|
||||
describe("question", () => {
|
||||
const setup = ({
|
||||
display,
|
||||
createQuestionOverride,
|
||||
}: {
|
||||
display?: Card["display"];
|
||||
createQuestionOverride?: () => void;
|
||||
} = {}) => {
|
||||
signInAsAdminAndSetupGuestEmbedding({
|
||||
token: "starter",
|
||||
});
|
||||
|
||||
if (!createQuestionOverride) {
|
||||
createQuestion({
|
||||
name: "Question for Guest Embed SDK",
|
||||
enable_embedding: true,
|
||||
embedding_type: "guest-embed",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["max", ["field", ORDERS.QUANTITY, null]]],
|
||||
breakout: [["field", ORDERS.PRODUCT_ID, null]],
|
||||
limit: 2,
|
||||
},
|
||||
display,
|
||||
}).then(({ body: question }) => {
|
||||
cy.wrap(question.id).as("questionId");
|
||||
});
|
||||
} else {
|
||||
createQuestionOverride();
|
||||
}
|
||||
|
||||
cy.signOut();
|
||||
};
|
||||
|
||||
it("should show question content for unauthorized user", () => {
|
||||
setup();
|
||||
|
||||
cy.intercept("/api/user/*").as("internalApiRequest");
|
||||
cy.intercept("/api/card/*").as("internalApiRequest");
|
||||
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion({ token });
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Product ID").should("be.visible");
|
||||
cy.findByText("Max of Quantity").should("be.visible");
|
||||
});
|
||||
|
||||
cy.get("@internalApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show content of a question with `pivot table` type for unauthorized user", () => {
|
||||
setup({ display: "pivot" });
|
||||
|
||||
cy.intercept("/api/card/pivot/*/query*").as("internalApiRequest");
|
||||
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion(
|
||||
{ token },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
cy.wait("@getCardPivotQuery");
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Product ID").should("be.visible");
|
||||
cy.findByText("Max of Quantity").should("be.visible");
|
||||
});
|
||||
|
||||
cy.get("@internalApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show content of a question with `map` type with tiles for unauthorized user", () => {
|
||||
setup({
|
||||
createQuestionOverride: () => {
|
||||
createNativeQuestion(
|
||||
questionAsPinMapWithTiles as NativeQuestionDetails,
|
||||
{
|
||||
wrapId: true,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
cy.intercept("GET", "/api/embed/tiles/card/**").as("mapTilesApiRequest");
|
||||
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion(
|
||||
{ token },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
cy.wait("@mapTilesApiRequest");
|
||||
|
||||
cy.get("@mapTilesApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length.greaterThan(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should properly work with question parameters", () => {
|
||||
setup({
|
||||
createQuestionOverride: () => {
|
||||
createNativeQuestion(
|
||||
{
|
||||
name: "Orders native question",
|
||||
native: {
|
||||
query:
|
||||
"SELECT * " +
|
||||
"FROM ORDERS " +
|
||||
"JOIN PEOPLE ON ORDERS.USER_ID = PEOPLE.ID " +
|
||||
"WHERE {{quantity}} AND {{product_id_fk}} AND {{user_id_pk}}",
|
||||
"template-tags": TEMPLATE_TAGS,
|
||||
},
|
||||
parameters: PARAMETERS,
|
||||
enable_embedding: true,
|
||||
embedding_params: {
|
||||
quantity: "enabled",
|
||||
product_id_fk: "locked",
|
||||
user_id_pk: "enabled",
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
cy.intercept("GET", "/api/embed/card/*/params/*/values").as(
|
||||
"parameterValuesApiRequest",
|
||||
);
|
||||
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
params: {
|
||||
product_id_fk: 1,
|
||||
},
|
||||
});
|
||||
|
||||
mountGuestEmbedQuestion(
|
||||
{ token },
|
||||
{
|
||||
shouldAssertCardQuery: false,
|
||||
},
|
||||
);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findAllByTestId("parameter-widget").should("have.length", 2);
|
||||
|
||||
cy.findAllByTestId("parameter-widget")
|
||||
.filter(':contains("PK->Name")')
|
||||
.click();
|
||||
});
|
||||
|
||||
cy.wait("@parameterValuesApiRequest");
|
||||
|
||||
cy.get("@parameterValuesApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
|
||||
cy.findByTestId("table-body")
|
||||
.findAllByRole("row")
|
||||
.should("have.length.above", 10);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("parameter-value-dropdown").contains("74").click();
|
||||
cy.findByTestId("parameter-value-dropdown")
|
||||
.contains("Add filter")
|
||||
.click();
|
||||
|
||||
cy.findByTestId("table-body")
|
||||
.findAllByRole("row")
|
||||
.should("have.length", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dashboard", () => {
|
||||
const setup = ({
|
||||
display,
|
||||
questionDetails,
|
||||
}: {
|
||||
display?: Card["display"];
|
||||
questionDetails?: StructuredQuestionDetails;
|
||||
} = {}) => {
|
||||
signInAsAdminAndSetupGuestEmbedding({
|
||||
token: "starter",
|
||||
});
|
||||
|
||||
createQuestionAndDashboard({
|
||||
questionDetails: questionDetails ?? {
|
||||
name: "Sample Question",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["count"]],
|
||||
breakout: [
|
||||
["field", ORDERS.CREATED_AT, { "temporal-unit": "month" }],
|
||||
["field", ORDERS.QUANTITY, null],
|
||||
],
|
||||
},
|
||||
display,
|
||||
},
|
||||
dashboardDetails: {
|
||||
name: "Embedding SDK Test Dashboard",
|
||||
enable_embedding: true,
|
||||
embedding_type: "guest-embed",
|
||||
},
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
cy.wrap(dashboard_id).as("dashboardId");
|
||||
});
|
||||
|
||||
cy.signOut();
|
||||
};
|
||||
|
||||
it("should show dashboard content for unauthorized user", () => {
|
||||
cy.intercept("/api/user/*").as("internalApiRequest");
|
||||
cy.intercept("/api/dashboard/*").as("internalApiRequest");
|
||||
|
||||
setup();
|
||||
|
||||
cy.get("@dashboardId").then(async (dashboardId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: dashboardId as unknown as number,
|
||||
resourceType: "dashboard",
|
||||
});
|
||||
|
||||
mountGuestEmbedDashboard({ token });
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Embedding SDK Test Dashboard").should("be.visible");
|
||||
cy.findByText("Sample Question").should("be.visible");
|
||||
});
|
||||
|
||||
// Wait for requests
|
||||
cy.wait(WAIT_FOR_INTERNAL_API_REQUESTS_MS);
|
||||
|
||||
cy.get("@internalApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show dashboard content with a pivot card for unauthorized user", () => {
|
||||
cy.intercept("/api/user/*").as("internalApiRequest");
|
||||
cy.intercept("/api/dashboard/*").as("internalApiRequest");
|
||||
|
||||
setup({ display: "pivot" });
|
||||
|
||||
cy.get("@dashboardId").then(async (dashboardId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: dashboardId as unknown as number,
|
||||
resourceType: "dashboard",
|
||||
});
|
||||
|
||||
mountGuestEmbedDashboard({ token });
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText("Embedding SDK Test Dashboard").should("be.visible");
|
||||
});
|
||||
|
||||
// Wait for requests
|
||||
cy.wait(WAIT_FOR_INTERNAL_API_REQUESTS_MS);
|
||||
|
||||
cy.get("@internalApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show content of a question with `map` type with tiles for unauthorized user", () => {
|
||||
setup({
|
||||
questionDetails: {
|
||||
...questionAsPinMapWithTiles,
|
||||
query: {
|
||||
"source-table": PEOPLE_ID,
|
||||
},
|
||||
} as StructuredQuestionDetails,
|
||||
});
|
||||
|
||||
cy.intercept("GET", "/api/embed/tiles/dashboard/**").as(
|
||||
"mapTilesApiRequest",
|
||||
);
|
||||
|
||||
cy.get("@dashboardId").then(async (dashboardId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: dashboardId as unknown as number,
|
||||
resourceType: "dashboard",
|
||||
});
|
||||
|
||||
mountGuestEmbedDashboard({ token });
|
||||
|
||||
cy.wait("@mapTilesApiRequest");
|
||||
|
||||
cy.get("@mapTilesApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length.greaterThan(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should properly work with dashboard parameters", () => {
|
||||
signInAsAdminAndSetupGuestEmbedding({
|
||||
token: "starter",
|
||||
});
|
||||
|
||||
createNativeQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Orders native question",
|
||||
native: {
|
||||
query:
|
||||
"SELECT * " +
|
||||
"FROM ORDERS " +
|
||||
"JOIN PEOPLE ON ORDERS.USER_ID = PEOPLE.ID " +
|
||||
"WHERE {{quantity}} AND {{product_id_fk}} AND {{user_id_pk}}",
|
||||
"template-tags": TEMPLATE_TAGS,
|
||||
},
|
||||
},
|
||||
dashboardDetails: {
|
||||
name: "Embedding SDK Test Dashboard",
|
||||
embedding_type: "guest-embed",
|
||||
parameters: PARAMETERS,
|
||||
enable_embedding: true,
|
||||
embedding_params: {
|
||||
quantity: "enabled",
|
||||
product_id_fk: "locked",
|
||||
user_id_pk: "enabled",
|
||||
},
|
||||
},
|
||||
}).then(({ body: { id: dashcardId, card_id, dashboard_id } }) => {
|
||||
// Connect dashboard parameters to the card's template-tags
|
||||
cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
|
||||
dashcards: [
|
||||
{
|
||||
id: dashcardId,
|
||||
card_id,
|
||||
row: 0,
|
||||
col: 0,
|
||||
size_x: 16,
|
||||
size_y: 8,
|
||||
parameter_mappings: [
|
||||
{
|
||||
parameter_id: "quantity",
|
||||
card_id,
|
||||
target: ["dimension", ["template-tag", "quantity"]],
|
||||
},
|
||||
{
|
||||
parameter_id: "product_id_fk",
|
||||
card_id,
|
||||
target: ["dimension", ["template-tag", "product_id_fk"]],
|
||||
},
|
||||
{
|
||||
parameter_id: "user_id_pk",
|
||||
card_id,
|
||||
target: ["dimension", ["template-tag", "user_id_pk"]],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.wrap(dashboard_id).as("dashboardId");
|
||||
});
|
||||
|
||||
cy.signOut();
|
||||
|
||||
cy.intercept("GET", "/api/embed/dashboard/*/params/*/values").as(
|
||||
"parameterValuesApiRequest",
|
||||
);
|
||||
|
||||
cy.get("@dashboardId").then(async (dashboardId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: dashboardId as unknown as number,
|
||||
resourceType: "dashboard",
|
||||
params: {
|
||||
product_id_fk: 1,
|
||||
},
|
||||
});
|
||||
|
||||
mountGuestEmbedDashboard({ token });
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findAllByTestId("parameter-widget").should("have.length", 2);
|
||||
|
||||
cy.findAllByTestId("parameter-widget")
|
||||
.filter(':contains("PK->Name")')
|
||||
.click();
|
||||
});
|
||||
|
||||
cy.wait("@parameterValuesApiRequest");
|
||||
|
||||
cy.get("@parameterValuesApiRequest.all").then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
|
||||
cy.findByTestId("table-body")
|
||||
.findAllByRole("row")
|
||||
.should("have.length.above", 10);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("parameter-value-dropdown").contains("74").click();
|
||||
cy.findByTestId("parameter-value-dropdown")
|
||||
.contains("Add filter")
|
||||
.click();
|
||||
|
||||
cy.findByTestId("table-body")
|
||||
.findAllByRole("row")
|
||||
.should("have.length", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -354,16 +354,24 @@ const MODEL_NAME = "Test Action Model";
|
||||
});
|
||||
cy.visit(`/dashboard/${dashboard.id}`);
|
||||
cy.wrap(dashboard.id).as("dashboardId");
|
||||
|
||||
cy.log("The action should be visible in the dashboard");
|
||||
cy.findByRole("button", { name: "Create" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("Visit static embed dashboard");
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard.id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cy.log("The action should be visible in the dashboard");
|
||||
cy.findByRole("button", { name: "Create" }).should("be.visible");
|
||||
|
||||
cy.log("Visit static embed dashboard");
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
H.visitIframe();
|
||||
|
||||
cy.log("Assert static embed dashboard");
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
DashboardDetails,
|
||||
StructuredQuestionDetails,
|
||||
} from "e2e/support/helpers";
|
||||
import { uploadTranslationDictionaryViaAPI } from "e2e/support/helpers/e2e-content-translation-helpers";
|
||||
import {
|
||||
ORDERS_COUNT_BY_CREATED_AT_AND_PRODUCT_CATEGORY,
|
||||
PRODUCTS_COUNT_BY_CATEGORY_PIE,
|
||||
@@ -22,11 +23,10 @@ import {
|
||||
germanFieldNames,
|
||||
germanFieldValues,
|
||||
} from "./constants";
|
||||
import { uploadTranslationDictionaryViaAPI } from "./helpers/e2e-content-translation-helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > content translation > static embedding > dashboards", () => {
|
||||
describe("scenarios > content translation > static embeds > dashboards", () => {
|
||||
describe("pivot table renamed column (metabase#63296)", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -85,8 +85,9 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
@@ -156,7 +157,7 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
);
|
||||
});
|
||||
|
||||
it("should translate static embedding dashboard card titles and descriptions", () => {
|
||||
it("should translate guest embeds dashboard card titles and descriptions", () => {
|
||||
H.createDashboard({
|
||||
name: "the_dashboard",
|
||||
}).then(({ body: { id: dashboardId } }) => {
|
||||
@@ -167,8 +168,9 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
H.sidebar().findByText(PRODUCTS_COUNT_BY_CATEGORY_PIE.name).click();
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
@@ -263,8 +265,9 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
H.sidebar().findByText(PRODUCTS_COUNT_BY_CATEGORY_PIE.name).click();
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
@@ -307,7 +310,7 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
});
|
||||
});
|
||||
|
||||
it("should translate static embedding dashboard values on visualizer cards (metabase#62373)", () => {
|
||||
it("should translate guest embeds dashboard values on visualizer cards (metabase#62373)", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
|
||||
H.editDashboard();
|
||||
@@ -327,8 +330,9 @@ describe("scenarios > content translation > static embedding > dashboards", () =
|
||||
H.saveDashcardVisualizerModal({ mode: "create" });
|
||||
H.saveDashboard();
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: ORDERS_DASHBOARD_ID,
|
||||
});
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { parse } from "csv-parse/browser/esm/sync";
|
||||
|
||||
import { METABASE_SECRET_KEY } from "e2e/support/cypress_data";
|
||||
import { getCSVWithHeaderRow } from "e2e/support/helpers/e2e-content-translation-helpers";
|
||||
import type { DictionaryArray, DictionaryResponse } from "metabase-types/api";
|
||||
import { isDictionaryArray } from "metabase-types/guards/content-translation";
|
||||
|
||||
export const getCSVWithHeaderRow = (dictionary: DictionaryArray) => {
|
||||
const header = ["Language", "String", "Translation"];
|
||||
return [header, ...dictionary]
|
||||
.map((row) => Object.values(row).join(","))
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
export const uploadTranslationDictionary = (rows: DictionaryArray) => {
|
||||
cy.intercept("POST", "/api/ee/content-translation/upload-dictionary").as(
|
||||
"uploadDictionary",
|
||||
);
|
||||
cy.intercept("GET", "/api/setting").as("getSettings");
|
||||
cy.signInAsAdmin();
|
||||
cy.visit("/admin/embedding/static");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.wait("@getSettings");
|
||||
|
||||
cy.findByTestId("content-localization-setting").findByText(
|
||||
@@ -44,27 +38,6 @@ export const uploadTranslationDictionary = (rows: DictionaryArray) => {
|
||||
}).scrollIntoView();
|
||||
};
|
||||
|
||||
export const uploadTranslationDictionaryViaAPI = (rows: DictionaryArray) => {
|
||||
cy.signInAsAdmin();
|
||||
const csvString = getCSVWithHeaderRow(rows);
|
||||
const fileBlob = new Blob([csvString], { type: "text/csv" });
|
||||
const formData = new FormData();
|
||||
formData.append("file", fileBlob, "dictionary.csv");
|
||||
|
||||
return cy
|
||||
.request({
|
||||
method: "POST",
|
||||
url: "/api/ee/content-translation/upload-dictionary",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
};
|
||||
|
||||
export const assertOnlyTheseTranslationsAreStored = (
|
||||
rows: DictionaryArray,
|
||||
locale = "de",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { NORMAL_USER_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import { uploadTranslationDictionaryViaAPI } from "e2e/support/helpers/e2e-content-translation-helpers";
|
||||
|
||||
import { germanFieldNames } from "./constants";
|
||||
import { uploadTranslationDictionaryViaAPI } from "./helpers/e2e-content-translation-helpers";
|
||||
|
||||
const { PRODUCTS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > content translation > static embedding > questions", () => {
|
||||
describe("scenarios > content translation > guest embeds > questions", () => {
|
||||
describe("ee", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", "/api/ee/content-translation/upload-dictionary").as(
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import path from "path";
|
||||
|
||||
import {
|
||||
getCSVWithHeaderRow,
|
||||
uploadTranslationDictionaryViaAPI,
|
||||
} from "e2e/support/helpers/e2e-content-translation-helpers";
|
||||
|
||||
import {
|
||||
germanFieldNames,
|
||||
invalidLocaleXX,
|
||||
@@ -11,14 +16,12 @@ import {
|
||||
import {
|
||||
assertOnlyTheseTranslationsAreStored,
|
||||
generateLargeCSV,
|
||||
getCSVWithHeaderRow,
|
||||
uploadTranslationDictionary,
|
||||
uploadTranslationDictionaryViaAPI,
|
||||
} from "./helpers/e2e-content-translation-helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > admin > embedding > static embedding> content translation", () => {
|
||||
describe("scenarios > admin > embedding > guest embeds> content translation", () => {
|
||||
describe("oss", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -26,7 +29,7 @@ describe("scenarios > admin > embedding > static embedding> content translation"
|
||||
});
|
||||
|
||||
it("admin settings configuration form is not present", () => {
|
||||
cy.visit("/admin/embedding/static");
|
||||
cy.visit("/admin/embedding/guest");
|
||||
cy.findByTestId("content-translation-configuration").should("not.exist");
|
||||
});
|
||||
});
|
||||
@@ -52,7 +55,7 @@ describe("scenarios > admin > embedding > static embedding> content translation"
|
||||
describe("The translation download button", () => {
|
||||
it("downloads the stored translations", () => {
|
||||
uploadTranslationDictionaryViaAPI(germanFieldNames);
|
||||
cy.visit("/admin/embedding/static");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.findByTestId("content-translation-configuration")
|
||||
.button(/Get translation dictionary template/i)
|
||||
.click();
|
||||
@@ -180,7 +183,7 @@ describe("scenarios > admin > embedding > static embedding> content translation"
|
||||
});
|
||||
|
||||
it("rejects, in the frontend, a CSV upload that is too big", () => {
|
||||
cy.visit("/admin/embedding/static");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.get("#content-translation-dictionary-upload-input").selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from(
|
||||
@@ -206,7 +209,7 @@ describe("scenarios > admin > embedding > static embedding> content translation"
|
||||
});
|
||||
|
||||
it("rejects invalid CSV", () => {
|
||||
cy.visit("/admin/embedding/static");
|
||||
cy.visit("/admin/embedding");
|
||||
const validCSV = getCSVWithHeaderRow(germanFieldNames);
|
||||
const invalidCSV = validCSV + '\nde,Price,"Preis"X';
|
||||
|
||||
|
||||
@@ -1958,12 +1958,15 @@ describe("scenarios > dashboard > dashboard cards > click behavior", () => {
|
||||
})
|
||||
.then(({ body: dashCard }) => {
|
||||
H.visitDashboard(dashCard.dashboard_id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashCard.dashboard_id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
acceptTerms: false,
|
||||
});
|
||||
H.visitIframe();
|
||||
clickLineChartPoint();
|
||||
|
||||
|
||||
@@ -1123,7 +1123,10 @@ describe("scenarios > dashboard", () => {
|
||||
cy.contains("37.65");
|
||||
assertScrollBarExists();
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: ORDERS_DASHBOARD_ID,
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.icon("close").click();
|
||||
|
||||
@@ -554,7 +554,11 @@ describe("scenarios > dashboard > tabs", () => {
|
||||
cy.spy().as("secondTabQuerySpy"),
|
||||
).as("secondTabQuery");
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters", acceptTerms: true });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: ORDERS_DASHBOARD_ID,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
|
||||
// publish the embedded dashboard so that we can directly navigate to its url
|
||||
H.publishChanges("dashboard", () => {});
|
||||
|
||||
@@ -15,48 +15,43 @@ describe(
|
||||
cy.log("Navigate to Embedding admin section");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.log("Check that we're on the modular embedding page");
|
||||
cy.url().should("include", "/admin/embedding/modular");
|
||||
cy.get("main").findByText("Modular embedding").should("be.visible");
|
||||
|
||||
cy.log("Verify sidebar contains static smbedding link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Static/ })
|
||||
.should("have.attr", "href", "/admin/embedding/static");
|
||||
|
||||
cy.log("Verify sidebar contains interactive embedding");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Interactive/ })
|
||||
.should("have.attr", "href", "/admin/embedding/interactive");
|
||||
cy.log("Check that we're on the embedding settings page");
|
||||
cy.url().should("include", "/admin/embedding");
|
||||
cy.get("main").findByText("Embedding settings").should("be.visible");
|
||||
|
||||
cy.log("Verify sidebar does not contain setup guide");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Setup guide/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify 2 upsell icons are present in sidebar");
|
||||
cy.log("Verify sidebar does not contain guest embeds link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.icon("gem")
|
||||
.should("have.length", 2);
|
||||
.findByRole("link", { name: /Guest embeds/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify sidebar does not contain security settings link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Security/ })
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
it("should show interactive embedding upsell on oss", () => {
|
||||
it("should show embedding upsell on oss", () => {
|
||||
cy.visit("/admin/embedding/interactive");
|
||||
|
||||
cy.findByTestId("admin-layout-content").within(() => {
|
||||
cy.findByRole("heading", { name: "Interactive embedding" }).should(
|
||||
cy.findByRole("heading", { name: "Embedding settings" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("upsell gem icon should be visible");
|
||||
cy.icon("gem").should("be.visible");
|
||||
|
||||
cy.findByRole("link", { name: /Check out our Quickstart/i })
|
||||
.should("be.visible")
|
||||
.and("have.attr", "href")
|
||||
.and("contain", "interactive-embedding-quick-start-guide");
|
||||
|
||||
cy.findByRole("link", { name: "Try for free" }).should("be.visible");
|
||||
cy.findByRole("link", { name: "Upgrade" })
|
||||
.should("have.attr", "href")
|
||||
.and(
|
||||
"eq",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embedding-page&source_plan=oss&utm_users=10&utm_campaign=embedded-analytics-js",
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -68,9 +68,12 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
it("should be disabled by default but able to be set to editable and/or locked (metabase#20357)", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
acceptTerms: true,
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
cy.findByLabelText("Configuring parameters").as("allParameters");
|
||||
@@ -119,6 +122,9 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "locked",
|
||||
name: "enabled",
|
||||
source: "disabled",
|
||||
user_id: "disabled",
|
||||
not_used: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,11 +159,14 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
acceptTerms: false,
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
cy.get("@allParameters").findByText("Locked").click();
|
||||
H.popover().contains("Disabled").click();
|
||||
|
||||
@@ -168,6 +177,9 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
name: "disabled",
|
||||
id: "disabled",
|
||||
source: "disabled",
|
||||
user_id: "disabled",
|
||||
not_used: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,11 +247,15 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
});
|
||||
H.saveDashboard();
|
||||
|
||||
// Check that parameter visibility is correct
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
acceptTerms: true,
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
// Check that parameter visibility is correct
|
||||
H.assertEmbeddingParameter("Id", "Disabled");
|
||||
H.assertEmbeddingParameter("Name", "Editable");
|
||||
H.assertEmbeddingParameter("Source", "Disabled");
|
||||
@@ -251,7 +267,11 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
// "enabled" must be set by default for required params.
|
||||
H.publishChanges("dashboard", ({ request }) => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "disabled",
|
||||
name: "enabled",
|
||||
source: "disabled",
|
||||
user_id: "disabled",
|
||||
not_used: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -268,17 +288,24 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
it("should (dis)allow setting parameters as required for a published embedding", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
// Set an "editable" and "locked" parameters and leave the rest "disabled"
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
acceptTerms: true,
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
// Set an "editable" and "locked" parameters and leave the rest "disabled"
|
||||
H.setEmbeddingParameter("Name", "Editable");
|
||||
H.setEmbeddingParameter("Source", "Locked");
|
||||
H.publishChanges("dashboard", ({ request }) => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "disabled",
|
||||
name: "enabled",
|
||||
source: "locked",
|
||||
user_id: "disabled",
|
||||
not_used: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -394,9 +421,13 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
it("should allow searching dashboard parameters in preview embed modal", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
@@ -637,15 +668,24 @@ describe("scenarios > embedding > dashboard parameters with defaults", () => {
|
||||
});
|
||||
|
||||
it("card parameter defaults should apply for disabled parameters, but not for editable or locked parameters", () => {
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
// ID param is disabled by default
|
||||
H.setEmbeddingParameter("Name", "Editable");
|
||||
H.setEmbeddingParameter("Source", "Locked");
|
||||
H.publishChanges("dashboard", ({ request }) => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "disabled",
|
||||
source: "locked",
|
||||
name: "enabled",
|
||||
user_id: "disabled",
|
||||
not_used: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -723,7 +763,16 @@ describe("scenarios > embedding > dashboard parameters with defaults", () => {
|
||||
});
|
||||
|
||||
H.visitDashboard("@dashboardId");
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
cy.log("should show card results by default");
|
||||
@@ -781,19 +830,19 @@ describe("scenarios > embedding > dashboard appearance", () => {
|
||||
dashboardDetails,
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
|
||||
cy.intercept(
|
||||
"GET",
|
||||
"api/preview_embed/dashboard/*",
|
||||
cy.spy().as("previewEmbedSpy"),
|
||||
).as("previewEmbed");
|
||||
cy.intercept(
|
||||
"GET",
|
||||
"api/preview_embed/dashboard/*",
|
||||
cy.spy().as("previewEmbedSpy"),
|
||||
).as("previewEmbed");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
// EE users don't have to accept terms
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait("@previewEmbed");
|
||||
@@ -916,21 +965,21 @@ describe("scenarios > embedding > dashboard appearance", () => {
|
||||
})
|
||||
.then((dashboard) => {
|
||||
H.visitDashboard(dashboard.id);
|
||||
|
||||
cy.intercept(
|
||||
"GET",
|
||||
"api/preview_embed/dashboard/*",
|
||||
cy.spy().as("previewEmbedSpy"),
|
||||
).as("previewEmbed");
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard.id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept(
|
||||
"GET",
|
||||
"api/preview_embed/dashboard/*",
|
||||
cy.spy().as("previewEmbedSpy"),
|
||||
).as("previewEmbed");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
// EE users don't have to accept terms
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
cy.wait("@previewEmbed");
|
||||
|
||||
H.modal().within(() => {
|
||||
@@ -1220,10 +1269,11 @@ describe("scenarios > embedding > dashboard appearance", () => {
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitDashboard(dashboard_id);
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
|
||||
@@ -28,14 +28,27 @@ describe("scenarios > embedding > native questions", () => {
|
||||
visitQuestion: true,
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("should not display disabled parameters", () => {
|
||||
createAndVisitQuestion();
|
||||
|
||||
H.publishChanges("card", ({ request }) => {
|
||||
assert.deepEqual(request.body.embedding_params, {});
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "disabled",
|
||||
state: "disabled",
|
||||
created_at: "disabled",
|
||||
total: "disabled",
|
||||
source: "disabled",
|
||||
product_id: "disabled",
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
@@ -68,6 +81,7 @@ describe("scenarios > embedding > native questions", () => {
|
||||
total: "locked",
|
||||
state: "enabled",
|
||||
product_id: "enabled",
|
||||
source: "disabled",
|
||||
};
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
@@ -148,7 +162,12 @@ describe("scenarios > embedding > native questions", () => {
|
||||
// weren't touched and therefore aren't changed, whereas
|
||||
// "enabled" must be set by default for required params.
|
||||
const expected = {
|
||||
id: "disabled",
|
||||
state: "disabled",
|
||||
created_at: "disabled",
|
||||
total: "enabled",
|
||||
source: "disabled",
|
||||
product_id: "disabled",
|
||||
};
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
@@ -352,10 +371,15 @@ describe("scenarios > embedding > native questions", () => {
|
||||
});
|
||||
|
||||
it("locked parameters should still render results in the preview by default (metabase#47570)", () => {
|
||||
H.visitQuestion("@questionId");
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
H.visitQuestion("@questionId").then((id) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
cy.log("should show card results by default");
|
||||
@@ -377,15 +401,23 @@ describe("scenarios > embedding > native questions with default parameters", ()
|
||||
wrapId: true,
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
// Note: ID is disabled
|
||||
H.setEmbeddingParameter("Source", "Locked");
|
||||
H.setEmbeddingParameter("Name", "Editable");
|
||||
H.publishChanges("card", ({ request }) => {
|
||||
assert.deepEqual(request.body.embedding_params, {
|
||||
id: "disabled",
|
||||
source: "locked",
|
||||
name: "enabled",
|
||||
user_id: "disabled",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,10 +44,17 @@ describe("scenarios > embedding > questions", () => {
|
||||
display: "scalar",
|
||||
enable_embedding: true,
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
{ visitQuestion: true, wrapId: true },
|
||||
);
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
@@ -66,10 +73,14 @@ describe("scenarios > embedding > questions", () => {
|
||||
cy.request("PUT", `/api/card/${id}`, { enable_embedding: true });
|
||||
|
||||
H.visitQuestion(id);
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
cy.findByTestId("embed-frame").within(() => {
|
||||
@@ -106,9 +117,14 @@ describe("scenarios > embedding > questions", () => {
|
||||
cy.request("PUT", `/api/card/${id}`, { enable_embedding: true });
|
||||
|
||||
H.visitQuestion(id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
@@ -141,11 +157,16 @@ describe("scenarios > embedding > questions", () => {
|
||||
cy.request("PUT", `/api/card/${nestedId}`, { enable_embedding: true });
|
||||
|
||||
H.visitQuestion(nestedId);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: nestedId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
// Global (Data model) settings should be preserved
|
||||
@@ -178,9 +199,14 @@ describe("scenarios > embedding > questions", () => {
|
||||
cy.request("PUT", `/api/card/${id}`, { enable_embedding: true });
|
||||
|
||||
H.visitQuestion(id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
@@ -247,7 +273,7 @@ describe("scenarios [EE] > embedding > questions", () => {
|
||||
|
||||
cy.wait("@deLocale");
|
||||
|
||||
H.main().findByText("Februar 11, 2025, 9:40 PM");
|
||||
H.main().findByText("Februar 11, 2025, 9:40 PM").realHover();
|
||||
cy.findByRole("button", { name: "Ergebnis downloaden" }).should("exist");
|
||||
cy.url().should("include", "locale=de");
|
||||
});
|
||||
@@ -313,7 +339,11 @@ describe("scenarios > embedding > questions > downloads", () => {
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.visitQuestion(questionId);
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "lookAndFeel" });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "lookAndFeel",
|
||||
});
|
||||
|
||||
cy.log(
|
||||
"Embedding settings page should not show option to disable downloads",
|
||||
@@ -375,9 +405,10 @@ describe("scenarios > embedding > questions > downloads", () => {
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.visitQuestion(questionId);
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "lookAndFeel",
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
cy.log("Disable downloads");
|
||||
|
||||
@@ -141,14 +141,17 @@ describe("issue 15860", { tags: "@skip" }, () => {
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should work for locked linked filters connected to different cards with the same source table (metabase#15860)", () => {
|
||||
cy.icon("share").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Embed in your application").click();
|
||||
|
||||
setDefaultValueForLockedFilter("Q1 ID", 1);
|
||||
setDefaultValueForLockedFilter("Q2 ID", 3);
|
||||
|
||||
@@ -243,12 +246,22 @@ describe("issue 20438", () => {
|
||||
embedding_params: { [filter.slug]: "enabled" },
|
||||
});
|
||||
|
||||
cy.wrap(card_id).as("questionId");
|
||||
cy.wrap(dashboard_id).as("dashboardId");
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("dashboard filter connected to the field filter should work with a single value in embedded dashboards (metabase#20438)", () => {
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboardId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.visitIframe();
|
||||
|
||||
@@ -294,12 +307,22 @@ describe("locked parameters in embedded question (metabase#20634)", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
{
|
||||
visitQuestion: true,
|
||||
wrapId: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should let the user lock parameters to specific values", () => {
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
// select the dropdown next to the Text parameter so that we can set the value to "Locked"
|
||||
@@ -318,7 +341,7 @@ describe("locked parameters in embedded question (metabase#20634)", () => {
|
||||
cy.findByPlaceholderText("Text").type("foo{enter}");
|
||||
|
||||
// publish the embedded question so that we can directly navigate to its url
|
||||
cy.findByText("Publish").click();
|
||||
cy.findByText("Publish changes").click();
|
||||
cy.wait("@publishChanges");
|
||||
});
|
||||
|
||||
@@ -671,16 +694,18 @@ describe("issue 30535", () => {
|
||||
cy.request("PUT", `/api/card/${id}`, { enable_embedding: true });
|
||||
|
||||
H.visitQuestion(id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("user session should not apply sandboxing to a signed embedded question (metabase#30535)", () => {
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
cy.document().then((doc) => {
|
||||
const iframe = doc.querySelector("iframe");
|
||||
|
||||
@@ -790,11 +815,13 @@ describe("dashboard preview", () => {
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
@@ -875,11 +902,13 @@ describe("dashboard preview", () => {
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
// Makes it less likely to flake.
|
||||
@@ -943,7 +972,9 @@ describe("issue 40660", () => {
|
||||
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("static dashboard content shouldn't overflow its container (metabase#40660)", () => {
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails,
|
||||
dashboardDetails,
|
||||
@@ -954,13 +985,13 @@ describe("issue 40660", () => {
|
||||
});
|
||||
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("static dashboard content shouldn't overflow its container (metabase#40660)", () => {
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
H.getIframeBody().within(() => {
|
||||
@@ -991,20 +1022,23 @@ describe("issue 49142", { tags: "@skip" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("embedding preview should be always working", () => {
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails,
|
||||
dashboardDetails,
|
||||
}).then(({ body: { dashboard_id } }) => {
|
||||
H.visitDashboard(dashboard_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("embedding preview should be always working", () => {
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "lookAndFeel",
|
||||
previewMode: "preview",
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: dashboard_id,
|
||||
activeTab: "lookAndFeel",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
cy.findByTestId("embed-preview-iframe")
|
||||
.its("0.contentDocument.body")
|
||||
.should("be.visible")
|
||||
@@ -1505,9 +1539,17 @@ describe("issue 63687", () => {
|
||||
it("should properly display pin map tiles without auth errors for a valid JWT token", () => {
|
||||
H.createNativeQuestion(questionAsPinMapWithTiles, {
|
||||
visitQuestion: true,
|
||||
wrapId: true,
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
cy.get("@questionId").then((questionId) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: questionId,
|
||||
activeTab: "parameters",
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept("/api/embed/tiles/**").as("getTiles");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const standalonePath = "/admin/embedding/static";
|
||||
const standalonePath = "/admin/embedding/guest";
|
||||
|
||||
// These tests will run on both OSS and EE instances. Both without a token!
|
||||
describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
@@ -39,29 +39,23 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
it("should show the sdk upsell link in oss", () => {
|
||||
cy.visit("/admin/embedding/modular");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
mainPage().within(() => {
|
||||
cy.findByRole("link", { name: "Try for free" })
|
||||
cy.findByRole("link", { name: "Upgrade" })
|
||||
.should("have.attr", "href")
|
||||
.and(
|
||||
"eq",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=product&utm_medium=upsell&utm_campaign=embedded-analytics-js&utm_content=embedding-page&source_plan=oss",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embedding-page&source_plan=oss&utm_users=10&utm_campaign=embedded-analytics-js",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not let you embed the question", () => {
|
||||
it("should not let you use non-guest auth methods", () => {
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
|
||||
ensureEmbeddingIsDisabled();
|
||||
});
|
||||
|
||||
it("should not let you embed the dashboard", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
|
||||
ensureEmbeddingIsDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
context("embedding enabled", () => {
|
||||
@@ -238,7 +232,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
visitAndEnableSharing("question");
|
||||
visitAndEnableSharing("question", false);
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByRole("tab", { name: "Parameters" }).click();
|
||||
@@ -302,29 +296,34 @@ function resetEmbedding() {
|
||||
}
|
||||
|
||||
function ensureEmbeddingIsDisabled() {
|
||||
H.openSharingMenu();
|
||||
H.sharingMenu()
|
||||
.findByRole("menuitem", { name: "Embed" })
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
H.modal()
|
||||
.findByRole("article", { name: "Static embedding" })
|
||||
.within(() => {
|
||||
cy.findByText("Disabled.").should("be.visible");
|
||||
cy.findByText("Enable in admin settings").should("be.visible");
|
||||
});
|
||||
H.openEmbedJsModal();
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.embedModalContent().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("be.disabled");
|
||||
cy.findByLabelText("Existing Metabase session").should("be.disabled");
|
||||
});
|
||||
}
|
||||
|
||||
function visitAndEnableSharing(object, acceptTerms = true) {
|
||||
if (object === "question") {
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
}
|
||||
function visitAndEnableSharing(object, unpublishBeforeOpen = true) {
|
||||
const { id, visitFunction } = {
|
||||
question: {
|
||||
id: ORDERS_QUESTION_ID,
|
||||
visitFunction: H.visitQuestion,
|
||||
},
|
||||
dashboard: {
|
||||
id: ORDERS_DASHBOARD_ID,
|
||||
visitFunction: H.visitDashboard,
|
||||
},
|
||||
}[object];
|
||||
|
||||
if (object === "dashboard") {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
}
|
||||
visitFunction(id);
|
||||
|
||||
H.openStaticEmbeddingModal({ acceptTerms });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: object,
|
||||
resourceId: id,
|
||||
unpublishBeforeOpen,
|
||||
});
|
||||
}
|
||||
|
||||
function mainPage() {
|
||||
|
||||
@@ -28,7 +28,11 @@ tokens.forEach((token) => {
|
||||
const defaultDownloadsValue =
|
||||
token === "pro-self-hosted" ? true : undefined;
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
H.openStaticEmbeddingModal({ acceptTerms: false });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: ORDERS_DASHBOARD_ID,
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByText(
|
||||
@@ -127,7 +131,11 @@ tokens.forEach((token) => {
|
||||
const defaultDownloadsValue =
|
||||
token === "pro-self-hosted" ? true : undefined;
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
H.openStaticEmbeddingModal({ acceptTerms: false });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "question",
|
||||
resourceId: ORDERS_QUESTION_ID,
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByText(
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > embedding > upsells", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.updateSetting("show-sdk-embed-terms", false);
|
||||
});
|
||||
|
||||
it("should show the sdk settings page with upsell button", () => {
|
||||
cy.on("window:before:load", (win) => {
|
||||
// prevent Cypress opening in a new window/tab and spy on this method
|
||||
cy.stub(win, "open").as("open");
|
||||
});
|
||||
|
||||
cy.visit("/admin/embedding/modular");
|
||||
|
||||
mainPage().within(() => {
|
||||
cy.contains("SDK for React").should("be.visible");
|
||||
cy.contains("Embedded Analytics JS").should("be.visible");
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Try for free" }).click();
|
||||
|
||||
cy.get<sinon.SinonSpy>("@open").should((spy) => {
|
||||
const url = spy.getCall(0).args[0];
|
||||
|
||||
expect(url).to.include(
|
||||
"https://test-store.metabase.com/checkout/upgrade/self-hosted",
|
||||
);
|
||||
|
||||
expect(url).to.match(/utm_source=product/);
|
||||
expect(url).to.match(/utm_medium=upsell/);
|
||||
expect(url).to.match(/utm_campaign=embedded-analytics-js/);
|
||||
expect(url).to.match(/utm_content=embedding-page/);
|
||||
expect(url).to.match(/source_plan=oss/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mainPage() {
|
||||
return cy.findByTestId("admin-layout-content");
|
||||
}
|
||||
@@ -59,6 +59,9 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
});
|
||||
|
||||
cy.wait("@dashboard");
|
||||
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
cy.get("[data-iframe-loaded]", { timeout: 20000 }).should("have.length", 1);
|
||||
|
||||
H.modal().should("exist");
|
||||
|
||||
@@ -1,71 +1,139 @@
|
||||
import { embedModalEnableEmbeddingCard } from "e2e/support/helpers";
|
||||
|
||||
import { getEmbedSidebar } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > embedding > sdk iframe embed setup > enable embed js", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
H.updateSetting("enable-embedding-simple", false);
|
||||
const { IS_ENTERPRISE } = Cypress.env();
|
||||
const IS_OSS = !IS_ENTERPRISE;
|
||||
const MB_EDITION = IS_ENTERPRISE ? "ee" : "oss";
|
||||
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
const DATA_BY_MB_EDITION = {
|
||||
oss: {
|
||||
cardTestId: "guest-embeds-setting-card",
|
||||
cardText:
|
||||
"To continue, enable Guest embeds and agree to the usage conditions.",
|
||||
embeddingSettingName: "enable-embedding-static",
|
||||
showTermsSettingName: "show-static-embed-terms",
|
||||
},
|
||||
ee: {
|
||||
cardTestId: "sdk-setting-card",
|
||||
cardText:
|
||||
"To continue, enable Embedded Analytics JS and agree to the usage conditions.",
|
||||
embeddingSettingName: "enable-embedding-simple",
|
||||
showTermsSettingName: "show-simple-embed-terms",
|
||||
},
|
||||
} as const;
|
||||
|
||||
it("shows the Enable to Continue button and enables embedding on click", () => {
|
||||
cy.visit("/admin/embedding");
|
||||
describe(
|
||||
`scenarios > embedding > sdk iframe embed setup > enable embed js > ${MB_EDITION}`,
|
||||
{ ...(IS_OSS && { tags: "@OSS" }) },
|
||||
() => {
|
||||
const { embeddingSettingName, showTermsSettingName, cardTestId, cardText } =
|
||||
DATA_BY_MB_EDITION[MB_EDITION];
|
||||
|
||||
cy.findAllByTestId("sdk-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
cy.log("shows tooltip with fair usage info");
|
||||
getEmbedSidebar().findByLabelText("info icon").trigger("mouseover");
|
||||
if (IS_ENTERPRISE) {
|
||||
H.activateToken("bleeding-edge");
|
||||
}
|
||||
|
||||
H.hovercard()
|
||||
.contains(/When using the Embedded analytics SDK/)
|
||||
.should("be.visible");
|
||||
|
||||
H.hovercard()
|
||||
.contains(/Sharing Metabase accounts is a security risk/)
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByRole("button", { name: "Enable to continue" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("preview panel should show placeholder");
|
||||
cy.get('[alt="No results"]').should("be.visible");
|
||||
|
||||
cy.findByRole("button", { name: "Enable to continue" }).click();
|
||||
|
||||
cy.log("button should change to Enabled state");
|
||||
cy.findByRole("button", { name: /Enabled/ })
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
|
||||
cy.log("Preview should load after embedding is enabled");
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText("Orders in a dashboard").should("be.visible");
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the enable card when embedding is already enabled", () => {
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
it("shows the Enable to Continue button and enables embedding on click", () => {
|
||||
H.updateSetting(embeddingSettingName, false);
|
||||
H.updateSetting(showTermsSettingName, true);
|
||||
|
||||
cy.visit("/admin/embedding");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.findAllByTestId("sdk-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText(cardText).should("exist");
|
||||
});
|
||||
|
||||
getEmbedSidebar()
|
||||
.contains("Enable Embedded Analytics JS to get started.")
|
||||
.should("not.exist");
|
||||
});
|
||||
});
|
||||
cy.log("shows tooltip with fair usage info");
|
||||
getEmbedSidebar().findByLabelText("info icon").trigger("mouseover");
|
||||
|
||||
H.hovercard()
|
||||
.contains(
|
||||
/You should, however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature/,
|
||||
)
|
||||
.should("be.visible");
|
||||
|
||||
getEmbedSidebar().findByLabelText("info icon").trigger("mouseout");
|
||||
|
||||
cy.findByRole("button", { name: "Agree and enable" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("preview panel should show placeholder");
|
||||
cy.get('[alt="No results"]').should("be.visible");
|
||||
|
||||
cy.findByRole("button", { name: "Agree and enable" }).click();
|
||||
|
||||
cy.log("button should change to Enabled state");
|
||||
cy.findByRole("button", { name: /Enabled/ })
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
|
||||
cy.log("Preview should load after embedding is enabled");
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText("Orders in a dashboard").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the enable card with fair usage terms when embedding is already enabled", () => {
|
||||
H.updateSetting(embeddingSettingName, true);
|
||||
H.updateSetting(showTermsSettingName, true);
|
||||
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText("Agree to the usage conditions to continue.").should(
|
||||
"exist",
|
||||
);
|
||||
|
||||
cy.findByText(cardText).should("not.exist");
|
||||
});
|
||||
|
||||
cy.log("shows tooltip with fair usage info");
|
||||
getEmbedSidebar().findByLabelText("info icon").trigger("mouseover");
|
||||
|
||||
H.hovercard()
|
||||
.contains(
|
||||
/You should, however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature/,
|
||||
)
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("hides the enable card when embedding is already enabled", () => {
|
||||
H.updateSetting(embeddingSettingName, true);
|
||||
H.updateSetting(showTermsSettingName, false);
|
||||
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
getEmbedSidebar().contains(cardText).should("not.exist");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ describe(suiteTitle, () => {
|
||||
H.activateToken("bleeding-edge");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ import {
|
||||
ORDERS_DASHBOARD_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
import { enableSamlAuth } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
|
||||
import { codeBlock, getEmbedSidebar, navigateToGetCodeStep } from "./helpers";
|
||||
import {
|
||||
codeBlock,
|
||||
getEmbedSidebar,
|
||||
navigateToEmbedOptionsStep,
|
||||
navigateToGetCodeStep,
|
||||
} from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
@@ -35,63 +39,6 @@ describe(suiteTitle, () => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("should select user session auth method by default", () => {
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Authentication").should("be.visible");
|
||||
cy.findByText("Choose the authentication method for embedding:").should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.findByLabelText("Existing Metabase session")
|
||||
.should("be.visible")
|
||||
.should("be.checked");
|
||||
|
||||
cy.findByLabelText("Single sign-on (SSO)")
|
||||
.should("be.visible")
|
||||
.should("not.be.checked");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable SSO radio button when JWT and SAML are not configured", () => {
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should enable SSO radio button when JWT is configured", () => {
|
||||
enableJwtAuth();
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("not.be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should enable SSO radio button when SAML is configured", () => {
|
||||
enableSamlAuth();
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("not.be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should display code snippet with syntax highlighting", () => {
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
@@ -107,81 +54,100 @@ describe(suiteTitle, () => {
|
||||
});
|
||||
|
||||
it("should include useExistingUserSession when user session is selected", () => {
|
||||
navigateToGetCodeStep({
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Existing Metabase session").should("be.checked");
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().should("contain", '"useExistingUserSession": true');
|
||||
|
||||
cy.findByText(/Copy code/).click();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_code_copied",
|
||||
event_detail: "user_session",
|
||||
event_detail:
|
||||
"experience=dashboard,snippetType=frontend,auth=user-session",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should track embed_wizard_code_copied when copy event triggers", () => {
|
||||
navigateToGetCodeStep({
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Existing Metabase session").should("be.checked");
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().should("contain", '"useExistingUserSession": true');
|
||||
codeBlock().trigger("copy");
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_code_copied",
|
||||
event_detail: "user_session",
|
||||
event_detail:
|
||||
"experience=dashboard,snippetType=frontend,auth=user-session",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include useExistingUserSession when SSO is selected", () => {
|
||||
enableJwtAuth();
|
||||
navigateToGetCodeStep({
|
||||
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().should("not.contain", "useExistingUserSession");
|
||||
|
||||
cy.findByText(/Copy code/).click();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_code_copied",
|
||||
event_detail: "sso",
|
||||
event_detail: "experience=dashboard,snippetType=frontend,auth=sso",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should set dashboard-id for dashboard experience", () => {
|
||||
navigateToGetCodeStep({
|
||||
it("should set dashboard-id for regular dashboard experience", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().should("contain", `dashboard-id="${ORDERS_DASHBOARD_ID}"`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set question-id for chart experience", () => {
|
||||
navigateToGetCodeStep({
|
||||
it("should set question-id for regular chart experience", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "chart",
|
||||
resourceName: QUESTION_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().should(
|
||||
"contain",
|
||||
`question-id="${ORDERS_COUNT_QUESTION_ID}"`,
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
JWT_SHARED_SECRET,
|
||||
entityPickerModal,
|
||||
getParametersContainer,
|
||||
} from "e2e/support/helpers";
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
import {
|
||||
codeBlock,
|
||||
getEmbedSidebar,
|
||||
navigateToEmbedOptionsStep,
|
||||
visitNewEmbedPage,
|
||||
} from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const DASHBOARD_NAME = "Dashboard with Parameters";
|
||||
const DASHBOARD_PARAMETERS = [
|
||||
{
|
||||
name: "ID",
|
||||
slug: "id",
|
||||
id: "11111111",
|
||||
type: "id",
|
||||
},
|
||||
{
|
||||
name: "Product ID",
|
||||
slug: "product_id",
|
||||
id: "22222222",
|
||||
type: "id",
|
||||
},
|
||||
];
|
||||
|
||||
const FIRST_QUESTION_NAME = "Question With Params 1";
|
||||
const SECOND_QUESTION_NAME = "Question With Params 2";
|
||||
|
||||
describe("scenarios > embedding > sdk iframe embed setup > guest-embed", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
H.enableTracking();
|
||||
|
||||
H.updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Orders table",
|
||||
query: { "source-table": ORDERS_ID },
|
||||
},
|
||||
dashboardDetails: {
|
||||
name: DASHBOARD_NAME,
|
||||
parameters: DASHBOARD_PARAMETERS,
|
||||
},
|
||||
}).then(({ body: card }) => {
|
||||
cy.wrap(card.dashboard_id).as("dashboardId");
|
||||
});
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: FIRST_QUESTION_NAME,
|
||||
native: {
|
||||
query: "select {{text}}",
|
||||
"template-tags": {
|
||||
text: {
|
||||
id: "abc-123",
|
||||
name: "text",
|
||||
"display-name": "Text",
|
||||
type: "text",
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
enable_embedding: false,
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
idAlias: "question1Id",
|
||||
},
|
||||
);
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: SECOND_QUESTION_NAME,
|
||||
native: {
|
||||
query: "select {{text}}",
|
||||
"template-tags": {
|
||||
text1: {
|
||||
id: "abc-123",
|
||||
name: "text1",
|
||||
"display-name": "Text1",
|
||||
type: "text",
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
text2: {
|
||||
id: "abc-456",
|
||||
name: "text2",
|
||||
"display-name": "Text2",
|
||||
type: "text",
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
enable_embedding: true,
|
||||
embedding_params: {
|
||||
text1: "enabled",
|
||||
state2: "disabled",
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
idAlias: "question2Id",
|
||||
},
|
||||
);
|
||||
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
describe("Happy path", () => {
|
||||
it("Navigates through the guest-embed flow for a question and opens its embed page", () => {
|
||||
visitNewEmbedPage();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
|
||||
// Experience step
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByTestId("upsell-gem").should("not.exist");
|
||||
|
||||
cy.findByText("Chart").click();
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_experience_completed",
|
||||
event_detail: "custom=chart",
|
||||
});
|
||||
|
||||
// Entity selection step
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByTestId("embed-browse-entity-button").click();
|
||||
});
|
||||
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Select a chart").should("be.visible");
|
||||
cy.findByText("Questions").click();
|
||||
cy.findByText(FIRST_QUESTION_NAME).click();
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "isDefaultResource=false,experience=chart",
|
||||
});
|
||||
|
||||
// Options step
|
||||
cy.findByLabelText("Guest").should("be.visible").should("be.checked");
|
||||
|
||||
cy.findByLabelText("Allow people to drill through on data points")
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
cy.findByLabelText("Allow downloads")
|
||||
.should("be.visible")
|
||||
.should("not.be.disabled")
|
||||
.should("not.be.checked");
|
||||
cy.findByLabelText("Allow people to save new questions")
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
|
||||
H.setEmbeddingParameter("Text", "Locked");
|
||||
cy.findAllByTestId("parameter-widget").find("input").type("Foo Bar Baz");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByTestId("upsell-gem").should("not.exist");
|
||||
});
|
||||
|
||||
H.publishChanges("card");
|
||||
cy.button("Unpublish").should("be.visible");
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText("Foo Bar Baz").should("be.visible");
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Get code").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
'settings=custom,experience=chart,guestEmbedEnabled=true,guestEmbedType=guest-embed,auth=guest-embed,drills=false,withDownloads=false,withTitle=true,isSaveEnabled=false,params={"disabled":0,"locked":1,"enabled":0},theme=default',
|
||||
});
|
||||
|
||||
// Get code step
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findAllByText(/Copy code/)
|
||||
.first()
|
||||
.click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_code_copied",
|
||||
event_detail:
|
||||
"experience=chart,snippetType=frontend,guestEmbedEnabled=true,guestEmbedType=guest-embed,auth=guest-embed",
|
||||
});
|
||||
|
||||
// Visit embed page
|
||||
getEmbedSidebar().within(() => {
|
||||
codeBlock()
|
||||
.invoke("text")
|
||||
.then((code: string) => {
|
||||
const match = code.match(/token="([^"]+)"/);
|
||||
expect(match, "JWT token present in code block").to.not.be.null;
|
||||
const jwtToken = match ? match[1] : "";
|
||||
cy.wrap(jwtToken).as("guestEmbedToken");
|
||||
});
|
||||
});
|
||||
|
||||
cy.get<string>("@guestEmbedToken").then((token) => {
|
||||
const frame = H.loadSdkIframeEmbedTestPage({
|
||||
metabaseConfig: { isGuest: true },
|
||||
elements: [
|
||||
{
|
||||
component: "metabase-question",
|
||||
attributes: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
frame.within(() => {
|
||||
cy.findByText("Foo Bar Baz").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Properly re-initializes embedding parameters and Guest Embed navbar", () => {
|
||||
cy.get("@question1Id").then((questionId) => {
|
||||
cy.request("PUT", `/api/card/${questionId}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "chart",
|
||||
resourceName: FIRST_QUESTION_NAME,
|
||||
});
|
||||
|
||||
cy.button("Unpublish").should("be.visible");
|
||||
|
||||
getParametersContainer()
|
||||
.findByLabelText("Text")
|
||||
.should("contain.text", "Disabled");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Back").click();
|
||||
|
||||
cy.findByTestId("embed-browse-entity-button").click();
|
||||
});
|
||||
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText("Questions").click();
|
||||
cy.findAllByText(SECOND_QUESTION_NAME).first().click();
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
cy.button("Unpublish").should("be.visible");
|
||||
|
||||
getParametersContainer()
|
||||
.findByLabelText("Text1")
|
||||
.should("contain.text", "Editable");
|
||||
|
||||
getParametersContainer()
|
||||
.findByLabelText("Text2")
|
||||
.should("contain.text", "Disabled");
|
||||
|
||||
H.setEmbeddingParameter("Text1", "Locked");
|
||||
|
||||
cy.button("Publish changes").should("be.visible");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Back").click();
|
||||
|
||||
cy.findByTestId("embed-browse-entity-button").click();
|
||||
});
|
||||
|
||||
entityPickerModal().within(() => {
|
||||
cy.findByText("Questions").click();
|
||||
cy.findAllByText(FIRST_QUESTION_NAME).first().click();
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
cy.button("Unpublish").should("be.visible");
|
||||
|
||||
getParametersContainer()
|
||||
.findByLabelText("Text")
|
||||
.should("contain.text", "Disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("Properly adjusts EmbedJS options when switching between guest/sso modes for a Dashboard", () => {
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
cy.request("PUT", `/api/dashboard/${dashboardId}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
|
||||
enableJwtAuth();
|
||||
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Guest").should("be.visible").should("be.checked");
|
||||
});
|
||||
|
||||
H.setEmbeddingParameter("Product ID", "Locked");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().first().should("contain", "token=");
|
||||
|
||||
codeBlock().first().should("not.contain", "dashboard-id=");
|
||||
codeBlock().first().should("not.contain", "hidden-parameters=");
|
||||
codeBlock().first().should("not.contain", "locked-parameters=");
|
||||
|
||||
cy.findByText("Back").click();
|
||||
|
||||
cy.findByLabelText("Single sign-on (SSO)").click();
|
||||
|
||||
cy.findByText("Get code").click();
|
||||
|
||||
codeBlock().first().should("contain", "dashboard-id=");
|
||||
codeBlock().first().should("contain", "hidden-parameters=");
|
||||
|
||||
codeBlock().first().should("not.contain", "token=");
|
||||
codeBlock().first().should("not.contain", "locked-parameters=");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { JWT_SHARED_SECRET } from "e2e/support/helpers";
|
||||
|
||||
import { codeBlock, getEmbedSidebar, visitNewEmbedPage } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const DASHBOARD_NAME = "Dashboard with Parameters";
|
||||
const DASHBOARD_PARAMETERS = [
|
||||
{
|
||||
name: "ID",
|
||||
slug: "id",
|
||||
id: "11111111",
|
||||
type: "id",
|
||||
},
|
||||
{
|
||||
name: "Product ID",
|
||||
slug: "product_id",
|
||||
id: "22222222",
|
||||
type: "id",
|
||||
},
|
||||
];
|
||||
|
||||
const FIRST_QUESTION_NAME = "Question With Params 1";
|
||||
const SECOND_QUESTION_NAME = "Question With Params 2";
|
||||
|
||||
describe(
|
||||
"scenarios > embedding > sdk iframe embed setup > guest-embed",
|
||||
{ tags: "@OSS" },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.enableTracking();
|
||||
|
||||
H.updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
|
||||
H.createQuestionAndDashboard({
|
||||
questionDetails: {
|
||||
name: "Orders table",
|
||||
query: { "source-table": ORDERS_ID },
|
||||
},
|
||||
dashboardDetails: {
|
||||
name: DASHBOARD_NAME,
|
||||
parameters: DASHBOARD_PARAMETERS,
|
||||
},
|
||||
}).then(({ body: card }) => {
|
||||
cy.wrap(card.dashboard_id).as("dashboardId");
|
||||
});
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: FIRST_QUESTION_NAME,
|
||||
native: {
|
||||
query: "select {{text}}",
|
||||
"template-tags": {
|
||||
text: {
|
||||
id: "abc-123",
|
||||
name: "text",
|
||||
"display-name": "Text",
|
||||
type: "text",
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
enable_embedding: false,
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
idAlias: "question1Id",
|
||||
},
|
||||
);
|
||||
|
||||
H.createNativeQuestion(
|
||||
{
|
||||
name: SECOND_QUESTION_NAME,
|
||||
native: {
|
||||
query: "select {{text}}",
|
||||
"template-tags": {
|
||||
text1: {
|
||||
id: "abc-123",
|
||||
name: "text1",
|
||||
"display-name": "Text1",
|
||||
type: "text",
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
text2: {
|
||||
id: "abc-456",
|
||||
name: "text2",
|
||||
"display-name": "Text2",
|
||||
type: "text",
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
enable_embedding: true,
|
||||
embedding_params: {
|
||||
text1: "enabled",
|
||||
state2: "disabled",
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapId: true,
|
||||
idAlias: "question2Id",
|
||||
},
|
||||
);
|
||||
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
describe("Happy path", () => {
|
||||
it("Navigates through the guest-embed flow for a question and opens its embed page", () => {
|
||||
visitNewEmbedPage();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
|
||||
// Experience step
|
||||
getEmbedSidebar().within(() => {
|
||||
["Exploration", "Browser"].forEach((label) => {
|
||||
cy.findByLabelText(label).should("be.disabled");
|
||||
cy.get("label")
|
||||
.contains(label)
|
||||
.within(() => {
|
||||
cy.findByTestId("upsell-gem").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
cy.findByText("Chart").click();
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_experience_completed",
|
||||
event_detail: "custom=chart",
|
||||
});
|
||||
|
||||
// Entity selection step
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByTestId("embed-browse-entity-button").click();
|
||||
});
|
||||
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Select a chart").should("be.visible");
|
||||
cy.findByText("Questions").click();
|
||||
cy.findByText(FIRST_QUESTION_NAME).click();
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Next").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "isDefaultResource=false,experience=chart",
|
||||
});
|
||||
|
||||
// Options step
|
||||
cy.findByLabelText("Guest").should("be.visible").should("be.checked");
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("be.disabled");
|
||||
|
||||
cy.findByLabelText("Allow people to drill through on data points")
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
cy.findByLabelText("Allow downloads")
|
||||
.should("be.visible")
|
||||
.should("be.disabled")
|
||||
.should("be.checked");
|
||||
cy.findByLabelText("Allow people to save new questions")
|
||||
.should("be.visible")
|
||||
.should("be.disabled");
|
||||
|
||||
[
|
||||
"Single sign-on (SSO)",
|
||||
"Allow people to drill through on data points",
|
||||
"Allow downloads",
|
||||
"Allow people to save new questions",
|
||||
].forEach((text) => {
|
||||
cy.findAllByTestId("tooltip-warning")
|
||||
.filter(`:contains("${text}")`)
|
||||
.within(() => {
|
||||
cy.findByTestId("upsell-gem").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
H.setEmbeddingParameter("Text", "Locked");
|
||||
cy.findAllByTestId("parameter-widget")
|
||||
.find("input")
|
||||
.type("Foo Bar Baz");
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByTestId("appearance-section").within(() => {
|
||||
cy.findByTestId("upsell-gem").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
H.publishChanges("card");
|
||||
cy.button("Unpublish").should("be.visible");
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText("Foo Bar Baz").should("be.visible");
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Get code").click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
'settings=custom,experience=chart,guestEmbedEnabled=true,guestEmbedType=guest-embed,auth=guest-embed,drills=false,withDownloads=true,withTitle=true,isSaveEnabled=false,params={"disabled":0,"locked":1,"enabled":0},theme=default',
|
||||
});
|
||||
|
||||
// Get code step
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findAllByText(/Copy code/)
|
||||
.first()
|
||||
.click();
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_code_copied",
|
||||
event_detail:
|
||||
"experience=chart,snippetType=frontend,guestEmbedEnabled=true,guestEmbedType=guest-embed,auth=guest-embed",
|
||||
});
|
||||
|
||||
// Visit embed page
|
||||
getEmbedSidebar().within(() => {
|
||||
codeBlock()
|
||||
.invoke("text")
|
||||
.then((code: string) => {
|
||||
const match = code.match(/token="([^"]+)"/);
|
||||
expect(match, "JWT token present in code block").to.not.be.null;
|
||||
const jwtToken = match ? match[1] : "";
|
||||
cy.wrap(jwtToken).as("guestEmbedToken");
|
||||
});
|
||||
});
|
||||
|
||||
cy.get<string>("@guestEmbedToken").then((token) => {
|
||||
const frame = H.loadSdkIframeEmbedTestPage({
|
||||
metabaseConfig: { isGuest: true },
|
||||
elements: [
|
||||
{
|
||||
component: "metabase-question",
|
||||
attributes: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
frame.within(() => {
|
||||
cy.findByText("Foo Bar Baz").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,10 @@
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { entityPickerModal, modal } from "e2e/support/helpers";
|
||||
import {
|
||||
embedModalEnableEmbedding,
|
||||
entityPickerModal,
|
||||
modal,
|
||||
} from "e2e/support/helpers";
|
||||
import type { Dashboard, RecentItem } from "metabase-types/api";
|
||||
|
||||
type RecentActivityIntercept = {
|
||||
@@ -24,15 +28,24 @@ export const visitNewEmbedPage = () => {
|
||||
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.findAllByTestId("sdk-setting-card")
|
||||
cy.findAllByTestId(/(sdk-setting-card|guest-embeds-setting-card)/)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.wait("@dashboard");
|
||||
cy.get("body").then(($body) => {
|
||||
const isEmbeddingDisabled =
|
||||
$body.find('[data-testid="enable-embedding-card"]').length > 0;
|
||||
|
||||
cy.get("[data-iframe-loaded]", { timeout: 20000 }).should("have.length", 1);
|
||||
if (isEmbeddingDisabled) {
|
||||
embedModalEnableEmbedding();
|
||||
}
|
||||
|
||||
cy.wait("@dashboard");
|
||||
|
||||
cy.get("[data-iframe-loaded]", { timeout: 20000 }).should("have.length", 1);
|
||||
});
|
||||
};
|
||||
|
||||
export const assertRecentItemName = (
|
||||
|
||||
@@ -41,7 +41,7 @@ describe(suiteTitle, () => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("tracks 'default' when keeping the default dashboard selection", () => {
|
||||
it("tracks event details with `isDefaultResource=true` when keeping the default dashboard selection", () => {
|
||||
visitNewEmbedPage();
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
@@ -55,11 +55,11 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "default",
|
||||
event_detail: "isDefaultResource=true,experience=dashboard",
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks 'custom' when selecting a different dashboard", () => {
|
||||
it("tracks event details with `isDefaultResource=false` when selecting a different dashboard", () => {
|
||||
cy.log("add two dashboards to activity log");
|
||||
|
||||
H.createDashboard({ name: SECOND_DASHBOARD_NAME }).then(
|
||||
@@ -101,7 +101,7 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "custom",
|
||||
event_detail: "isDefaultResource=false,experience=dashboard",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,12 +136,11 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "custom",
|
||||
event_detail: "isDefaultResource=false,experience=chart",
|
||||
});
|
||||
});
|
||||
|
||||
cy.log("selected question should be shown in the preview");
|
||||
cy.wait("@cardQuery");
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText(SECOND_QUESTION_NAME).should("be.visible");
|
||||
});
|
||||
@@ -209,10 +208,9 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "custom",
|
||||
event_detail: "isDefaultResource=false,experience=chart",
|
||||
});
|
||||
|
||||
cy.wait("@cardQuery");
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText(FIRST_QUESTION_NAME).should("be.visible");
|
||||
});
|
||||
|
||||
@@ -84,8 +84,6 @@ describe(suiteTitle, () => {
|
||||
event_detail: "custom=chart",
|
||||
});
|
||||
|
||||
cy.wait("@cardQuery");
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.log("question title is visible");
|
||||
cy.findByText(questionName).should("be.visible");
|
||||
@@ -193,6 +191,7 @@ describe(suiteTitle, () => {
|
||||
cy.visit(`/question/${ORDERS_QUESTION_ID}`);
|
||||
|
||||
H.openEmbedJsModal();
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
cy.get("#iframe-embed-container")
|
||||
.findByTestId("preview-loading-indicator", { timeout: 20_000 })
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { mockEmbedJsToDevServer } from "e2e/support/helpers";
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
import { enableSamlAuth } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
|
||||
import {
|
||||
codeBlock,
|
||||
@@ -33,12 +35,69 @@ describe(suiteTitle, () => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("toggles drill-throughs for dashboards", () => {
|
||||
it("should select user session auth method by default", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByText("Authentication").should("be.visible");
|
||||
|
||||
cy.findByLabelText("Guest").should("be.visible").should("be.checked");
|
||||
cy.findByLabelText("Existing Metabase session")
|
||||
.should("be.visible")
|
||||
.should("not.be.checked");
|
||||
|
||||
cy.findByLabelText("Single sign-on (SSO)")
|
||||
.should("be.visible")
|
||||
.should("not.be.checked");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable SSO radio button when JWT and SAML are not configured", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should enable SSO radio button when JWT is configured", () => {
|
||||
enableJwtAuth();
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("not.be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("should enable SSO radio button when SAML is configured", () => {
|
||||
enableSamlAuth();
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("not.be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles drill-throughs for dashboards when non-authorized auth method is selected", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
getEmbedSidebar()
|
||||
.findByLabelText("Allow people to drill through on data points")
|
||||
.should("be.checked");
|
||||
@@ -67,7 +126,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,drills=false,withDownloads=false,withTitle=true",
|
||||
"settings=custom,experience=dashboard,auth=user-session,drills=false,withDownloads=false,withTitle=true,isSaveEnabled=false,theme=default",
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'drills="false"');
|
||||
@@ -103,7 +162,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,drills=true,withDownloads=true,withTitle=true",
|
||||
'settings=custom,experience=dashboard,guestEmbedEnabled=false,auth=guest-embed,drills=false,withDownloads=true,withTitle=true,params={"disabled":0,"locked":0,"enabled":0},theme=default',
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'with-downloads="true"');
|
||||
@@ -139,17 +198,18 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,drills=true,withDownloads=false,withTitle=false",
|
||||
'settings=custom,experience=dashboard,guestEmbedEnabled=false,auth=guest-embed,drills=false,withDownloads=false,withTitle=false,params={"disabled":0,"locked":0,"enabled":0},theme=default',
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'with-title="false"');
|
||||
});
|
||||
|
||||
it("toggles drill-through for charts", () => {
|
||||
it("toggles drill-through for charts for non-authorized auth mode", () => {
|
||||
navigateToEmbedOptionsStep({
|
||||
experience: "chart",
|
||||
resourceName: QUESTION_NAME,
|
||||
});
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
getEmbedSidebar()
|
||||
.findByLabelText("Allow people to drill through on data points")
|
||||
@@ -211,7 +271,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,drills=true,withDownloads=true,withTitle=true,isSaveEnabled=false",
|
||||
'settings=custom,experience=chart,guestEmbedEnabled=false,auth=guest-embed,drills=false,withDownloads=true,withTitle=true,isSaveEnabled=false,params={"disabled":0,"locked":0,"enabled":0},theme=default',
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'with-downloads="true"');
|
||||
@@ -222,6 +282,7 @@ describe(suiteTitle, () => {
|
||||
experience: "chart",
|
||||
resourceName: QUESTION_NAME,
|
||||
});
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.log("chart title should be visible by default");
|
||||
getEmbedSidebar().findByLabelText("Show chart title").should("be.checked");
|
||||
@@ -285,6 +346,8 @@ describe(suiteTitle, () => {
|
||||
: { experience: "exploration" },
|
||||
);
|
||||
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
if (experience === "exploration") {
|
||||
cy.log("visualize a question to enable the save button");
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
@@ -325,8 +388,8 @@ describe(suiteTitle, () => {
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
experience === "chart"
|
||||
? "settings=custom,theme=default,auth=user_session,drills=true,withDownloads=false,withTitle=true,isSaveEnabled=true"
|
||||
: "settings=custom,theme=default,auth=user_session,isSaveEnabled=true",
|
||||
? "settings=custom,experience=chart,auth=user-session,drills=true,withDownloads=false,withTitle=true,isSaveEnabled=true,theme=default"
|
||||
: "settings=custom,experience=exploration,auth=user-session,isSaveEnabled=true,theme=default",
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'is-save-enabled="true"');
|
||||
@@ -363,7 +426,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,readOnly=false",
|
||||
"settings=custom,experience=browser,auth=user-session,readOnly=false,theme=default",
|
||||
});
|
||||
|
||||
codeBlock().should("contain", 'read-only="false"');
|
||||
@@ -409,7 +472,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=custom,auth=user_session,drills=true,withDownloads=false,withTitle=true",
|
||||
'settings=custom,experience=dashboard,guestEmbedEnabled=false,auth=guest-embed,drills=false,withDownloads=false,withTitle=true,params={"disabled":0,"locked":0,"enabled":0},theme=custom',
|
||||
});
|
||||
|
||||
codeBlock().should("contain", '"theme": {');
|
||||
@@ -446,6 +509,7 @@ describe(suiteTitle, () => {
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
cy.findByLabelText("Existing Metabase session").click();
|
||||
|
||||
cy.log("click on brand color picker");
|
||||
cy.findByTestId("brand-color-picker").findByRole("button").click();
|
||||
@@ -477,7 +541,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=custom,auth=user_session,drills=true,withDownloads=false,withTitle=true",
|
||||
"settings=custom,experience=dashboard,auth=user-session,drills=true,withDownloads=false,withTitle=true,isSaveEnabled=false,theme=custom",
|
||||
});
|
||||
|
||||
// derived-colors-for-embed-flow.unit.spec.ts contains the tests for other derived colors.
|
||||
@@ -503,7 +567,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,layout=stacked",
|
||||
"settings=custom,experience=metabot,auth=user-session,layout=stacked,theme=default",
|
||||
});
|
||||
|
||||
getEmbedSidebar().findByText("Back").click();
|
||||
@@ -519,7 +583,7 @@ describe(suiteTitle, () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_options_completed",
|
||||
event_detail:
|
||||
"settings=custom,theme=default,auth=user_session,layout=sidebar",
|
||||
"settings=custom,experience=metabot,auth=user-session,layout=sidebar,theme=default",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
import {
|
||||
codeBlock,
|
||||
getEmbedSidebar,
|
||||
navigateToEmbedOptionsStep,
|
||||
navigateToGetCodeStep,
|
||||
} from "./helpers";
|
||||
import { navigateToEmbedOptionsStep } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
@@ -45,6 +38,9 @@ describe("scenarios > embedding > sdk iframe embed setup > user settings persist
|
||||
.first()
|
||||
.should("have.css", "color", "rgb(255, 0, 0)");
|
||||
|
||||
// Wait for debounce
|
||||
cy.wait(800);
|
||||
|
||||
cy.log("2. reload the page");
|
||||
cy.wait("@persistSettings");
|
||||
|
||||
@@ -59,38 +55,4 @@ describe("scenarios > embedding > sdk iframe embed setup > user settings persist
|
||||
.first()
|
||||
.should("have.css", "color", "rgb(255, 0, 0)");
|
||||
});
|
||||
|
||||
it("persists sso auth method", () => {
|
||||
enableJwtAuth();
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
cy.log("1. select sso auth method");
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.log("single sign on should not be checked by default");
|
||||
cy.findByLabelText("Single sign-on (SSO)")
|
||||
.should("not.be.checked")
|
||||
.click()
|
||||
.should("be.checked");
|
||||
|
||||
codeBlock().should("not.contain", "useExistingUserSession");
|
||||
});
|
||||
|
||||
cy.log("2. reload the page");
|
||||
cy.wait("@persistSettings");
|
||||
|
||||
cy.log("3. auth method should persist");
|
||||
navigateToGetCodeStep({
|
||||
experience: "dashboard",
|
||||
resourceName: DASHBOARD_NAME,
|
||||
});
|
||||
|
||||
getEmbedSidebar().within(() => {
|
||||
cy.findByLabelText("Existing Metabase session").should("not.be.checked");
|
||||
cy.findByLabelText("Single sign-on (SSO)").should("be.checked");
|
||||
codeBlock().should("not.contain", "useExistingUserSession");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { createQuestion, getSignedJwtForResource } from "e2e/support/helpers";
|
||||
|
||||
const { H } = cy;
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
const { IS_ENTERPRISE } = Cypress.env();
|
||||
const IS_OSS = !IS_ENTERPRISE;
|
||||
const MB_EDITION = IS_ENTERPRISE ? "ee" : "oss";
|
||||
|
||||
describe(
|
||||
`scenarios > embedding > sdk iframe embedding > guest-embed > ${MB_EDITION}`,
|
||||
{ ...(IS_OSS && { tags: "@OSS" }) },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
H.prepareGuestEmbedSdkIframeEmbedTest({
|
||||
onPrepare: () => {
|
||||
createQuestion({
|
||||
name: "47563",
|
||||
enable_embedding: true,
|
||||
embedding_type: "guest-embed",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["max", ["field", ORDERS.QUANTITY, null]]],
|
||||
breakout: [["field", ORDERS.PRODUCT_ID, null]],
|
||||
limit: 2,
|
||||
},
|
||||
}).then(({ body: question }) => {
|
||||
cy.wrap(question.id).as("questionId");
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a static question", () => {
|
||||
cy.get("@questionId").then(async (questionId) => {
|
||||
const token = await getSignedJwtForResource({
|
||||
resourceId: questionId as unknown as number,
|
||||
resourceType: "question",
|
||||
});
|
||||
|
||||
const frame = H.loadSdkIframeEmbedTestPage({
|
||||
metabaseConfig: { isGuest: true },
|
||||
elements: [
|
||||
{
|
||||
component: "metabase-question",
|
||||
attributes: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cy.wait("@getCardQuery");
|
||||
|
||||
frame.within(() => {
|
||||
cy.findByText("Product ID").should("be.visible");
|
||||
cy.findByText("Max of Quantity").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("shows an error for a component without guest embed support", () => {
|
||||
const frame = H.loadSdkIframeEmbedTestPage({
|
||||
metabaseConfig: { isGuest: true },
|
||||
elements: [
|
||||
{
|
||||
component: "metabase-browser",
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
frame.within(() => {
|
||||
cy.findByText("This component does not support guest embeds").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -414,6 +414,7 @@ describe("scenarios > embedding > embedded analytics JS", () => {
|
||||
cy.visit(`/question/${ORDERS_QUESTION_ID}`);
|
||||
|
||||
H.openEmbedJsModal();
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.waitForSimpleEmbedIframesToLoad();
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
|
||||
@@ -209,9 +209,10 @@ describe("issue 35954", () => {
|
||||
H.updateSetting("show-static-embed-terms", false);
|
||||
|
||||
H.visitDashboard(id);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.modal().findByText("Static embedding").click();
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: id,
|
||||
});
|
||||
|
||||
cy.findByTestId("embedding-preview").within(() => {
|
||||
cy.intercept("GET", "api/preview_embed/dashboard/**").as(
|
||||
|
||||
@@ -28,10 +28,8 @@ const { H } = cy;
|
||||
.should("be.visible")
|
||||
.and("be.enabled")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
cy.button("Get embedding code").click();
|
||||
});
|
||||
H.popover().findByTestId("copy-button").should("be.visible");
|
||||
|
||||
H.embedModalContent().should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +61,7 @@ const { H } = cy;
|
||||
});
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
H.modal().findByText("Embed Metabase").should("be.visible");
|
||||
H.embedModalContent().should("be.visible");
|
||||
});
|
||||
|
||||
it(`should let the user create a public link for ${resource}`, () => {
|
||||
@@ -138,14 +136,6 @@ const { H } = cy;
|
||||
});
|
||||
|
||||
cy.findByTestId("embed-menu-embed-modal-item").click();
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByText("Static embedding").should("be.visible");
|
||||
cy.findByText(/Use public embedding/).should("not.exist");
|
||||
cy.findByText("Public embeds and links are disabled.").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,7 +158,7 @@ const { H } = cy;
|
||||
});
|
||||
});
|
||||
|
||||
describe("embed modal display", () => {
|
||||
describe("Embed JS modal display", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
@@ -179,24 +169,14 @@ describe("embed modal display", () => {
|
||||
});
|
||||
|
||||
describe("when the user has a paid instance", () => {
|
||||
it("should display a disabled state and a link to the Embedded Analytics JS settings", () => {
|
||||
it("should open Embed JS modal with the `enable simple embedding` card", () => {
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByText("Static embedding").should("be.visible");
|
||||
cy.findByText("Embedded Analytics JS").should("be.visible");
|
||||
|
||||
cy.findByRole("article", { name: "Embedded Analytics JS" }).within(
|
||||
() => {
|
||||
cy.findByText("Disabled.").should("be.visible");
|
||||
cy.findByText("Enable in admin settings")
|
||||
.should("be.visible")
|
||||
.and("have.attr", "href", "/admin/embedding/modular");
|
||||
},
|
||||
);
|
||||
H.embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText(/Embedded Analytics JS/).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -210,23 +190,15 @@ describe("embed modal display", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByText("Static embedding").should("be.visible");
|
||||
cy.findByText("Embedded Analytics JS").should("be.visible");
|
||||
it("should open Embed JS modal with the `enable simple embedding` card", () => {
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
cy.findByRole("link", { name: "Embedded Analytics JS" }).should(
|
||||
"have.attr",
|
||||
"href",
|
||||
"https://www.metabase.com/product/embedded-analytics?utm_source=product&utm_medium=upsell&utm_campaign=embedded-analytics-js&utm_content=static-embed-popover&source_plan=oss",
|
||||
);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
cy.findByRole("article", { name: "Embedded Analytics JS" }).within(
|
||||
() => {
|
||||
cy.findByText("Learn more").should("be.visible");
|
||||
cy.findByText("Disabled.").should("not.exist");
|
||||
cy.findByText("Enable in admin settings").should("not.exist");
|
||||
},
|
||||
);
|
||||
H.embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText(/guest embeds/).should("be.visible");
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -272,7 +244,7 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
apiPath: "card",
|
||||
},
|
||||
].forEach(({ resource, apiPath }) => {
|
||||
describe(`static modal behavior for ${resource}`, () => {
|
||||
describe(`legacy static modal behavior for ${resource}`, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
@@ -286,9 +258,13 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
it("should set a proper embedding_type", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
visitResource(resource, id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource,
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
});
|
||||
|
||||
H.publishChanges(apiPath, ({ request, response }) => {
|
||||
assert.deepEqual(request.body.embedding_type, "static-legacy");
|
||||
@@ -398,9 +374,7 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
visitResource(resource, id);
|
||||
});
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.modal().findByText("Get embedding code").click();
|
||||
H.openSharingMenu("Create a public link");
|
||||
|
||||
// mock clipboardData so that copy-to-clipboard doesn't use window.prompt, pausing the tests
|
||||
cy.window().then((win) => {
|
||||
@@ -414,9 +388,8 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
H.popover().findByTestId("copy-button").click();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "public_embed_code_copied",
|
||||
event: "public_link_copied",
|
||||
artifact: resource,
|
||||
source: "public-embed",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -425,15 +398,14 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
visitResource(resource, id);
|
||||
});
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
H.modal().findByText("Get embedding code").click();
|
||||
H.openSharingMenu("Create a public link");
|
||||
|
||||
H.popover().findByText("Remove public link").click();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "public_link_removed",
|
||||
artifact: resource,
|
||||
source: "public-embed",
|
||||
source: "public-share",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -442,14 +414,17 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
it("should send `static_embed_code_copied` when copying the static embed code", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({ resource, resourceId: id });
|
||||
});
|
||||
H.openStaticEmbeddingModal();
|
||||
|
||||
cy.log("Assert copying codes in Overview tab");
|
||||
cy.findByTestId("embed-backend")
|
||||
.findByTestId("copy-button")
|
||||
.realClick();
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
|
||||
// TODO: fix this test, it's flaky on CI
|
||||
/*H.expectUnstructuredSnowplowEvent({
|
||||
event: "static_embed_code_copied",
|
||||
artifact: resource,
|
||||
language: "node",
|
||||
@@ -463,7 +438,7 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
theme: "light",
|
||||
downloads: null,
|
||||
},
|
||||
});
|
||||
});*/
|
||||
|
||||
cy.findByTestId("embed-frontend")
|
||||
.findByTestId("copy-button")
|
||||
@@ -583,14 +558,20 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
it("should send `static_embed_code_copied` when copying the static embed code", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource,
|
||||
resourceId: id,
|
||||
});
|
||||
});
|
||||
H.openStaticEmbeddingModal({ acceptTerms: false });
|
||||
|
||||
cy.log("Assert copying codes in Overview tab");
|
||||
cy.findByTestId("embed-backend")
|
||||
.findByTestId("copy-button")
|
||||
.realClick();
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
|
||||
// TODO: fix this test, it's flaky on CI
|
||||
/*H.expectUnstructuredSnowplowEvent({
|
||||
event: "static_embed_code_copied",
|
||||
artifact: resource,
|
||||
language: "node",
|
||||
@@ -604,7 +585,7 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
theme: "light",
|
||||
enabled_download_types: { pdf: true, results: true },
|
||||
},
|
||||
});
|
||||
});*/
|
||||
|
||||
cy.findByTestId("embed-frontend")
|
||||
.findByTestId("copy-button")
|
||||
@@ -716,8 +697,12 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
it("should support disabling PDF and result downloads individually in `static_embed_code_copied`", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: id,
|
||||
});
|
||||
});
|
||||
H.openStaticEmbeddingModal({ acceptTerms: false });
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByRole("tab", { name: "Look and Feel" }).click();
|
||||
@@ -789,12 +774,18 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
|
||||
it("should send `static_embed_discarded` when discarding changes in the static embed modal", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
enableEmbeddingForResource({ resource, id });
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource,
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
});
|
||||
|
||||
H.publishChanges(apiPath);
|
||||
});
|
||||
|
||||
cy.log("changing parameters, so we could discard changes");
|
||||
H.openStaticEmbeddingModal({ activeTab: "parameters" });
|
||||
H.modal().button("Price").click();
|
||||
H.popover().findByText("Editable").click();
|
||||
|
||||
@@ -814,8 +805,9 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
});
|
||||
cy.get("@resourceId").then((id) => {
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({ resource, resourceId: id });
|
||||
});
|
||||
H.openStaticEmbeddingModal();
|
||||
|
||||
cy.findByTestId("embed-modal-content-status-bar")
|
||||
.button("Publish")
|
||||
@@ -825,12 +817,15 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "static_embed_published",
|
||||
artifact: resource,
|
||||
new_embed: true,
|
||||
new_embed: false,
|
||||
time_since_creation: closeTo(
|
||||
toSecond(Date.now() - this.timeAfterResourceCreation),
|
||||
15,
|
||||
),
|
||||
time_since_initial_publication: null,
|
||||
time_since_initial_publication: closeTo(
|
||||
toSecond(Date.now() - this.timeAfterResourceCreation),
|
||||
15,
|
||||
),
|
||||
params: {
|
||||
disabled: 3,
|
||||
locked: 0,
|
||||
@@ -877,10 +872,12 @@ describe("#39152 sharing an unsaved question", () => {
|
||||
|
||||
it("should send `static_embed_unpublished` when unpublishing changes in the static embed modal", () => {
|
||||
cy.get("@resourceId").then((id) => {
|
||||
enableEmbeddingForResource({ resource, id });
|
||||
visitResource(resource, id);
|
||||
|
||||
H.openLegacyStaticEmbeddingModal({ resource, resourceId: id });
|
||||
|
||||
H.publishChanges(apiPath);
|
||||
});
|
||||
H.openStaticEmbeddingModal();
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
cy.clock(new Date(Date.now() + HOUR));
|
||||
@@ -989,13 +986,6 @@ function visitResource(resource, id) {
|
||||
}
|
||||
}
|
||||
|
||||
function enableEmbeddingForResource({ resource, id }) {
|
||||
const endpoint = resource === "question" ? "card" : "dashboard";
|
||||
cy.request("PUT", `/api/${endpoint}/${id}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
}
|
||||
|
||||
function assertValidPublicLink({ resource, shouldHaveRemoveLink }) {
|
||||
const regex = new RegExp(
|
||||
`https?:\\/\\/[^\\/]+\\/public\\/${resource}\\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\\.csv|\\.json|\\.xlsx)?`,
|
||||
|
||||
@@ -26,10 +26,7 @@ describe(suiteTitle, () => {
|
||||
it("pre-selects dashboard in embed flow when opened from dashboard sharing modal", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByRole("button", { name: "Embedded Analytics JS" }).click();
|
||||
});
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
@@ -55,10 +52,7 @@ describe(suiteTitle, () => {
|
||||
it("pre-selects question in embed flow when opened from question sharing modal", () => {
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane()
|
||||
.findByRole("button", { name: "Embedded Analytics JS" })
|
||||
.click();
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
@@ -84,10 +78,7 @@ describe(suiteTitle, () => {
|
||||
it("tracks default resources for pre-selected dashboard", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByRole("button", { name: "Embedded Analytics JS" }).click();
|
||||
});
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
@@ -100,17 +91,14 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "default",
|
||||
event_detail: "isDefaultResource=true,experience=dashboard",
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks default resources for pre-selected question", () => {
|
||||
H.visitQuestion(ORDERS_QUESTION_ID);
|
||||
H.openSharingMenu("Embed");
|
||||
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByRole("button", { name: "Embedded Analytics JS" }).click();
|
||||
});
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({ event: "embed_wizard_opened" });
|
||||
|
||||
@@ -123,7 +111,7 @@ describe(suiteTitle, () => {
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "embed_wizard_resource_selection_completed",
|
||||
event_detail: "default",
|
||||
event_detail: "isDefaultResource=true,experience=chart",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -658,12 +658,13 @@ describe("issue 26988", () => {
|
||||
},
|
||||
}).then(({ body: card }) => {
|
||||
H.visitDashboard(card.dashboard_id);
|
||||
});
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "lookAndFeel",
|
||||
previewMode: "preview",
|
||||
acceptTerms: false,
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: "dashboard",
|
||||
resourceId: card.dashboard_id,
|
||||
activeTab: "lookAndFeel",
|
||||
previewMode: "preview",
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait("@previewDashboard");
|
||||
|
||||
@@ -23,10 +23,7 @@ describe("scenarios > dashboard > subscriptions", () => {
|
||||
cy.findByTestId("public-link-popover-content").should("be.visible");
|
||||
|
||||
H.openSharingMenu("Embed");
|
||||
H.getEmbedModalSharingPane().within(() => {
|
||||
cy.findByText("public embedding").should("be.visible");
|
||||
cy.findByText("Static embedding").should("be.visible");
|
||||
});
|
||||
H.embedModalContent().should("be.visible");
|
||||
});
|
||||
|
||||
it("should allow sharing if dashboard contains only text cards (metabase#15077)", () => {
|
||||
|
||||
@@ -650,6 +650,9 @@ describe("scenarios > visualizations > pivot tables", { tags: "@slow" }, () => {
|
||||
});
|
||||
|
||||
H.visitQuestion(card_id);
|
||||
|
||||
cy.wrap(card_id).as("questionId");
|
||||
cy.wrap(dashboard_id).as("dashboardId");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -689,9 +692,25 @@ describe("scenarios > visualizations > pivot tables", { tags: "@slow" }, () => {
|
||||
});
|
||||
}
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
confirmSave: test.confirmSave,
|
||||
const { alias, resource } = {
|
||||
question: {
|
||||
alias: "@questionId",
|
||||
resource: "question",
|
||||
},
|
||||
dashboard: {
|
||||
alias: "@dashboardId",
|
||||
resource: "dashboard",
|
||||
},
|
||||
}[test.case];
|
||||
|
||||
cy.get(alias).then((id) => {
|
||||
H.openLegacyStaticEmbeddingModal({
|
||||
resource: resource,
|
||||
resourceId: id,
|
||||
activeTab: "parameters",
|
||||
confirmSave: test.confirmSave,
|
||||
unpublishBeforeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
// visit the iframe src directly to ensure it's not sing preview endpoints
|
||||
|
||||
@@ -133,6 +133,8 @@ const MetabotQuestionWrapped = (props: MetabotQuestionProps) => {
|
||||
|
||||
// side effect that activates the plugin
|
||||
METABOT_SDK_EE_PLUGIN.MetabotQuestion = Object.assign(
|
||||
withPublicComponentWrapper(MetabotQuestionWrapped),
|
||||
withPublicComponentWrapper(MetabotQuestionWrapped, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
{ schema: metabotQuestionSchema },
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ export const setup = ({
|
||||
|
||||
const { state } = setupSdkState({
|
||||
sdkState: createMockSdkState({
|
||||
loginStatus: createMockLoginStatusState({ status: "uninitialized" }),
|
||||
initStatus: createMockLoginStatusState({ status: "uninitialized" }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export const setup = (config: MetabaseConfigProps = {}) => {
|
||||
|
||||
const { state } = setupSdkState({
|
||||
sdkState: createMockSdkState({
|
||||
loginStatus: createMockLoginStatusState({ status: "uninitialized" }),
|
||||
initStatus: createMockLoginStatusState({ status: "uninitialized" }),
|
||||
}),
|
||||
});
|
||||
const view = renderWithProviders(<TestComponent config={config} />, {
|
||||
|
||||
@@ -2,7 +2,7 @@ export const SDK_NOT_STARTED_LOADING_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. Ensure all SDK components are wrapped in the Provider component.";
|
||||
|
||||
export const SDK_LOADING_ERROR_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. Verify the instance URL is correct and reachable.";
|
||||
"Error loading the Embedded Analytics SDK. Verify the instance is Enterprise and its URL is correct and reachable.";
|
||||
|
||||
export const SDK_NOT_LOADED_YET_MESSAGE =
|
||||
"Error loading the Embedded Analytics SDK. The loading state is `Loaded` but the SDK bundle is not loaded yet.";
|
||||
|
||||
@@ -89,11 +89,13 @@ export type {
|
||||
ChartColor,
|
||||
CreateDashboardValues,
|
||||
EntityTypeFilterKeys,
|
||||
InitializationStatus,
|
||||
LoginStatus,
|
||||
MetabaseAuthConfig,
|
||||
MetabaseAuthConfigWithApiKey,
|
||||
MetabaseAuthConfigWithJwt,
|
||||
MetabaseAuthConfigWithSaml,
|
||||
MetabaseIsGuestAuthConfig,
|
||||
MetabaseClickActionPluginsConfig,
|
||||
MetabaseColors,
|
||||
MetabaseClickAction,
|
||||
@@ -109,14 +111,17 @@ export type {
|
||||
MetabaseQuestion,
|
||||
MetabaseTheme,
|
||||
MetabaseUser,
|
||||
SdkEntityToken,
|
||||
SdkCollectionId,
|
||||
SdkDashboardId,
|
||||
SdkDashboardEntityPublicProps,
|
||||
SdkDashboardLoadEvent,
|
||||
SdkEntityId,
|
||||
SdkErrorComponent,
|
||||
SdkErrorComponentProps,
|
||||
SdkEventHandlersConfig,
|
||||
SdkQuestionId,
|
||||
SdkQuestionEntityPublicProps,
|
||||
SdkQuestionTitleProps,
|
||||
SdkUserId,
|
||||
SqlParameterValues,
|
||||
|
||||
@@ -16,14 +16,21 @@ import {
|
||||
*/
|
||||
export function initializePlugin() {
|
||||
if (hasPremiumFeature("content_translation")) {
|
||||
const getDictionaryBasePath = `/api/ee/content-translation/dictionary`;
|
||||
|
||||
Object.assign(PLUGIN_CONTENT_TRANSLATION, {
|
||||
isEnabled: true,
|
||||
useSortByContentTranslation,
|
||||
useTranslateContent,
|
||||
useTranslateFieldValuesInHoveredObject,
|
||||
useTranslateSeries,
|
||||
getDictionaryBasePath,
|
||||
setEndpointsForStaticEmbedding: (encodedToken: string) => {
|
||||
contentTranslationEndpoints.getDictionary = `/api/ee/content-translation/dictionary/${encodedToken}`;
|
||||
if (contentTranslationEndpoints.getDictionary) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentTranslationEndpoints.getDictionary = `${getDictionaryBasePath}/${encodedToken}`;
|
||||
},
|
||||
translateDisplayNames,
|
||||
ContentTranslationConfiguration,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { EmbeddingAppOriginDescription } from "./EmbeddingAppOriginDescription";
|
||||
@@ -1,10 +1,11 @@
|
||||
import cx from "classnames";
|
||||
import { jt, t } from "ttag";
|
||||
|
||||
import { AdminSettingInput } from "metabase/admin/settings/components/widgets/AdminSettingInput";
|
||||
import ExternalLink from "metabase/common/components/ExternalLink";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
|
||||
export const EmbeddingAppOriginDescription = () => {
|
||||
const Description = () => {
|
||||
return jt`Enter the origins for the websites or web apps where you want to allow embedding, separated by a space. Here are the ${(
|
||||
<ExternalLink
|
||||
key="specs"
|
||||
@@ -15,3 +16,13 @@ export const EmbeddingAppOriginDescription = () => {
|
||||
</ExternalLink>
|
||||
)} for what can be entered.`;
|
||||
};
|
||||
|
||||
export const InteractiveEmbeddingAuthorizedOriginsWidget = () => (
|
||||
<AdminSettingInput
|
||||
name="embedding-app-origins-interactive"
|
||||
title={t`Authorized origins`}
|
||||
description={<Description />}
|
||||
placeholder="https://*.example.com"
|
||||
inputType="text"
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
findRequests,
|
||||
setupPropertiesEndpoints,
|
||||
setupSettingsEndpoints,
|
||||
setupUpdateSettingEndpoint,
|
||||
} from "__support__/server-mocks";
|
||||
import { fireEvent, renderWithProviders, screen } from "__support__/ui";
|
||||
import { createMockSettings } from "metabase-types/api/mocks";
|
||||
|
||||
import { InteractiveEmbeddingAuthorizedOriginsWidget } from "./InteractiveEmbeddingAuthorizedOriginsWidget";
|
||||
|
||||
const setup = async ({ enabled }: { enabled: boolean }) => {
|
||||
const settings = createMockSettings({
|
||||
"enable-embedding-interactive": enabled,
|
||||
});
|
||||
|
||||
setupPropertiesEndpoints(settings);
|
||||
setupSettingsEndpoints([]);
|
||||
setupUpdateSettingEndpoint();
|
||||
|
||||
renderWithProviders(<InteractiveEmbeddingAuthorizedOriginsWidget />);
|
||||
};
|
||||
|
||||
describe("InteractiveEmbeddingAuthorizedOriginsWidget", () => {
|
||||
it("should allow changing authorized origins", async () => {
|
||||
await setup({ enabled: true });
|
||||
|
||||
const input = await screen.findByPlaceholderText("https://*.example.com");
|
||||
await userEvent.type(input, "https://*.foo.example.com");
|
||||
await fireEvent.blur(input);
|
||||
await screen.findByDisplayValue("https://*.foo.example.com");
|
||||
|
||||
const puts = await findRequests("PUT");
|
||||
expect(puts).toHaveLength(1);
|
||||
const [{ url, body }] = puts;
|
||||
expect(url).toContain("/setting/embedding-app-origins-interactive");
|
||||
expect(body).toEqual({ value: "https://*.foo.example.com" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { InteractiveEmbeddingAuthorizedOriginsWidget } from "./InteractiveEmbeddingAuthorizedOriginsWidget";
|
||||
@@ -1,60 +0,0 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import {
|
||||
RelatedSettingsSection,
|
||||
getInteractiveEmbeddingRelatedSettingItems,
|
||||
} from "metabase/admin/components/RelatedSettingsSection";
|
||||
import {
|
||||
SettingsPageWrapper,
|
||||
SettingsSection,
|
||||
} from "metabase/admin/components/SettingsSection";
|
||||
import { EmbeddingSettingsCard } from "metabase/admin/settings/components/EmbeddingSettings";
|
||||
import { AdminSettingInput } from "metabase/admin/settings/components/widgets/AdminSettingInput";
|
||||
import { UpsellDevInstances } from "metabase/admin/upsells";
|
||||
import { useDocsUrl } from "metabase/common/hooks";
|
||||
|
||||
import { EmbeddingAppOriginDescription } from "./EmbeddingAppOriginDescription";
|
||||
|
||||
const utmTags = {
|
||||
utm_source: "product",
|
||||
utm_medium: "docs",
|
||||
utm_campaign: "embedding-interactive",
|
||||
utm_content: "embedding-interactive-admin",
|
||||
};
|
||||
|
||||
export function InteractiveEmbeddingSettings() {
|
||||
const { url: quickStartUrl } = useDocsUrl(
|
||||
"embedding/interactive-embedding-quick-start-guide",
|
||||
{
|
||||
utm: utmTags,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsPageWrapper title={t`Interactive embedding`}>
|
||||
<EmbeddingSettingsCard
|
||||
title={t`Enable interactive embedding`}
|
||||
description={t`Embed the full power of Metabase into your application to build a custom analytics experience and programmatically manage dashboards and data.`}
|
||||
settingKey="enable-embedding-interactive"
|
||||
links={[{ icon: "bolt", title: t`Quick start`, href: quickStartUrl }]}
|
||||
/>
|
||||
|
||||
<SettingsSection>
|
||||
<AdminSettingInput
|
||||
name="embedding-app-origins-interactive"
|
||||
title={t`Authorized origins`}
|
||||
titleProps={{ fz: "lg", mb: "xs" }}
|
||||
description={<EmbeddingAppOriginDescription />}
|
||||
placeholder="https://*.example.com"
|
||||
inputType="text"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<RelatedSettingsSection
|
||||
items={getInteractiveEmbeddingRelatedSettingItems()}
|
||||
/>
|
||||
|
||||
<UpsellDevInstances location="embedding-page" />
|
||||
</SettingsPageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import cx from "classnames";
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import { EmbeddingSettingsCard } from "metabase/admin/settings/components/EmbeddingSettings";
|
||||
import ExternalLink from "metabase/common/components/ExternalLink";
|
||||
import { useDocsUrl, useSetting } from "metabase/common/hooks";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Stack } from "metabase/ui/components";
|
||||
import { InteractiveEmbeddingAuthorizedOriginsWidget } from "metabase-enterprise/embedding/components/InteractiveEmbeddingAuthorizedOriginsWidget";
|
||||
|
||||
const utmTags = {
|
||||
utm_source: "product",
|
||||
utm_medium: "docs",
|
||||
utm_campaign: "embedding-interactive",
|
||||
utm_content: "embedding-interactive-admin",
|
||||
};
|
||||
|
||||
export function InteractiveEmbeddingSettingsCard() {
|
||||
const isInteractiveEmbeddingEnabled = useSetting(
|
||||
"enable-embedding-interactive",
|
||||
);
|
||||
|
||||
const { url: quickStartUrl } = useDocsUrl(
|
||||
"embedding/interactive-embedding-quick-start-guide",
|
||||
{
|
||||
utm: utmTags,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<EmbeddingSettingsCard
|
||||
title={t`Enable legacy interactive embedding`}
|
||||
description={t`A way to embed the entire Metabase app in an iframe. It integrates your permissions and SSO to give people the right level ofaccess to query and drill-down into your data.`}
|
||||
settingKey="enable-embedding-interactive"
|
||||
alertInfoText={c(
|
||||
"{0} is the link to switch binaries. {1} is the link to upsell the SDK. {2} is the link to implement JWT or SAML authentication.",
|
||||
)
|
||||
.jt`The new embedding methods give you more flexibility. Interactive embedding and Static embedding will be deprecated in a future version of Metabase. ${(
|
||||
<ExternalLink
|
||||
key="migration-guide-from-legacy-embedding"
|
||||
href={quickStartUrl}
|
||||
className={cx(CS.link, CS.textBold)}
|
||||
>
|
||||
{t`Read more`}
|
||||
</ExternalLink>
|
||||
)}.`}
|
||||
>
|
||||
{isInteractiveEmbeddingEnabled && (
|
||||
<Stack gap="xl" px="xl" pb="lg">
|
||||
<InteractiveEmbeddingAuthorizedOriginsWidget />
|
||||
</Stack>
|
||||
)}
|
||||
</EmbeddingSettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
setupPropertiesEndpoints,
|
||||
setupSettingsEndpoints,
|
||||
setupUpdateSettingEndpoint,
|
||||
setupUpdateSettingsEndpoint,
|
||||
setupUserKeyValueEndpoints,
|
||||
} from "__support__/server-mocks";
|
||||
import { fireEvent, renderWithProviders, screen } from "__support__/ui";
|
||||
import { createMockSettings } from "metabase-types/api/mocks";
|
||||
|
||||
import { InteractiveEmbeddingSettings } from "./InteractiveEmbeddingSettings";
|
||||
import { InteractiveEmbeddingSettingsCard } from "./InteractiveEmbeddingSettingsCard";
|
||||
|
||||
const setup = async ({ enabled }: { enabled: boolean }) => {
|
||||
const settings = createMockSettings({
|
||||
@@ -20,44 +21,37 @@ const setup = async ({ enabled }: { enabled: boolean }) => {
|
||||
setupPropertiesEndpoints(settings);
|
||||
setupSettingsEndpoints([]);
|
||||
setupUpdateSettingEndpoint();
|
||||
setupUpdateSettingsEndpoint();
|
||||
setupUserKeyValueEndpoints({
|
||||
namespace: "user_acknowledgement",
|
||||
key: "upsell-dev_instances",
|
||||
value: true,
|
||||
});
|
||||
|
||||
renderWithProviders(<InteractiveEmbeddingSettings />);
|
||||
|
||||
await screen.findByText("Interactive embedding"); // breadcrumb
|
||||
renderWithProviders(<InteractiveEmbeddingSettingsCard />);
|
||||
};
|
||||
|
||||
describe("InteractiveEmbeddingSettings", () => {
|
||||
describe("InteractiveEmbeddingSettingsCard", () => {
|
||||
it("should show interactive embedding toggle", async () => {
|
||||
await setup({ enabled: true });
|
||||
|
||||
expect(
|
||||
await screen.findByText("Enable interactive embedding"),
|
||||
await screen.findByText("Enable legacy interactive embedding"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle interactive embedding on", async () => {
|
||||
await setup({ enabled: false });
|
||||
const toggle = await screen.findByLabelText(
|
||||
"Enable interactive embedding toggle",
|
||||
"Enable legacy interactive embedding toggle",
|
||||
);
|
||||
|
||||
await userEvent.click(toggle);
|
||||
const puts = await findRequests("PUT");
|
||||
expect(puts).toHaveLength(1);
|
||||
const [{ url, body }] = puts;
|
||||
expect(url).toContain("/setting/enable-embedding-interactive");
|
||||
expect(body).toEqual({ value: true });
|
||||
});
|
||||
|
||||
it("should show quickstart link", async () => {
|
||||
await setup({ enabled: true });
|
||||
|
||||
expect(await screen.findByText("Quick start")).toBeInTheDocument();
|
||||
expect(url).toContain("/setting");
|
||||
expect(body).toEqual({ "enable-embedding-interactive": true });
|
||||
});
|
||||
|
||||
it("should allow changing authorized origins", async () => {
|
||||
@@ -74,20 +68,4 @@ describe("InteractiveEmbeddingSettings", () => {
|
||||
expect(url).toContain("/setting/embedding-app-origins-interactive");
|
||||
expect(body).toEqual({ value: "https://*.foo.example.com" });
|
||||
});
|
||||
|
||||
it("should show cards with related settings", async () => {
|
||||
await setup({ enabled: true });
|
||||
|
||||
const relatedSettingCards = await screen.findAllByTestId(
|
||||
"related-setting-card",
|
||||
);
|
||||
expect(relatedSettingCards).toHaveLength(6);
|
||||
|
||||
expect(await screen.findByText("Authentication")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Databases")).toBeInTheDocument();
|
||||
expect(await screen.findByText("People")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Permissions")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Embedded Metabot")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Appearance")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { PLUGIN_ADMIN_SETTINGS, PLUGIN_EMBEDDING } from "metabase/plugins";
|
||||
import { isInteractiveEmbeddingEnabled } from "metabase-enterprise/embedding/selectors";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
import { InteractiveEmbeddingSettings } from "./components/InteractiveEmbeddingSettings";
|
||||
import { InteractiveEmbeddingSettingsCard } from "./components/InteractiveEmbeddingSettingsCard";
|
||||
|
||||
/**
|
||||
* We can't gate this component behind a feature flag, because SDK users could
|
||||
@@ -21,7 +21,7 @@ export function initializePlugin() {
|
||||
PLUGIN_EMBEDDING.isEnabled = () => true;
|
||||
PLUGIN_EMBEDDING.isInteractiveEmbeddingEnabled =
|
||||
isInteractiveEmbeddingEnabled;
|
||||
PLUGIN_ADMIN_SETTINGS.InteractiveEmbeddingSettings =
|
||||
InteractiveEmbeddingSettings;
|
||||
PLUGIN_ADMIN_SETTINGS.InteractiveEmbeddingSettingsCard =
|
||||
InteractiveEmbeddingSettingsCard;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { SdkIframeEmbedRoute } from "metabase/embedding/embedding-iframe-sdk/components/SdkIframeEmbedRoute";
|
||||
import { PLUGIN_EMBEDDING_IFRAME_SDK } from "metabase/plugins";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
// We allow users try out the iframe embedding without valid license on localhost.
|
||||
// This should always be usable on an EE instance regardless of license.
|
||||
PLUGIN_EMBEDDING_IFRAME_SDK.SdkIframeEmbedRoute = SdkIframeEmbedRoute;
|
||||
|
||||
if (hasPremiumFeature("embedding_simple")) {
|
||||
PLUGIN_EMBEDDING_IFRAME_SDK.hasValidLicense = () => true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SdkIframeEmbedSetupModal } from "metabase/embedding/embedding-iframe-sdk-setup/components/SdkIframeEmbedSetupModal";
|
||||
import { PLUGIN_EMBEDDING_IFRAME_SDK_SETUP } from "metabase/plugins";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
@@ -10,8 +9,5 @@ export function initializePlugin() {
|
||||
// In the future, we may drop this to enable users to try out new iframe embedding.
|
||||
if (hasPremiumFeature("embedding_simple")) {
|
||||
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP.isFeatureEnabled = () => true;
|
||||
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP.shouldShowEmbedInNewItemMenu = () => true;
|
||||
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP.SdkIframeEmbedSetupModal =
|
||||
SdkIframeEmbedSetupModal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ PLUGIN_IS_EE_BUILD.isEEBuild = () => true;
|
||||
import "./shared";
|
||||
|
||||
// CORE APP PLUGINS THAT USE hasPremiumFeature (import initialization functions):
|
||||
import { initializePlugin as initializeContentTranslation } from "./content_translation";
|
||||
import { initializePlugin as initializeEmbedding } from "./embedding";
|
||||
import { initializePlugin as initializeEmbeddingSdk } from "./embedding-sdk";
|
||||
import { initializePlugin as initializeMetabot } from "./metabot";
|
||||
@@ -22,6 +23,7 @@ export function initializePlugins() {
|
||||
initializeMetabot?.();
|
||||
initializeWhitelabelPlugin?.();
|
||||
initializeWhitelabelOverridePlugin?.();
|
||||
initializeContentTranslation?.();
|
||||
}
|
||||
|
||||
// "SDK EE-plugins", that are specific to the embedding sdk.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { SdkError } from "embedding-sdk-bundle/components/private/PublicComponentWrapper/SdkError";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getIsGuestEmbed } from "embedding-sdk-bundle/store/selectors";
|
||||
|
||||
export const GuestEmbedGuard = ({
|
||||
children,
|
||||
supportsGuestEmbed,
|
||||
}: PropsWithChildren<{
|
||||
componentName: string;
|
||||
supportsGuestEmbed: boolean;
|
||||
}>) => {
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
return isGuestEmbed && !supportsGuestEmbed ? (
|
||||
<SdkError message={t`This component does not support guest embeds`} />
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { SdkError } from "embedding-sdk-bundle/components/private/PublicComponen
|
||||
import { SdkLoader } from "embedding-sdk-bundle/components/private/PublicComponentWrapper/SdkLoader";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import {
|
||||
getLoginStatus,
|
||||
getInitStatus,
|
||||
getUsageProblem,
|
||||
} from "embedding-sdk-bundle/store/selectors";
|
||||
import type { CommonStylingProps } from "embedding-sdk-bundle/types/props";
|
||||
@@ -18,21 +18,21 @@ export const PublicComponentWrapper = forwardRef<
|
||||
HTMLDivElement,
|
||||
PublicComponentWrapperProps
|
||||
>(function PublicComponentWrapper({ children, className, style }, ref) {
|
||||
const loginStatus = useSdkSelector(getLoginStatus);
|
||||
const initStatus = useSdkSelector(getInitStatus);
|
||||
const usageProblem = useSdkSelector(getUsageProblem);
|
||||
|
||||
let content = children;
|
||||
|
||||
if (
|
||||
loginStatus.status === "uninitialized" ||
|
||||
loginStatus.status === "loading"
|
||||
initStatus.status === "uninitialized" ||
|
||||
initStatus.status === "loading"
|
||||
) {
|
||||
content = <SdkLoader />;
|
||||
}
|
||||
|
||||
if (loginStatus.status === "error") {
|
||||
if (initStatus.status === "error") {
|
||||
content = (
|
||||
<SdkError message={loginStatus.error.message} error={loginStatus.error} />
|
||||
<SdkError message={initStatus.error.message} error={initStatus.error} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { PublicComponentWrapper } from "./PublicComponentWrapper";
|
||||
const setup = (status: LoginStatus = { status: "uninitialized" }) => {
|
||||
const state = createMockState({
|
||||
sdk: createMockSdkState({
|
||||
loginStatus: createMockLoginStatusState(status),
|
||||
initStatus: createMockLoginStatusState(status),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import type React from "react";
|
||||
|
||||
import { GuestEmbedGuard } from "./GuestEmbedGuard";
|
||||
import { PublicComponentWrapper } from "./PublicComponentWrapper";
|
||||
|
||||
export function withPublicComponentWrapper<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
{ supportsGuestEmbed }: { supportsGuestEmbed: boolean },
|
||||
): (props: P) => React.ReactNode {
|
||||
const componentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
|
||||
const WithPublicComponentWrapper: React.FC<P> = (props) => {
|
||||
return (
|
||||
<PublicComponentWrapper>
|
||||
<WrappedComponent {...props} />
|
||||
</PublicComponentWrapper>
|
||||
<GuestEmbedGuard
|
||||
componentName={componentName}
|
||||
supportsGuestEmbed={supportsGuestEmbed}
|
||||
>
|
||||
<PublicComponentWrapper>
|
||||
<WrappedComponent {...props} />
|
||||
</PublicComponentWrapper>
|
||||
</GuestEmbedGuard>
|
||||
);
|
||||
};
|
||||
|
||||
WithPublicComponentWrapper.displayName = `withPublicComponentWrapper(${
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component"
|
||||
})`;
|
||||
WithPublicComponentWrapper.displayName = `withPublicComponentWrapper(${componentName})`;
|
||||
|
||||
return WithPublicComponentWrapper;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import cx from "classnames";
|
||||
|
||||
import type { CommonStylingProps } from "embedding-sdk-bundle/types/props";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { useTranslateContent } from "metabase/i18n/hooks";
|
||||
import {
|
||||
getAdHocQuestionDescription,
|
||||
shouldRenderAdhocDescription,
|
||||
@@ -49,6 +50,7 @@ export const QuestionTitle = ({
|
||||
style,
|
||||
}: QuestionTitleProps) => {
|
||||
const questionTitle = getQuestionTitle({ question });
|
||||
const tc = useTranslateContent();
|
||||
|
||||
if (questionTitle === null) {
|
||||
return null;
|
||||
@@ -56,7 +58,7 @@ export const QuestionTitle = ({
|
||||
|
||||
return (
|
||||
<h2 className={cx(CS.h2, CS.textWrap, className)} style={style}>
|
||||
{questionTitle}
|
||||
{tc(questionTitle)}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,11 @@ const DownloadWidgetInner = ({
|
||||
...rest
|
||||
}: DownloadWidgetProps &
|
||||
Pick<UseDownloadDataParams, "question" | "result">) => {
|
||||
const { withDownloads } = useSdkQuestionContext();
|
||||
const { withDownloads, token } = useSdkQuestionContext();
|
||||
const [, handleDownload] = useDownloadData({
|
||||
question,
|
||||
result,
|
||||
token,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
||||
|
||||
import { useSdkQuestionContext } from "embedding-sdk-bundle/components/private/SdkQuestion/context";
|
||||
import { ResponsiveParametersList } from "metabase/query_builder/components/ResponsiveParametersList";
|
||||
import { Box } from "metabase/ui";
|
||||
import * as Lib from "metabase-lib";
|
||||
import type { ParameterId } from "metabase-types/api";
|
||||
|
||||
@@ -17,6 +18,7 @@ export const SqlParametersList = () => {
|
||||
const {
|
||||
question,
|
||||
originalQuestion,
|
||||
token,
|
||||
parameterValues,
|
||||
updateParameterValues,
|
||||
hiddenParameters,
|
||||
@@ -49,7 +51,7 @@ export const SqlParametersList = () => {
|
||||
);
|
||||
}, [question, originalQuestion, hiddenParameters]);
|
||||
|
||||
if (!question || !isNativeQuestion) {
|
||||
if (!question || !isNativeQuestion || !parameters.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -63,18 +65,21 @@ export const SqlParametersList = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveParametersList
|
||||
classNames={{
|
||||
container: SqlParametersListS.SqlParametersListContainer,
|
||||
parametersList: SqlParametersListS.SqlParametersList,
|
||||
}}
|
||||
cardId={question.id()}
|
||||
dashboardId={question.dashboardId() ?? undefined}
|
||||
parameters={parameters}
|
||||
setParameterValue={setParameterValue}
|
||||
enableParameterRequiredBehavior
|
||||
commitImmediately={false}
|
||||
isSortable={false}
|
||||
/>
|
||||
<Box w="100%">
|
||||
<ResponsiveParametersList
|
||||
classNames={{
|
||||
container: SqlParametersListS.SqlParametersListContainer,
|
||||
parametersList: SqlParametersListS.SqlParametersList,
|
||||
}}
|
||||
cardId={question.id()}
|
||||
dashboardId={question.dashboardId() ?? undefined}
|
||||
token={token}
|
||||
parameters={parameters}
|
||||
setParameterValue={setParameterValue}
|
||||
enableParameterRequiredBehavior
|
||||
commitImmediately={false}
|
||||
isSortable={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
SdkLoader,
|
||||
} from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { shouldRunCardQuery } from "embedding-sdk-bundle/lib/sdk-question";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getIsGuestEmbed } from "embedding-sdk-bundle/store/selectors";
|
||||
import { useLocale } from "metabase/common/hooks/use-locale";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
|
||||
@@ -50,7 +52,9 @@ export const QuestionVisualization = ({
|
||||
updateQuestion,
|
||||
originalId,
|
||||
onVisualizationChange,
|
||||
token,
|
||||
} = useSdkQuestionContext();
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
const display = useMemo(() => question?.display(), [question]);
|
||||
|
||||
@@ -62,7 +66,7 @@ export const QuestionVisualization = ({
|
||||
|
||||
// When visualizing a question for the first time, there is no query result yet.
|
||||
const isQueryResultLoading =
|
||||
question && shouldRunCardQuery(question) && !queryResults;
|
||||
question && shouldRunCardQuery({ question, isGuestEmbed }) && !queryResults;
|
||||
|
||||
if (isLocaleLoading || isQuestionLoading || isQueryResultLoading) {
|
||||
return <SdkLoader />;
|
||||
@@ -97,6 +101,7 @@ export const QuestionVisualization = ({
|
||||
result={result}
|
||||
noHeader
|
||||
mode={mode}
|
||||
token={token}
|
||||
navigateToNewCardInsideQB={navigateToNewCard}
|
||||
onNavigateBack={onNavigateBack}
|
||||
onUpdateQuestion={(question: Question) =>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { createContext, useContext, useEffect, useMemo } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { SdkError } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { useExtractResourceIdFromJwtToken } from "embedding-sdk-bundle/hooks/private/use-extract-resource-id-from-jwt-token";
|
||||
import { useLoadQuestion } from "embedding-sdk-bundle/hooks/private/use-load-question";
|
||||
import { useSdkDispatch, useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getError, getPlugins } from "embedding-sdk-bundle/store/selectors";
|
||||
import {
|
||||
getError,
|
||||
getIsGuestEmbed,
|
||||
getPlugins,
|
||||
} from "embedding-sdk-bundle/store/selectors";
|
||||
import type { MetabasePluginsConfig } from "embedding-sdk-bundle/types/plugins";
|
||||
import { transformSdkQuestion } from "metabase/embedding-sdk/lib/transform-question";
|
||||
import type { MetabasePluginsConfig as InternalMetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
|
||||
import { PLUGIN_CONTENT_TRANSLATION } from "metabase/plugins";
|
||||
import {
|
||||
type OnCreateOptions,
|
||||
useCreateQuestion,
|
||||
@@ -33,7 +40,8 @@ export const SdkQuestionContext = createContext<
|
||||
const DEFAULT_OPTIONS = {};
|
||||
|
||||
export const SdkQuestionProvider = ({
|
||||
questionId,
|
||||
questionId: rawQuestionId,
|
||||
token: rawToken,
|
||||
options = DEFAULT_OPTIONS,
|
||||
deserializedCard,
|
||||
componentPlugins,
|
||||
@@ -54,6 +62,26 @@ export const SdkQuestionProvider = ({
|
||||
navigateToNewCard: userNavigateToNewCard,
|
||||
onVisualizationChange,
|
||||
}: SdkQuestionProviderProps) => {
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
const {
|
||||
resourceId: questionId,
|
||||
token,
|
||||
tokenError,
|
||||
} = useExtractResourceIdFromJwtToken({
|
||||
isGuestEmbed,
|
||||
resourceId: rawQuestionId,
|
||||
token: rawToken ?? undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isGuestEmbed && token) {
|
||||
PLUGIN_CONTENT_TRANSLATION.setEndpointsForStaticEmbedding(token);
|
||||
}
|
||||
}, [isGuestEmbed, token]);
|
||||
|
||||
const isNewQuestion = questionId === "new";
|
||||
|
||||
const error = useSdkSelector(getError);
|
||||
|
||||
const handleCreateQuestion = useCreateQuestion();
|
||||
@@ -113,6 +141,8 @@ export const SdkQuestionProvider = ({
|
||||
navigateToNewCard,
|
||||
} = useLoadQuestion({
|
||||
questionId,
|
||||
isGuestEmbed,
|
||||
token,
|
||||
options,
|
||||
deserializedCard,
|
||||
initialSqlParameters,
|
||||
@@ -142,6 +172,7 @@ export const SdkQuestionProvider = ({
|
||||
|
||||
const questionContext: SdkQuestionContextType = {
|
||||
originalId: questionId,
|
||||
token,
|
||||
isQuestionLoading,
|
||||
isQueryRunning,
|
||||
resetQuestion: loadAndQueryQuestion,
|
||||
@@ -173,8 +204,12 @@ export const SdkQuestionProvider = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenError) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadAndQueryQuestion();
|
||||
}, [loadAndQueryQuestion]);
|
||||
}, [loadAndQueryQuestion, tokenError]);
|
||||
|
||||
const dispatch = useSdkDispatch();
|
||||
|
||||
@@ -182,6 +217,18 @@ export const SdkQuestionProvider = ({
|
||||
dispatch(setEntityTypes(entityTypes));
|
||||
}, [dispatch, entityTypes]);
|
||||
|
||||
if (isGuestEmbed && isNewQuestion) {
|
||||
return (
|
||||
<SdkError
|
||||
message={t`You can't explore or save questions in Guest Embed mode`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
return <SdkError message={tokenError} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <SdkError message={error.message} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import type { LoadQuestionHookResult } from "embedding-sdk-bundle/hooks/private/use-load-question";
|
||||
import type { SdkEntityToken } from "embedding-sdk-bundle/types";
|
||||
import type { SdkCollectionId } from "embedding-sdk-bundle/types/collection";
|
||||
import type { MetabasePluginsConfig } from "embedding-sdk-bundle/types/plugins";
|
||||
import type {
|
||||
@@ -105,10 +106,18 @@ export type QuestionMockLocationParameters = {
|
||||
params: { slug?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type SdkQuestionEntityInternalProps = {
|
||||
questionId?: SdkQuestionId | null;
|
||||
token?: SdkEntityToken | null;
|
||||
};
|
||||
|
||||
export type SdkQuestionProviderProps = PropsWithChildren<
|
||||
SdkQuestionConfig &
|
||||
Omit<LoadSdkQuestionParams, "questionId"> & {
|
||||
questionId: SdkQuestionId | null;
|
||||
Omit<LoadSdkQuestionParams, "questionId"> &
|
||||
SdkQuestionEntityInternalProps & {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -139,6 +148,7 @@ export type SdkQuestionContextType = Omit<
|
||||
plugins: SdkQuestionConfig["componentPlugins"] | null;
|
||||
mode: QueryClickActionsMode | Mode | null | undefined;
|
||||
originalId: SdkQuestionId | null;
|
||||
token: string | null | undefined;
|
||||
resetQuestion: () => void;
|
||||
onReset: () => void;
|
||||
onCreate: (question: Question) => Promise<Question>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import { useTranslateContent } from "metabase/i18n/hooks";
|
||||
import { Anchor, Stack, Text } from "metabase/ui";
|
||||
|
||||
import { getQuestionTitle } from "../QuestionTitle";
|
||||
@@ -46,6 +47,7 @@ export const DefaultViewTitle = ({
|
||||
withResetButton = false,
|
||||
}: SdkQuestionDefaultViewProps) => {
|
||||
const { question, originalQuestion, onReset } = useSdkQuestionContext();
|
||||
const tc = useTranslateContent();
|
||||
|
||||
const isQuestionChanged = originalQuestion
|
||||
? question?.isQueryDirtyComparedTo(originalQuestion)
|
||||
@@ -58,7 +60,7 @@ export const DefaultViewTitle = ({
|
||||
if (title === undefined || title === true) {
|
||||
const originalName = originalQuestion?.displayName();
|
||||
|
||||
const titleText = getQuestionTitle({ question });
|
||||
const titleText = tc(getQuestionTitle({ question }));
|
||||
|
||||
return (
|
||||
<DefaultViewTitleText
|
||||
@@ -78,11 +80,13 @@ export const DefaultViewTitle = ({
|
||||
}
|
||||
|
||||
if (typeof title === "string") {
|
||||
const titleText = tc(title);
|
||||
|
||||
return (
|
||||
<DefaultViewTitleText
|
||||
title={
|
||||
<Text fw={700} c="var(--mb-color-text-primary)" fz="xl">
|
||||
{title}
|
||||
{titleText}
|
||||
</Text>
|
||||
}
|
||||
withResetButton={withResetButton}
|
||||
|
||||
@@ -21,6 +21,20 @@
|
||||
"Main";
|
||||
}
|
||||
|
||||
.ContainerTwoRows {
|
||||
display: grid;
|
||||
grid-template: auto 1fr / 1fr;
|
||||
grid-template-areas:
|
||||
"TopBar"
|
||||
"Main";
|
||||
}
|
||||
|
||||
.ContainerSingleRow {
|
||||
display: grid;
|
||||
grid-template: 1fr / 1fr;
|
||||
grid-template-areas: "Main";
|
||||
}
|
||||
|
||||
.TopBar {
|
||||
grid-area: TopBar;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ import {
|
||||
SdkLoader,
|
||||
} from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { QuestionVisualization } from "embedding-sdk-bundle/components/private/SdkQuestion/components/Visualization";
|
||||
import { SdkQuestion } from "embedding-sdk-bundle/components/public/SdkQuestion";
|
||||
import { useSdkBreadcrumbs } from "embedding-sdk-bundle/hooks/private/use-sdk-breadcrumb";
|
||||
import { shouldRunCardQuery } from "embedding-sdk-bundle/lib/sdk-question";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getIsGuestEmbed } from "embedding-sdk-bundle/store/selectors";
|
||||
import type { SdkQuestionTitleProps } from "embedding-sdk-bundle/types/question";
|
||||
import { SaveQuestionModal } from "metabase/common/components/SaveQuestionModal";
|
||||
import { useLocale } from "metabase/common/hooks/use-locale";
|
||||
@@ -88,6 +91,7 @@ export const SdkQuestionDefaultView = ({
|
||||
} = useSdkQuestionContext();
|
||||
|
||||
const { isBreadcrumbEnabled, reportLocation } = useSdkBreadcrumbs();
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
const isNewQuestion = originalId === "new";
|
||||
const isQuestionSaved = question?.isSaved();
|
||||
@@ -123,7 +127,7 @@ export const SdkQuestionDefaultView = ({
|
||||
|
||||
// When visualizing a question for the first time, there is no query result yet.
|
||||
const isQueryResultLoading =
|
||||
question && shouldRunCardQuery(question) && !queryResults;
|
||||
question && shouldRunCardQuery({ question, isGuestEmbed }) && !queryResults;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -239,6 +243,12 @@ export const SdkQuestionDefaultView = ({
|
||||
</Group>
|
||||
</ResultToolbar>
|
||||
)}
|
||||
|
||||
{isGuestEmbed && (
|
||||
<Box w="100%">
|
||||
<SdkQuestion.SqlParametersList />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
|
||||
@@ -205,6 +205,8 @@ const CollectionBrowserWrapper = ({
|
||||
};
|
||||
|
||||
export const CollectionBrowser = Object.assign(
|
||||
withPublicComponentWrapper(CollectionBrowserWrapper),
|
||||
withPublicComponentWrapper(CollectionBrowserWrapper, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
{ schema: collectionBrowserPropsSchema },
|
||||
);
|
||||
|
||||
@@ -4,10 +4,12 @@ import { type JSX, memo, useEffect, useId, useRef } from "react";
|
||||
|
||||
import { SdkThemeProvider } from "embedding-sdk-bundle/components/private/SdkThemeProvider";
|
||||
import { useInitDataInternal } from "embedding-sdk-bundle/hooks/private/use-init-data";
|
||||
import { useNormalizeComponentProviderProps } from "embedding-sdk-bundle/hooks/private/use-normalize-component-provider-props";
|
||||
import { getSdkStore } from "embedding-sdk-bundle/store";
|
||||
import {
|
||||
setErrorComponent,
|
||||
setEventHandlers,
|
||||
setIsGuestEmbed,
|
||||
setLoaderComponent,
|
||||
setPlugins,
|
||||
} from "embedding-sdk-bundle/store/reducer";
|
||||
@@ -63,16 +65,26 @@ export const ComponentProviderInternal = ({
|
||||
allowConsoleLog,
|
||||
isLocalHost,
|
||||
}: ComponentProviderInternalProps): JSX.Element => {
|
||||
const isGuestEmbed = !!authConfig.isGuest;
|
||||
const { fontFamily } = theme ?? {};
|
||||
|
||||
// The main call of useInitData happens in the MetabaseProvider
|
||||
// This call in the ComponentProvider is still needed for:
|
||||
// - Storybook stories, where we don't have the MetabaseProvider
|
||||
// - Unit tests
|
||||
useInitDataInternal({ reduxStore, authConfig, isLocalHost });
|
||||
useInitDataInternal({
|
||||
reduxStore,
|
||||
isGuestEmbed,
|
||||
authConfig,
|
||||
isLocalHost,
|
||||
});
|
||||
|
||||
useInitPlugins();
|
||||
|
||||
useEffect(() => {
|
||||
reduxStore.dispatch(setIsGuestEmbed(!!isGuestEmbed));
|
||||
}, [reduxStore, isGuestEmbed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fontFamily) {
|
||||
reduxStore.dispatch(setOptions({ font: fontFamily }));
|
||||
@@ -152,11 +164,13 @@ export const ComponentProvider = memo(function ComponentProvider({
|
||||
reduxStoreRef.current = props.reduxStore ?? getSdkStore();
|
||||
}
|
||||
|
||||
const normalizedProps = useNormalizeComponentProviderProps(props);
|
||||
|
||||
return (
|
||||
<MetabaseReduxProvider store={reduxStoreRef.current!}>
|
||||
<METABOT_SDK_EE_PLUGIN.MetabotProvider>
|
||||
<ComponentProviderInternal
|
||||
{...props}
|
||||
{...normalizedProps}
|
||||
reduxStore={reduxStoreRef.current!}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -74,6 +74,8 @@ const CreateDashboardModalInner = ({
|
||||
};
|
||||
|
||||
export const CreateDashboardModal = Object.assign(
|
||||
withPublicComponentWrapper(CreateDashboardModalInner),
|
||||
withPublicComponentWrapper(CreateDashboardModalInner, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
{ schema: createDashboardModalSchema },
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
|
||||
import {
|
||||
InteractiveQuestion,
|
||||
type InteractiveQuestionProps,
|
||||
@@ -13,6 +15,10 @@ export type CreateQuestionProps = Omit<
|
||||
"questionId" | "children"
|
||||
>;
|
||||
|
||||
export const CreateQuestion = (props: CreateQuestionProps = {}) => (
|
||||
const CreateQuestionInner = (props: CreateQuestionProps = {}) => (
|
||||
<InteractiveQuestion {...props} questionId="new" />
|
||||
);
|
||||
|
||||
export const CreateQuestion = withPublicComponentWrapper(CreateQuestionInner, {
|
||||
supportsGuestEmbed: false,
|
||||
}) as typeof CreateQuestionInner;
|
||||
|
||||
@@ -31,6 +31,7 @@ const propsSchema: Yup.SchemaOf<InteractiveQuestionProps> = Yup.object({
|
||||
.optional()
|
||||
.noUnknown(),
|
||||
questionId: Yup.mixed().required(),
|
||||
token: Yup.mixed().optional(),
|
||||
style: Yup.mixed().optional(),
|
||||
targetCollection: Yup.mixed().optional(),
|
||||
targetDashboardId: Yup.mixed().optional(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import {
|
||||
BackButton,
|
||||
Breakout,
|
||||
@@ -36,7 +37,7 @@ import { interactiveQuestionSchema } from "./InteractiveQuestion.schema";
|
||||
*/
|
||||
export type InteractiveQuestionProps = Omit<
|
||||
SdkQuestionProps,
|
||||
"getClickActionMode" | "navigateToNewCard" | "backToDashboard"
|
||||
"token" | "getClickActionMode" | "navigateToNewCard" | "backToDashboard"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -103,7 +104,9 @@ const subComponents: InteractiveQuestionComponents = {
|
||||
};
|
||||
|
||||
export const InteractiveQuestion = Object.assign(
|
||||
_InteractiveQuestion,
|
||||
withPublicComponentWrapper(_InteractiveQuestion, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
subComponents,
|
||||
{ schema: interactiveQuestionSchema },
|
||||
);
|
||||
|
||||
@@ -115,6 +115,7 @@ export type SdkQuestionComponents = {
|
||||
|
||||
export const _SdkQuestion = ({
|
||||
questionId,
|
||||
token,
|
||||
options,
|
||||
deserializedCard,
|
||||
plugins,
|
||||
@@ -145,6 +146,7 @@ export const _SdkQuestion = ({
|
||||
}: SdkQuestionProps): JSX.Element | null => (
|
||||
<SdkQuestionProvider
|
||||
questionId={questionId}
|
||||
token={token}
|
||||
options={options}
|
||||
deserializedCard={deserializedCard}
|
||||
componentPlugins={plugins}
|
||||
@@ -206,6 +208,8 @@ const subComponents: SdkQuestionComponents = {
|
||||
};
|
||||
|
||||
export const SdkQuestion = Object.assign(
|
||||
withPublicComponentWrapper(_SdkQuestion),
|
||||
withPublicComponentWrapper(_SdkQuestion, {
|
||||
supportsGuestEmbed: true,
|
||||
}),
|
||||
subComponents,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,12 @@ const propsSchema: Yup.SchemaOf<StaticQuestionProps> = Yup.object({
|
||||
height: Yup.mixed().optional(),
|
||||
initialSqlParameters: Yup.mixed().optional(),
|
||||
hiddenParameters: Yup.mixed().optional(),
|
||||
questionId: Yup.mixed().required(),
|
||||
questionId: Yup.mixed().when("token", {
|
||||
is: (token: unknown) => token !== undefined,
|
||||
then: (schema) => schema.optional(),
|
||||
otherwise: (schema) => schema.required(),
|
||||
}),
|
||||
token: Yup.mixed().optional(),
|
||||
style: Yup.mixed().optional(),
|
||||
title: Yup.mixed().optional(),
|
||||
width: Yup.mixed().optional(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { FlexibleSizeComponent } from "embedding-sdk-bundle/components/private/FlexibleSizeComponent";
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import {
|
||||
Breakout,
|
||||
BreakoutDropdown,
|
||||
@@ -26,23 +27,25 @@ import {
|
||||
SdkQuestion,
|
||||
type SdkQuestionProps,
|
||||
} from "embedding-sdk-bundle/components/public/SdkQuestion/SdkQuestion";
|
||||
import { StaticQuestionSdkMode } from "embedding-sdk-bundle/components/public/StaticQuestion/mode";
|
||||
import { useNormalizeGuestEmbedQuestionOrDashboardComponentProps } from "embedding-sdk-bundle/hooks/private/use-normalize-guest-embed-question-or-dashboard-component-props";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getIsGuestEmbed } from "embedding-sdk-bundle/store/selectors";
|
||||
import type { SdkQuestionEntityPublicProps } from "embedding-sdk-bundle/types/question";
|
||||
import { Box, Stack } from "metabase/ui";
|
||||
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
|
||||
import { EmbeddingSdkStaticMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkStaticMode";
|
||||
import type { ClickActionModeGetter } from "metabase/visualizations/types";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
|
||||
import { staticQuestionSchema } from "./StaticQuestion.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
* @category StaticQuestion
|
||||
*/
|
||||
export type StaticQuestionProps = PropsWithChildren<
|
||||
Pick<
|
||||
SdkQuestionProps,
|
||||
| "questionId"
|
||||
| "withChartTypeSelector"
|
||||
| "height"
|
||||
| "width"
|
||||
@@ -53,7 +56,8 @@ export type StaticQuestionProps = PropsWithChildren<
|
||||
| "withDownloads"
|
||||
| "title"
|
||||
>
|
||||
>;
|
||||
> &
|
||||
SdkQuestionEntityPublicProps;
|
||||
|
||||
/**
|
||||
* @interface
|
||||
@@ -77,19 +81,30 @@ export type StaticQuestionComponents = {
|
||||
SqlParametersList: typeof SqlParametersList;
|
||||
};
|
||||
|
||||
const StaticQuestionInner = ({
|
||||
questionId,
|
||||
withChartTypeSelector,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
style,
|
||||
initialSqlParameters,
|
||||
hiddenParameters,
|
||||
withDownloads,
|
||||
title = false, // Hidden by default for backwards-compatibility.
|
||||
children,
|
||||
}: StaticQuestionProps): JSX.Element | null => {
|
||||
const StaticQuestionInner = (
|
||||
props: StaticQuestionProps,
|
||||
): JSX.Element | null => {
|
||||
// Normalize props for Guest Embed usage (e.g. enforce withDownloads in OSS).
|
||||
const normalizedProps =
|
||||
useNormalizeGuestEmbedQuestionOrDashboardComponentProps(props);
|
||||
|
||||
const {
|
||||
questionId,
|
||||
token,
|
||||
withChartTypeSelector,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
style,
|
||||
initialSqlParameters,
|
||||
hiddenParameters,
|
||||
withDownloads,
|
||||
title = false, // Hidden by default for backwards-compatibility.
|
||||
children,
|
||||
} = normalizedProps;
|
||||
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
const getClickActionMode: ClickActionModeGetter = ({
|
||||
question,
|
||||
}: {
|
||||
@@ -99,16 +114,15 @@ const StaticQuestionInner = ({
|
||||
question &&
|
||||
getEmbeddingMode({
|
||||
question,
|
||||
queryMode: StaticQuestionSdkMode,
|
||||
queryMode: EmbeddingSdkStaticMode,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const hasTopBar = Boolean(title || withChartTypeSelector || withDownloads);
|
||||
|
||||
return (
|
||||
<SdkQuestion
|
||||
questionId={questionId}
|
||||
questionId={questionId ?? null}
|
||||
token={token}
|
||||
getClickActionMode={getClickActionMode}
|
||||
navigateToNewCard={null}
|
||||
initialSqlParameters={initialSqlParameters}
|
||||
@@ -128,18 +142,18 @@ const StaticQuestionInner = ({
|
||||
h="100%"
|
||||
gap="xs"
|
||||
>
|
||||
{hasTopBar && (
|
||||
<Stack className={InteractiveQuestionS.TopBar} gap="sm" p="md">
|
||||
{title && <DefaultViewTitle title={title} />}
|
||||
<Stack className={InteractiveQuestionS.TopBar} gap="sm" p="md">
|
||||
{title && <DefaultViewTitle title={title} />}
|
||||
|
||||
{(withChartTypeSelector || withDownloads) && (
|
||||
<ResultToolbar>
|
||||
{withChartTypeSelector && <SdkQuestion.ChartTypeDropdown />}
|
||||
{withDownloads && <SdkQuestion.DownloadWidgetDropdown />}
|
||||
</ResultToolbar>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{(withChartTypeSelector || withDownloads) && (
|
||||
<ResultToolbar>
|
||||
{withChartTypeSelector && <SdkQuestion.ChartTypeDropdown />}
|
||||
{withDownloads && <SdkQuestion.DownloadWidgetDropdown />}
|
||||
</ResultToolbar>
|
||||
)}
|
||||
|
||||
{isGuestEmbed && <SdkQuestion.SqlParametersList />}
|
||||
</Stack>
|
||||
|
||||
<Box className={InteractiveQuestionS.Main} w="100%" h="100%">
|
||||
<Box className={InteractiveQuestionS.Content}>
|
||||
@@ -178,7 +192,9 @@ const subComponents: StaticQuestionComponents = {
|
||||
};
|
||||
|
||||
export const StaticQuestion = Object.assign(
|
||||
StaticQuestionInner,
|
||||
withPublicComponentWrapper(StaticQuestionInner, {
|
||||
supportsGuestEmbed: true,
|
||||
}),
|
||||
subComponents,
|
||||
{ schema: staticQuestionSchema },
|
||||
);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { QueryClickActionsMode } from "metabase/visualizations/types";
|
||||
|
||||
export const StaticQuestionSdkMode: QueryClickActionsMode = {
|
||||
name: "static-question-sdk",
|
||||
hasDrills: false,
|
||||
clickActions: [],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const propsSchema: Yup.SchemaOf<EditableDashboardProps> = Yup.object({
|
||||
children: Yup.mixed().optional(),
|
||||
className: Yup.mixed().optional(),
|
||||
dashboardId: Yup.mixed().required(),
|
||||
token: Yup.mixed().optional(),
|
||||
dataPickerProps: Yup.object({
|
||||
entityTypes: Yup.mixed().optional(),
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { DASHBOARD_EDITING_ACTIONS } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
|
||||
import { DASHBOARD_ACTION } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/dashboard-action-keys";
|
||||
import type { MetabasePluginsConfig as InternalMetabasePluginsConfig } from "metabase/embedding-sdk/types/plugins";
|
||||
@@ -51,6 +52,11 @@ export const EditableDashboardInner = (props: EditableDashboardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableDashboard = Object.assign(EditableDashboardInner, {
|
||||
schema: editableDashboardSchema,
|
||||
});
|
||||
export const EditableDashboard = Object.assign(
|
||||
withPublicComponentWrapper(EditableDashboardInner, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
{
|
||||
schema: editableDashboardSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ const propsSchema: Yup.SchemaOf<InteractiveDashboardProps> = Yup.object({
|
||||
children: Yup.mixed().optional(),
|
||||
className: Yup.mixed().optional(),
|
||||
dashboardId: Yup.mixed().required(),
|
||||
token: Yup.mixed().optional(),
|
||||
dataPickerProps: Yup.object({
|
||||
entityTypes: Yup.mixed().optional(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getPlugins } from "embedding-sdk-bundle/store/selectors";
|
||||
import type { MetabasePluginsConfig } from "embedding-sdk-bundle/types/plugins";
|
||||
@@ -58,6 +59,11 @@ const InteractiveDashboardInner = (props: InteractiveDashboardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveDashboard = Object.assign(InteractiveDashboardInner, {
|
||||
schema: interactiveDashboardSchema,
|
||||
});
|
||||
export const InteractiveDashboard = Object.assign(
|
||||
withPublicComponentWrapper(InteractiveDashboardInner, {
|
||||
supportsGuestEmbed: false,
|
||||
}),
|
||||
{
|
||||
schema: interactiveDashboardSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -19,12 +19,14 @@ import {
|
||||
import { SdkAdHocQuestion } from "embedding-sdk-bundle/components/private/SdkAdHocQuestion";
|
||||
import { SdkQuestion } from "embedding-sdk-bundle/components/public/SdkQuestion/SdkQuestion";
|
||||
import { useDashboardLoadHandlers } from "embedding-sdk-bundle/hooks/private/use-dashboard-load-handlers";
|
||||
import { useExtractResourceIdFromJwtToken } from "embedding-sdk-bundle/hooks/private/use-extract-resource-id-from-jwt-token";
|
||||
import { useSdkBreadcrumbs } from "embedding-sdk-bundle/hooks/private/use-sdk-breadcrumb";
|
||||
import {
|
||||
type SdkDashboardDisplayProps,
|
||||
useSdkDashboardParams,
|
||||
} from "embedding-sdk-bundle/hooks/private/use-sdk-dashboard-params";
|
||||
import { useSdkDispatch, useSdkSelector } from "embedding-sdk-bundle/store";
|
||||
import { getIsGuestEmbed } from "embedding-sdk-bundle/store/selectors";
|
||||
import type { MetabaseQuestion } from "embedding-sdk-bundle/types";
|
||||
import type { DashboardEventHandlersProps } from "embedding-sdk-bundle/types/dashboard";
|
||||
import type { MetabasePluginsConfig } from "embedding-sdk-bundle/types/plugins";
|
||||
@@ -44,7 +46,9 @@ import {
|
||||
useDashboardContext,
|
||||
} from "metabase/dashboard/context";
|
||||
import { getDashboardComplete, getIsDirty } from "metabase/dashboard/selectors";
|
||||
import { isStaticEmbeddingEntityLoadingError } from "metabase/lib/errors/is-static-embedding-entity-loading-error";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { PLUGIN_CONTENT_TRANSLATION } from "metabase/plugins";
|
||||
import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
|
||||
import { resetErrorPage, setErrorPage } from "metabase/redux/app";
|
||||
import { dismissAllUndo } from "metabase/redux/undo";
|
||||
@@ -123,7 +127,8 @@ export type SdkDashboardInnerProps = SdkDashboardProps &
|
||||
>;
|
||||
|
||||
const SdkDashboardInner = ({
|
||||
dashboardId,
|
||||
dashboardId: rawDashboardId,
|
||||
token: rawToken,
|
||||
initialParameters = {},
|
||||
withTitle = true,
|
||||
withCardTitle = true,
|
||||
@@ -149,6 +154,24 @@ const SdkDashboardInner = ({
|
||||
dataPickerProps,
|
||||
onVisualizationChange,
|
||||
}: SdkDashboardInnerProps) => {
|
||||
const isGuestEmbed = useSdkSelector(getIsGuestEmbed);
|
||||
|
||||
const {
|
||||
resourceId: dashboardId,
|
||||
token,
|
||||
tokenError,
|
||||
} = useExtractResourceIdFromJwtToken({
|
||||
isGuestEmbed,
|
||||
resourceId: rawDashboardId,
|
||||
token: rawToken ?? undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isGuestEmbed && token) {
|
||||
PLUGIN_CONTENT_TRANSLATION.setEndpointsForStaticEmbedding(token);
|
||||
}
|
||||
}, [isGuestEmbed, token]);
|
||||
|
||||
const { handleLoad, handleLoadWithoutCards } = useDashboardLoadHandlers({
|
||||
onLoad,
|
||||
onLoadWithoutCards,
|
||||
@@ -158,12 +181,10 @@ const SdkDashboardInner = ({
|
||||
const { isBreadcrumbEnabled, reportLocation } = useSdkBreadcrumbs();
|
||||
|
||||
const { displayOptions } = useSdkDashboardParams({
|
||||
dashboardId,
|
||||
withDownloads,
|
||||
withTitle,
|
||||
withCardTitle,
|
||||
hiddenParameters,
|
||||
initialParameters,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -236,6 +257,22 @@ const SdkDashboardInner = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
return (
|
||||
<SdkDashboardStyledWrapper className={className} style={style}>
|
||||
<SdkError message={tokenError} />;
|
||||
</SdkDashboardStyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isStaticEmbeddingEntityLoadingError(errorPage, { isGuestEmbed })) {
|
||||
return (
|
||||
<SdkDashboardStyledWrapper className={className} style={style}>
|
||||
<SdkError message={errorPage.data ?? t`Something's gone wrong`} />
|
||||
</SdkDashboardStyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Passing an invalid entity ID format results in a 400 Bad Request.
|
||||
// We can show this as a generic "not found" error on the frontend.
|
||||
const isDashboardNotFound =
|
||||
@@ -244,7 +281,7 @@ const SdkDashboardInner = ({
|
||||
if (!dashboardId || isDashboardNotFound) {
|
||||
return (
|
||||
<SdkDashboardStyledWrapper className={className} style={style}>
|
||||
<DashboardNotFoundError id={dashboardId} />
|
||||
<DashboardNotFoundError id={dashboardId ?? ""} />
|
||||
</SdkDashboardStyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -263,6 +300,8 @@ const SdkDashboardInner = ({
|
||||
<DashboardContextProvider
|
||||
ref={dashboardContextProviderRef}
|
||||
dashboardId={dashboardId}
|
||||
token={token}
|
||||
isGuestEmbed={isGuestEmbed}
|
||||
parameterQueryParams={initialParameters}
|
||||
navigateToNewCardFromDashboard={
|
||||
navigateToNewCardFromDashboard !== undefined
|
||||
@@ -313,8 +352,8 @@ const SdkDashboardInner = ({
|
||||
}}
|
||||
autoScrollToDashcardId={autoScrollToDashcardId}
|
||||
>
|
||||
{match(finalRenderMode)
|
||||
.with("question", () => (
|
||||
{match({ finalRenderMode, isGuestEmbed })
|
||||
.with({ finalRenderMode: "question" }, () => (
|
||||
<SdkDashboardStyledWrapperWithRef className={className} style={style}>
|
||||
<SdkAdHocQuestion
|
||||
// `adhocQuestionUrl` would have value if renderMode is "question"
|
||||
@@ -327,7 +366,7 @@ const SdkDashboardInner = ({
|
||||
</SdkAdHocQuestion>
|
||||
</SdkDashboardStyledWrapperWithRef>
|
||||
))
|
||||
.with("dashboard", () => (
|
||||
.with({ finalRenderMode: "dashboard" }, () => (
|
||||
<SdkDashboardProvider
|
||||
plugins={plugins}
|
||||
onEditQuestion={onEditQuestion}
|
||||
@@ -342,29 +381,37 @@ const SdkDashboardInner = ({
|
||||
)}
|
||||
</SdkDashboardProvider>
|
||||
))
|
||||
.with("queryBuilder", () => (
|
||||
<DashboardQueryBuilder
|
||||
onCreate={(question) => {
|
||||
setNewDashboardQuestionId(question.id);
|
||||
setRenderMode("dashboard");
|
||||
dashboardContextProviderRef.current?.refetchDashboard();
|
||||
}}
|
||||
onNavigateBack={() => {
|
||||
setRenderMode("dashboard");
|
||||
}}
|
||||
dataPickerProps={dataPickerProps}
|
||||
onVisualizationChange={onVisualizationChange}
|
||||
/>
|
||||
))
|
||||
.with({ finalRenderMode: "queryBuilder" }, ({ isGuestEmbed }) =>
|
||||
isGuestEmbed ? (
|
||||
<SdkDashboardStyledWrapper className={className} style={style}>
|
||||
<SdkError
|
||||
message={t`You can't save questions in Guest Embed mode`}
|
||||
/>
|
||||
</SdkDashboardStyledWrapper>
|
||||
) : (
|
||||
<DashboardQueryBuilder
|
||||
onCreate={(question) => {
|
||||
setNewDashboardQuestionId(question.id);
|
||||
setRenderMode("dashboard");
|
||||
dashboardContextProviderRef.current?.refetchDashboard();
|
||||
}}
|
||||
onNavigateBack={() => {
|
||||
setRenderMode("dashboard");
|
||||
}}
|
||||
dataPickerProps={dataPickerProps}
|
||||
onVisualizationChange={onVisualizationChange}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.exhaustive()}
|
||||
{modalContent}
|
||||
</DashboardContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SdkDashboard = withPublicComponentWrapper(
|
||||
SdkDashboardInner,
|
||||
) as typeof SdkDashboardInner &
|
||||
export const SdkDashboard = withPublicComponentWrapper(SdkDashboardInner, {
|
||||
supportsGuestEmbed: true,
|
||||
}) as typeof SdkDashboardInner &
|
||||
Pick<
|
||||
typeof Dashboard,
|
||||
| "Grid"
|
||||
|
||||
@@ -7,7 +7,12 @@ import type { StaticDashboardProps } from "./StaticDashboard";
|
||||
const propsSchema: Yup.SchemaOf<StaticDashboardProps> = Yup.object({
|
||||
children: Yup.mixed().optional(),
|
||||
className: Yup.mixed().optional(),
|
||||
dashboardId: Yup.mixed().required(),
|
||||
dashboardId: Yup.mixed().when("token", {
|
||||
is: (token: unknown) => token !== undefined,
|
||||
then: (schema) => schema.optional(),
|
||||
otherwise: (schema) => schema.required(),
|
||||
}),
|
||||
token: Yup.mixed().optional(),
|
||||
dataPickerProps: Yup.object({
|
||||
entityTypes: Yup.mixed().optional(),
|
||||
})
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
import { withPublicComponentWrapper } from "embedding-sdk-bundle/components/private/PublicComponentWrapper";
|
||||
import { useNormalizeGuestEmbedQuestionOrDashboardComponentProps } from "embedding-sdk-bundle/hooks/private/use-normalize-guest-embed-question-or-dashboard-component-props";
|
||||
import type { SdkDashboardEntityPublicProps } from "embedding-sdk-bundle/types/dashboard";
|
||||
import { PublicOrEmbeddedDashCardMenu } from "metabase/dashboard/components/DashCard/PublicOrEmbeddedDashCardMenu";
|
||||
import { DASHBOARD_ACTION } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/dashboard-action-keys";
|
||||
import { isQuestionCard } from "metabase/dashboard/utils";
|
||||
import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
|
||||
import { EmbeddingSdkStaticMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkStaticMode";
|
||||
import type { ClickActionModeGetter } from "metabase/visualizations/types";
|
||||
|
||||
import { StaticQuestionSdkMode } from "../../StaticQuestion/mode";
|
||||
import { SdkDashboard, type SdkDashboardProps } from "../SdkDashboard";
|
||||
|
||||
import { staticDashboardSchema } from "./StaticDashboard.schema";
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @expand
|
||||
* @category Dashboard
|
||||
*/
|
||||
export type StaticDashboardProps = Omit<
|
||||
SdkDashboardProps,
|
||||
| "dashboardId"
|
||||
| "token"
|
||||
| "drillThroughQuestionProps"
|
||||
| "drillThroughQuestionHeight"
|
||||
| "renderDrillThroughQuestion"
|
||||
>;
|
||||
> &
|
||||
SdkDashboardEntityPublicProps;
|
||||
|
||||
const StaticDashboardInner = (props: StaticDashboardProps) => {
|
||||
// Normalize props for Guest Embed usage (e.g. enforce withDownloads in OSS).
|
||||
const normalizedProps =
|
||||
useNormalizeGuestEmbedQuestionOrDashboardComponentProps(props);
|
||||
|
||||
const { withDownloads } = normalizedProps;
|
||||
|
||||
const getClickActionMode: ClickActionModeGetter = ({ question }) =>
|
||||
getEmbeddingMode({
|
||||
question,
|
||||
queryMode: StaticQuestionSdkMode,
|
||||
queryMode: EmbeddingSdkStaticMode,
|
||||
});
|
||||
|
||||
return (
|
||||
<SdkDashboard
|
||||
{...props}
|
||||
{...(normalizedProps as SdkDashboardProps)}
|
||||
getClickActionMode={getClickActionMode}
|
||||
dashboardActions={({ downloadsEnabled }) =>
|
||||
downloadsEnabled.pdf ? [DASHBOARD_ACTION.DOWNLOAD_PDF] : []
|
||||
}
|
||||
navigateToNewCardFromDashboard={null}
|
||||
dashcardMenu={({ dashcard, result }) =>
|
||||
props.withDownloads &&
|
||||
withDownloads &&
|
||||
isQuestionCard(dashcard.card) &&
|
||||
!!result?.data &&
|
||||
!result?.error && (
|
||||
@@ -48,6 +59,11 @@ const StaticDashboardInner = (props: StaticDashboardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const StaticDashboard = Object.assign(StaticDashboardInner, {
|
||||
schema: staticDashboardSchema,
|
||||
});
|
||||
export const StaticDashboard = Object.assign(
|
||||
withPublicComponentWrapper(StaticDashboardInner, {
|
||||
supportsGuestEmbed: true,
|
||||
}),
|
||||
{
|
||||
schema: staticDashboardSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { screen, waitFor, within } from "__support__/ui";
|
||||
import type { SdkDashboardProps } from "embedding-sdk-bundle/components/public/dashboard/SdkDashboard";
|
||||
|
||||
import {
|
||||
type SetupSdkDashboardOptions,
|
||||
@@ -18,7 +20,7 @@ const setup = async (
|
||||
) => {
|
||||
return setupSdkDashboard({
|
||||
...options,
|
||||
component: StaticDashboard,
|
||||
component: StaticDashboard as ComponentType<SdkDashboardProps>,
|
||||
});
|
||||
};
|
||||
console.warn = () => {};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { indexBy } from "underscore";
|
||||
|
||||
import { setupEnterpriseOnlyPlugin } from "__support__/enterprise";
|
||||
@@ -110,10 +111,10 @@ export const DEFAULT_DASHCARDS: DashboardCard[] = [
|
||||
];
|
||||
|
||||
export interface SetupSdkDashboardOptions {
|
||||
props?: Partial<SdkDashboardProps>;
|
||||
props?: Omit<Partial<SdkDashboardProps>, "token">;
|
||||
providerProps?: Partial<MetabaseProviderProps>;
|
||||
isLocaleLoading?: boolean;
|
||||
component: React.ComponentType<SdkDashboardProps>;
|
||||
component: ComponentType<SdkDashboardProps>;
|
||||
dashboardName?: string;
|
||||
dataPickerProps?: EditableDashboardProps["dataPickerProps"];
|
||||
dashcards?: DashboardCard[];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user