From da628b52381ba3ad2d81345b54476e74f2843bf4 Mon Sep 17 00:00:00 2001 From: "metabase-bot[bot]" <109303359+metabase-bot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 08:28:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20backported=20"Fix=20embedding"?= =?UTF-8?q?=20(#40969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix embedding (#40841) * Fix overflow static dashboard * Fix `frame` event's height for interactive dashboards * Fix E2E tests * Fix test names to match the correct GitHub issue numbers * Use `data-element-id` for consistency * Make the test selector more consistent * Use the new backported util from master --------- Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev> Co-authored-by: Mahatthana Nomsawadi <mahatthana.n@gmail.com> --- e2e/support/helpers/e2e-embedding-helpers.js | 13 +++ .../helpers/e2e-ui-elements-helpers.js | 2 +- ...scalar-first-render-size-change.cy.spec.js | 17 +--- .../dashboard-filters-auto-apply.cy.spec.js | 13 +-- .../interactive-embedding.cy.spec.js | 91 ++++++++++++++++--- ...-when-embedding-long-dashboards.cy.spec.js | 59 ++++++++++++ .../src/metabase-types/api/mocks/dashboard.ts | 9 +- .../components/Dashboard/Dashboard.styled.tsx | 3 + .../components/Dashboard/Dashboard.tsx | 5 +- frontend/src/metabase/lib/embed.js | 24 ++++- .../metabase/nav/components/AppBar/AppBar.tsx | 6 +- .../EmbedFrame/EmbedFrame.styled.tsx | 1 + 12 files changed, 194 insertions(+), 49 deletions(-) create mode 100644 e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js diff --git a/e2e/support/helpers/e2e-embedding-helpers.js b/e2e/support/helpers/e2e-embedding-helpers.js index 3fa187c63d1..a4f6efa7454 100644 --- a/e2e/support/helpers/e2e-embedding-helpers.js +++ b/e2e/support/helpers/e2e-embedding-helpers.js @@ -261,3 +261,16 @@ export function createPublicQuestionLink(questionId) { export function createPublicDashboardLink(dashboardId) { return cy.request("POST", `/api/dashboard/${dashboardId}/public_link`, {}); } + +export const visitFullAppEmbeddingUrl = ({ url, qs, onBeforeLoad }) => { + cy.visit({ + url, + qs, + onBeforeLoad(window) { + // cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests + // by removing the property the app would work in embedding mode + window.Cypress = undefined; + onBeforeLoad?.(window); + }, + }); +}; diff --git a/e2e/support/helpers/e2e-ui-elements-helpers.js b/e2e/support/helpers/e2e-ui-elements-helpers.js index f998e154a16..539a4fa5699 100644 --- a/e2e/support/helpers/e2e-ui-elements-helpers.js +++ b/e2e/support/helpers/e2e-ui-elements-helpers.js @@ -201,5 +201,5 @@ export const undoToastList = () => { }; export function dashboardCards() { - return cy.get("#Dashboard-Cards-Container"); + return cy.get("[data-element-id=dashboard-cards-container]"); } diff --git a/e2e/test/scenarios/dashboard-cards/reproductions/29304-scalar-first-render-size-change.cy.spec.js b/e2e/test/scenarios/dashboard-cards/reproductions/29304-scalar-first-render-size-change.cy.spec.js index 2f0784839d1..9f9f3e212e2 100644 --- a/e2e/test/scenarios/dashboard-cards/reproductions/29304-scalar-first-render-size-change.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/reproductions/29304-scalar-first-render-size-change.cy.spec.js @@ -1,5 +1,5 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; -import { restore } from "e2e/support/helpers"; +import { restore, visitFullAppEmbeddingUrl } from "e2e/support/helpers"; // Couldn't import from `metabase/components/ExplicitSize` because dependency issue. // It will fail Cypress tests. @@ -102,18 +102,3 @@ describe("issue 29304", () => { }); }); }); - -// Use full-app embedding to test because `ExplicitSize` checks for `isCypressActive`, -// which checks `window.Cypress`, and will disable the refresh mode on Cypress test. -// If we test by simply visiting the dashboard, the refresh mode will be disabled, -// and we won't be able to reproduce the problem. -const visitFullAppEmbeddingUrl = ({ url }) => { - cy.visit({ - url, - onBeforeLoad(window) { - // cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests - // by removing the property the app would work in embedding mode - window.Cypress = undefined; - }, - }); -}; diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js index 6bcb1fb4b9e..eb3f132cf4b 100644 --- a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js @@ -19,6 +19,7 @@ import { undoToast, visitDashboard, visitEmbeddedPage, + visitFullAppEmbeddingUrl, visitPublicDashboard, } from "e2e/support/helpers"; @@ -729,15 +730,3 @@ const openSlowFullAppEmbeddingDashboard = (params = {}) => { getDashboardCard().should("be.visible"); }; - -const visitFullAppEmbeddingUrl = ({ url, qs }) => { - cy.visit({ - url, - qs, - onBeforeLoad(window) { - // cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests - // by removing the property the app would work in embedding mode - window.Cypress = undefined; - }, - }); -}; diff --git a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js index 2ac72dfad59..00d1aba3da3 100644 --- a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js +++ b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js @@ -18,8 +18,12 @@ import { dashboardGrid, createDashboardWithTabs, goToTab, + visitFullAppEmbeddingUrl, } from "e2e/support/helpers"; -import { createMockDashboardCard } from "metabase-types/api/mocks"; +import { + createMockDashboardCard, + createMockTextDashboardCard, +} from "metabase-types/api/mocks"; const { ORDERS } = SAMPLE_DATABASE; @@ -458,6 +462,79 @@ describeEE("scenarios > embedding > full app", () => { }, ); }); + + it("should send `frame` message with dashboard height when the dashboard is resized (metabase#37437)", () => { + const TAB_1 = { id: 1, name: "Tab 1" }; + const TAB_2 = { id: 2, name: "Tab 2" }; + createDashboardWithTabs({ + tabs: [TAB_1, TAB_2], + name: "Dashboard", + dashcards: [ + createMockTextDashboardCard({ + dashboard_tab_id: TAB_1.id, + size_x: 10, + size_y: 20, + text: "I am a text card", + }), + ], + }).then(dashboard => { + visitFullAppEmbeddingUrl({ + url: `/dashboard/${dashboard.id}`, + onBeforeLoad(window) { + cy.spy(window.parent, "postMessage").as("postMessage"); + }, + }); + }); + cy.get("@postMessage") + .should("have.been.calledWith", { + metabase: { + type: "frame", + frame: { + mode: "fit", + height: Cypress.sinon.match(value => value > 1000), + }, + }, + }) + .and("not.have.been.calledWith", { + metabase: { + type: "frame", + frame: { + mode: "fit", + height: Cypress.sinon.match(value => value < 400), + }, + }, + }); + + cy.findByRole("tab", { name: TAB_2.name }).click(); + cy.get("@postMessage").should("have.been.calledWith", { + metabase: { + type: "frame", + frame: { + mode: "fit", + height: Cypress.sinon.match(value => value < 400), + }, + }, + }); + + cy.findByTestId("app-bar").findByText("Our analytics").click(); + + cy.findByRole("heading", { name: "Metabase analytics" }).should( + "be.visible", + ); + cy.get("@postMessage").then(postMessage => { + expect( + postMessage.lastCall.calledWith({ + metabase: { + type: "frame", + frame: { + mode: "fit", + height: 800, + }, + }, + }), + ).to.be.true; + }); + }); }); describe("x-ray dashboards", () => { @@ -480,18 +557,6 @@ describeEE("scenarios > embedding > full app", () => { }); }); -const visitFullAppEmbeddingUrl = ({ url, qs }) => { - cy.visit({ - url, - qs, - onBeforeLoad(window) { - // cypress runs all tests in an iframe and the app uses this property to avoid embedding mode for all tests - // by removing the property the app would work in embedding mode - window.Cypress = undefined; - }, - }); -}; - const visitQuestionUrl = urlOptions => { visitFullAppEmbeddingUrl(urlOptions); cy.wait("@getCardQuery"); diff --git a/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js b/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js new file mode 100644 index 00000000000..d0b68551d2f --- /dev/null +++ b/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js @@ -0,0 +1,59 @@ +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; +import { + getIframeBody, + openStaticEmbeddingModal, + restore, + updateDashboardCards, + visitDashboard, +} from "e2e/support/helpers"; + +const { PRODUCTS_ID } = SAMPLE_DATABASE; + +const questionDetails = { + name: "Products", + query: { "source-table": PRODUCTS_ID, limit: 2 }, +}; + +const dashboardDetails = { + name: "long dashboard", + enable_embedding: true, +}; + +describe("issue 40660", () => { + beforeEach(() => { + cy.intercept("GET", "/api/preview_embed/dashboard/**").as( + "previewDashboard", + ); + cy.intercept("GET", "/api/dashboard/**/params/**/values").as("values"); + + restore(); + cy.signInAsAdmin(); + + cy.createQuestionAndDashboard({ + questionDetails, + dashboardDetails, + }).then(({ body: { id, card_id, dashboard_id } }) => { + updateDashboardCards({ + dashboard_id, + cards: [{ card_id }, { card_id }, { card_id }], + }); + + visitDashboard(dashboard_id); + }); + }); + + it("static dashboard content shouldn't overflow its container (metabase#40660)", () => { + openStaticEmbeddingModal({ + activeTab: "parameters", + previewMode: "preview", + }); + + getIframeBody().within(() => { + cy.findByTestId("embed-frame").scrollTo("bottom"); + + cy.findByRole("link", { name: "Powered by Metabase" }).should( + "be.visible", + ); + }); + }); +}); diff --git a/frontend/src/metabase-types/api/mocks/dashboard.ts b/frontend/src/metabase-types/api/mocks/dashboard.ts index 211f9821015..5e95ad14e73 100644 --- a/frontend/src/metabase-types/api/mocks/dashboard.ts +++ b/frontend/src/metabase-types/api/mocks/dashboard.ts @@ -131,14 +131,15 @@ export const createMockVirtualDashCard = ( }; }; -export const createMockTextDashboardCard = ( - opts?: VirtualDashboardCardOpts & { text?: string }, -): VirtualDashboardCard => +export const createMockTextDashboardCard = ({ + text, + ...opts +}: VirtualDashboardCardOpts & { text?: string } = {}): VirtualDashboardCard => createMockVirtualDashCard({ ...opts, card: createMockVirtualCard({ display: "text" }), visualization_settings: { - text: opts?.text ?? "Body Text", + text: text ?? "Body Text", }, }); diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.styled.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.styled.tsx index 79eec528a99..e2ec6045a10 100644 --- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.styled.tsx +++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.styled.tsx @@ -139,6 +139,9 @@ export const ParametersAndCardsContainer = styled.div<{ overflow-x: clip; } padding-bottom: 40px; + /* Makes sure it doesn't use all the height, so the actual content height could be used in embedding #37437 */ + align-self: ${({ shouldMakeDashboardHeaderStickyAfterScrolling }) => + !shouldMakeDashboardHeaderStickyAfterScrolling && "flex-start"}; &.${SAVING_DOM_IMAGE_CLASS} { ${ParametersWidgetContainer} { diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx index fe6d128bd98..852ee34c47a 100644 --- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx +++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx @@ -533,6 +533,8 @@ function DashboardInner(props: DashboardProps) { {() => ( <DashboardStyled> <DashboardHeaderContainer + data-element-id="dashboard-header-container" + id="Dashboard-Header-Container" isFullscreen={isFullscreen} isNightMode={shouldRenderAsNightMode} > @@ -553,13 +555,14 @@ function DashboardInner(props: DashboardProps) { <DashboardBody isEditingOrSharing={isEditing || isSharing}> <ParametersAndCardsContainer id={DASHBOARD_PDF_EXPORT_ROOT_ID} + data-element-id="dashboard-parameters-and-cards" data-testid="dashboard-parameters-and-cards" shouldMakeDashboardHeaderStickyAfterScrolling={ !isFullscreen && (isEditing || isSharing) } > {renderParameterList()} - <CardsContainer id="Dashboard-Cards-Container"> + <CardsContainer data-element-id="dashboard-cards-container"> {renderContent()} </CardsContainer> </ParametersAndCardsContainer> diff --git a/frontend/src/metabase/lib/embed.js b/frontend/src/metabase/lib/embed.js index 8d22fd6c9cf..760d1c7e8fe 100644 --- a/frontend/src/metabase/lib/embed.js +++ b/frontend/src/metabase/lib/embed.js @@ -59,8 +59,30 @@ function sendMessage(message) { function getFrameSpec() { if (isFitViewportMode()) { - return { mode: "fit", height: document.body.scrollHeight }; + return { mode: "fit", height: getScrollHeight() }; } else { return { mode: "normal", height: document.body.scrollHeight }; } } + +function defaultGetScrollHeight() { + return document.body.scrollHeight; +} + +function getScrollHeight() { + const appBarHeight = + document.getElementById("[data-element-id=app-bar]")?.offsetHeight ?? 0; + const dashboardHeaderHeight = + document.querySelector("[data-element-id=dashboard-header-container]") + ?.offsetHeight ?? 0; + const dashboardContentHeight = + document.querySelector("[data-element-id=dashboard-parameters-and-cards]") + ?.scrollHeight ?? 0; + const dashboardHeight = dashboardHeaderHeight + dashboardContentHeight; + + if (dashboardHeight > 0) { + return appBarHeight + dashboardHeight; + } + + return defaultGetScrollHeight(); +} diff --git a/frontend/src/metabase/nav/components/AppBar/AppBar.tsx b/frontend/src/metabase/nav/components/AppBar/AppBar.tsx index 776e968c3ba..a5c97973616 100644 --- a/frontend/src/metabase/nav/components/AppBar/AppBar.tsx +++ b/frontend/src/metabase/nav/components/AppBar/AppBar.tsx @@ -28,7 +28,11 @@ const AppBar = (props: AppBarProps): JSX.Element => { const isSmallScreen = useIsSmallScreen(); return ( - <AppBarRoot data-testid="app-bar" aria-label={t`Navigation bar`}> + <AppBarRoot + data-element-id="app-bar" + data-testid="app-bar" + aria-label={t`Navigation bar`} + > <ErrorBoundary> {isSmallScreen ? ( <AppBarSmall {...props} /> diff --git a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.styled.tsx b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.styled.tsx index b1916b44389..f2c6108c47d 100644 --- a/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.styled.tsx +++ b/frontend/src/metabase/public/components/EmbedFrame/EmbedFrame.styled.tsx @@ -17,6 +17,7 @@ export const Root = styled.div<{ }>` display: flex; flex-direction: column; + overflow: auto; ${props => props.hasScroll && -- GitLab