From d149840374c9e020ccb206271f4a14da8a371f45 Mon Sep 17 00:00:00 2001
From: "Mahatthana (Kelvin) Nomsawadi" <me@bboykelvin.dev>
Date: Tue, 21 Nov 2023 12:14:06 +0700
Subject: [PATCH] Fix embedding crashes when one of the cards fails to load
 (#35851)

* Fix static/public embed crashes when query returns error

* Add E2E test to reproduce the issue

* Sort the key more consistently
---
 .../embedding/embedding-dashboard.cy.spec.js  | 81 +++++++++++++++++++
 frontend/src/metabase-types/api/dataset.ts    | 18 +++--
 .../src/metabase-types/api/mocks/dataset.ts   | 42 ++++++----
 frontend/src/metabase/dashboard/utils.ts      |  7 +-
 .../PublicQuestion.unit.spec.tsx              |  4 +-
 .../test/__support__/server-mocks/public.ts   |  4 +-
 6 files changed, 131 insertions(+), 25 deletions(-)

diff --git a/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js b/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
index 63b9b0faf77..8621c6bb83e 100644
--- a/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-dashboard.cy.spec.js
@@ -6,6 +6,7 @@ import {
   filterWidget,
   visitIframe,
   getDashboardCard,
+  addOrUpdateDashboardCard,
 } from "e2e/support/helpers";
 
 import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
@@ -249,6 +250,86 @@ describe("scenarios > embedding > dashboard parameters", () => {
         .should("be.visible");
     });
   });
+
+  it("should render error without crashing when embed query returns error (metabase#34954)", () => {
+    const categoryTemplateTag = {
+      type: "text",
+      name: "category",
+      id: "377a4a4a-179e-4d86-8263-f3b3887df15f",
+      "display-name": "Category",
+    };
+    const questionDetails = {
+      native: {
+        query: "Select * from products Where category = {{category}}",
+        "template-tags": {
+          category: categoryTemplateTag,
+        },
+      },
+    };
+
+    const dashboardParameter = {
+      name: "Category",
+      slug: "category",
+      id: "9cd1ee78",
+      type: "string/=",
+      sectionId: "string",
+      values_query_type: "none",
+    };
+    const dashboardDetails = {
+      name: 'dashboard with "category" parameter',
+      parameters: [dashboardParameter],
+    };
+
+    cy.createNativeQuestionAndDashboard({
+      questionDetails,
+      dashboardDetails,
+    }).then(({ body: { card_id, dashboard_id } }) => {
+      cy.wrap(dashboard_id).as("dashboardId2");
+
+      addOrUpdateDashboardCard({
+        card_id,
+        dashboard_id,
+        card: {
+          parameter_mappings: [
+            {
+              parameter_id: dashboardParameter.id,
+              card_id,
+              target: ["variable", ["template-tag", categoryTemplateTag.name]],
+            },
+          ],
+          visualization_settings: {
+            "card.hide_empty": true,
+          },
+        },
+      });
+
+      cy.request("PUT", `/api/dashboard/${dashboard_id}`, {
+        embedding_params: {
+          category: "enabled",
+        },
+        enable_embedding: true,
+      });
+
+      const payload = {
+        resource: { dashboard: dashboard_id },
+        params: {},
+      };
+
+      visitEmbeddedPage(payload);
+    });
+
+    cy.log("The whole page would have crashed before the fix at this point");
+    getDashboardCard()
+      .findByText("There was a problem displaying this chart.")
+      .should("be.visible");
+
+    cy.log("Add a filter to complete the query");
+    filterWidget().findByPlaceholderText("Category").type("Widget{enter}");
+
+    getDashboardCard()
+      .findByText("Practical Bronze Computer")
+      .should("be.visible");
+  });
 });
 
 describe("scenarios > embedding > dashboard parameters with defaults", () => {
diff --git a/frontend/src/metabase-types/api/dataset.ts b/frontend/src/metabase-types/api/dataset.ts
index 2d02e046f1f..d613d1d8e1f 100644
--- a/frontend/src/metabase-types/api/dataset.ts
+++ b/frontend/src/metabase-types/api/dataset.ts
@@ -72,7 +72,7 @@ export interface Dataset {
   status?: string;
 }
 
-export interface PublicDatasetData {
+export interface EmbedDatasetData {
   rows: RowValues[];
   cols: DatasetColumn[];
   rows_truncated: number;
@@ -82,10 +82,18 @@ export interface PublicDatasetData {
   results_timezone?: string;
 }
 
-export interface PublicDataset {
-  data: PublicDatasetData;
-  json_query?: JsonQuery;
-  status?: string;
+export type EmbedDataset = SuccessEmbedDataset | ErrorEmbedDataset;
+
+interface SuccessEmbedDataset {
+  data: EmbedDatasetData;
+  json_query: JsonQuery;
+  status: string;
+}
+
+export interface ErrorEmbedDataset {
+  error_type: string;
+  error: string;
+  status: string;
 }
 
 export interface NativeQueryForm {
diff --git a/frontend/src/metabase-types/api/mocks/dataset.ts b/frontend/src/metabase-types/api/mocks/dataset.ts
index aa9db7ea082..3a6c7099f88 100644
--- a/frontend/src/metabase-types/api/mocks/dataset.ts
+++ b/frontend/src/metabase-types/api/mocks/dataset.ts
@@ -2,8 +2,9 @@ import type {
   Dataset,
   DatasetColumn,
   DatasetData,
-  PublicDataset,
-  PublicDatasetData,
+  EmbedDataset,
+  EmbedDatasetData,
+  ErrorEmbedDataset,
   ResultsMetadata,
   TemplateTag,
 } from "metabase-types/api/dataset";
@@ -57,7 +58,7 @@ export const createMockDataset = ({
   ...opts,
 });
 
-export const createMockPublicDatasetData = ({
+export const createMockEmbedDatasetData = ({
   cols = [
     createMockColumn({
       display_name: "NAME",
@@ -66,7 +67,7 @@ export const createMockPublicDatasetData = ({
     }),
   ],
   ...opts
-}: Partial<PublicDatasetData>): PublicDatasetData => ({
+}: Partial<EmbedDatasetData>): EmbedDatasetData => ({
   rows: [],
   cols,
   rows_truncated: 0,
@@ -74,16 +75,29 @@ export const createMockPublicDatasetData = ({
   ...opts,
 });
 
-export const createMockPublicDataset = ({
-  data = {},
-  ...opts
-}: MockDatasetOpts = {}): PublicDataset => ({
-  data: createMockPublicDatasetData(data),
-  database_id: 1,
-  row_count: 0,
-  running_time: 1000,
-  ...opts,
-});
+export type MockEmbedDatasetOpts = Omit<Partial<EmbedDataset>, "data"> & {
+  data?: Partial<EmbedDatasetData>;
+};
+
+export const createMockEmbedDataset = (
+  opts: MockEmbedDatasetOpts,
+): EmbedDataset => {
+  if ("data" in opts) {
+    const { data = {}, ...rest } = opts;
+    return {
+      data: createMockEmbedDatasetData(data),
+      json_query: {
+        database: 1,
+        type: "native",
+        native: { query: "SELECT 1" },
+      },
+      status: "success",
+      ...rest,
+    };
+  }
+
+  return opts as ErrorEmbedDataset;
+};
 
 export const createMockTemplateTag = (
   opts?: Partial<TemplateTag>,
diff --git a/frontend/src/metabase/dashboard/utils.ts b/frontend/src/metabase/dashboard/utils.ts
index 1219757d2c4..13b66f58c87 100644
--- a/frontend/src/metabase/dashboard/utils.ts
+++ b/frontend/src/metabase/dashboard/utils.ts
@@ -19,6 +19,7 @@ import type {
   Parameter,
   StructuredDatasetQuery,
   ActionDashboardCard,
+  EmbedDataset,
 } from "metabase-types/api";
 import type { SelectedTabId } from "metabase-types/store";
 import Question from "metabase-lib/Question";
@@ -225,14 +226,16 @@ const isDashcardDataLoaded = (
   return data != null && Object.values(data).every(result => result != null);
 };
 
-const hasRows = (dashcardData: Record<CardId, Dataset>) => {
+const hasRows = (dashcardData: Record<CardId, Dataset | EmbedDataset>) => {
   const queryResults = dashcardData
     ? Object.values(dashcardData).filter(Boolean)
     : [];
 
   return (
     queryResults.length > 0 &&
-    queryResults.every(queryResult => queryResult.data.rows.length > 0)
+    queryResults.every(
+      queryResult => "data" in queryResult && queryResult.data.rows.length > 0,
+    )
   );
 };
 
diff --git a/frontend/src/metabase/public/containers/PublicQuestion/PublicQuestion.unit.spec.tsx b/frontend/src/metabase/public/containers/PublicQuestion/PublicQuestion.unit.spec.tsx
index 91d1bd45221..1dd66aef2a8 100644
--- a/frontend/src/metabase/public/containers/PublicQuestion/PublicQuestion.unit.spec.tsx
+++ b/frontend/src/metabase/public/containers/PublicQuestion/PublicQuestion.unit.spec.tsx
@@ -9,7 +9,7 @@ import registerVisualizations from "metabase/visualizations/register";
 import { createMockState } from "metabase-types/store/mocks";
 import {
   createMockPublicCard,
-  createMockPublicDataset,
+  createMockEmbedDataset,
 } from "metabase-types/api/mocks";
 
 import { PublicQuestion } from "./PublicQuestion";
@@ -27,7 +27,7 @@ async function setup() {
   );
   setupPublicCardQueryEndpoints(
     FAKE_UUID,
-    createMockPublicDataset({
+    createMockEmbedDataset({
       data: { rows: [["John W."]] },
     }),
   );
diff --git a/frontend/test/__support__/server-mocks/public.ts b/frontend/test/__support__/server-mocks/public.ts
index a0af23b86f9..79a1645a303 100644
--- a/frontend/test/__support__/server-mocks/public.ts
+++ b/frontend/test/__support__/server-mocks/public.ts
@@ -1,6 +1,6 @@
 import fetchMock from "fetch-mock";
 
-import type { PublicCard, PublicDataset } from "metabase-types/api";
+import type { PublicCard, EmbedDataset } from "metabase-types/api";
 
 export function setupPublicQuestionEndpoints(
   uuid: string,
@@ -11,7 +11,7 @@ export function setupPublicQuestionEndpoints(
 
 export function setupPublicCardQueryEndpoints(
   uuid: string,
-  publicDataset: PublicDataset,
+  publicDataset: EmbedDataset,
 ) {
   fetchMock.get(`path:/api/public/card/${uuid}/query`, publicDataset);
 }
-- 
GitLab