From d616baf693f9aa890eba1685b3a415589c3236cd Mon Sep 17 00:00:00 2001 From: "Mahatthana (Kelvin) Nomsawadi" <me@bboykelvin.dev> Date: Fri, 25 Oct 2024 23:27:18 +0700 Subject: [PATCH] Fix static embed card download when param has accentuation (#49157) * Fix static embed card download when param has accentuation * Fix E2E util type * Add a dashboard E2E test * test question download with params * Fix some names regarding tests * Fix failed E2E tests --- .../api/createDashboardWithQuestions.ts | 15 +- .../embed-resource-downloads.cy.spec.ts | 176 ++++++++++++++++++ frontend/src/metabase/redux/downloads.ts | 15 +- 3 files changed, 191 insertions(+), 15 deletions(-) diff --git a/e2e/support/helpers/api/createDashboardWithQuestions.ts b/e2e/support/helpers/api/createDashboardWithQuestions.ts index d2a269cd292..99b8d6d1955 100644 --- a/e2e/support/helpers/api/createDashboardWithQuestions.ts +++ b/e2e/support/helpers/api/createDashboardWithQuestions.ts @@ -1,4 +1,4 @@ -import type { Card, Dashboard } from "metabase-types/api"; +import type { Card, Dashboard, DashboardCard } from "metabase-types/api"; import { cypressWaitAll } from "../e2e-misc-helpers"; @@ -16,14 +16,11 @@ export const createDashboardWithQuestions = ({ dashboardName?: string; dashboardDetails?: DashboardDetails; questions: (NativeQuestionDetails | StructuredQuestionDetails)[]; - cards?: Partial<Card>[]; -}): Cypress.Chainable< - Cypress.Response<{ - dashboard: Dashboard; - questions: Card; - }> -> => { - // @ts-expect-error - Cypress typings don't account for what happens in then() here + cards?: Partial<DashboardCard>[]; +}): Cypress.Chainable<{ + dashboard: Dashboard; + questions: Card[]; +}> => { return createDashboard({ name: dashboardName, ...dashboardDetails }).then( ({ body: dashboard }) => { return cypressWaitAll( diff --git a/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts b/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts index 534b746e04f..cd5258f9e6f 100644 --- a/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts +++ b/e2e/test/scenarios/embedding/embed-resource-downloads.cy.spec.ts @@ -1,8 +1,12 @@ +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { ORDERS_BY_YEAR_QUESTION_ID, ORDERS_DASHBOARD_ID, } from "e2e/support/cypress_sample_instance_data"; import { + addOrUpdateDashboardCard, + createDashboardWithQuestions, + createNativeQuestion, describeWithSnowplowEE, expectGoodSnowplowEvent, expectNoBadSnowplowEvents, @@ -16,6 +20,9 @@ import { showDashboardCardActions, visitEmbeddedPage, } from "e2e/support/helpers"; +import { createMockParameter } from "metabase-types/api/mocks"; + +const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE; /** These tests are about the `downloads` flag for static embeds, both dashboards and questions. * Unless the product changes, these should test the same things as `public-resource-downloads.cy.spec.ts` @@ -120,6 +127,89 @@ describeWithSnowplowEE( export_type: "csv", }); }); + + describe("with dashboard parameters", () => { + beforeEach(() => { + cy.signInAsAdmin(); + + setTokenFeatures("all"); + + // Test parameter with accentuation (metabase#49118) + const CATEGORY_FILTER = createMockParameter({ + id: "5aefc725", + name: "usuário", + slug: "usu%C3%A1rio", + type: "string/=", + }); + const questionDetails = { + name: "Products", + query: { + "source-table": PRODUCTS_ID, + }, + }; + createDashboardWithQuestions({ + // Can't figure out the type if I extracted `dashboardDetails` to a variable. + dashboardDetails: { + name: "Dashboard with a parameter", + parameters: [CATEGORY_FILTER], + enable_embedding: true, + embedding_params: { + [CATEGORY_FILTER.slug]: "enabled", + }, + }, + questions: [questionDetails], + }).then(({ dashboard, questions }) => { + const dashboardId = dashboard.id; + cy.wrap(dashboardId).as("dashboardId"); + const questionId = questions[0].id; + addOrUpdateDashboardCard({ + dashboard_id: dashboardId, + card_id: questionId, + card: { + parameter_mappings: [ + { + card_id: questionId, + parameter_id: CATEGORY_FILTER.id, + target: ["dimension", ["field", PRODUCTS.CATEGORY, null]], + }, + ], + }, + }); + }); + + cy.signOut(); + }); + + it("should be able to download a static embedded dashcard as CSV", () => { + cy.get("@dashboardId").then(dashboardId => { + visitEmbeddedPage( + { + resource: { dashboard: Number(dashboardId) }, + params: {}, + }, + { + pageStyle: { + downloads: true, + }, + }, + ); + }); + + waitLoading(); + + showDashboardCardActions(); + getDashboardCardMenu().click(); + exportFromDashcard(".csv"); + cy.verifyDownload(".csv", { contains: true }); + + expectGoodSnowplowEvent({ + event: "download_results_clicked", + resource_type: "dashcard", + accessed_via: "static-embed", + export_type: "csv", + }); + }); + }); }); describe("Static embed questions", () => { @@ -216,6 +306,92 @@ describeWithSnowplowEE( export_type: "csv", }); }); + + describe("with native question parameters", () => { + beforeEach(() => { + cy.signInAsAdmin(); + + setTokenFeatures("all"); + + // Can't figure out the type if I extracted `questionDetails` to a variable. + createNativeQuestion( + { + name: "Native question with a parameter", + native: { + "template-tags": { + num: { + id: "f7672b4d-1e84-1fa8-bf02-b5e584cd4535", + name: "num", + "display-name": "Num", + type: "number", + default: null, + }, + }, + query: "select {{num}}", + }, + parameters: [ + { + id: "f7672b4d-1e84-1fa8-bf02-b5e584cd4535", + type: "number/=", + target: ["variable", ["template-tag", "num"]], + name: "Num", + slug: "num", + default: null, + }, + ], + enable_embedding: true, + embedding_params: { + num: "enabled", + }, + }, + { + idAlias: "questionId", + wrapId: true, + }, + ); + + cy.signOut(); + }); + + it("should be able to download a static embedded dashcard as CSV", () => { + const value = 9999; + cy.get("@questionId").then(questionId => { + visitEmbeddedPage( + { + resource: { question: Number(questionId) }, + params: { + num: value, + }, + }, + { + pageStyle: { + downloads: true, + }, + }, + ); + }); + + waitLoading(); + + main().findByText(value).should("exist"); + + cy.findByTestId("download-button").click(); + + popover().within(() => { + cy.findByText(".csv").click(); + cy.findByTestId("download-results-button").click(); + }); + + cy.verifyDownload(".csv", { contains: true }); + + expectGoodSnowplowEvent({ + event: "download_results_clicked", + resource_type: "question", + accessed_via: "static-embed", + export_type: "csv", + }); + }); + }); }); }, ); diff --git a/frontend/src/metabase/redux/downloads.ts b/frontend/src/metabase/redux/downloads.ts index 76046612aaf..a4153974439 100644 --- a/frontend/src/metabase/redux/downloads.ts +++ b/frontend/src/metabase/redux/downloads.ts @@ -206,20 +206,23 @@ const getDatasetParams = ({ return { method: "GET", url: `/api/embed/dashboard/${token}/dashcard/${dashcardId}/card/${cardId}/${type}`, - params: Urls.getEncodedUrlSearchParams({ ...params, ...exportParams }), + params: new URLSearchParams({ + parameters: JSON.stringify(params), + ..._.mapObject(exportParams, value => String(value)), + }), }; } if (resource === "question" && token) { - // For whatever wacky reason the /api/embed endpoint expect params like ?key=value instead - // of like ?params=<json-encoded-params-array> like the other endpoints do. const params = new URLSearchParams(window.location.search); - params.set("format_rows", String(enableFormatting)); - params.set("pivot_results", String(enablePivot)); + return { method: "GET", url: Urls.embedCard(token, type), - params, + params: new URLSearchParams({ + parameters: JSON.stringify(Object.fromEntries(params)), + ..._.mapObject(exportParams, value => String(value)), + }), }; } } -- GitLab