Repository: metabase/metabase
Pull request #54700: fix(sdk): Consolidate StaticQuestion code using InteractiveQuestion code
Author: login: oisincoveney
Labels: name: .Team/Embedding
name: double-backport
Created: 2025-03-05T01:38:51Z | Merged: 2025-03-14T14:05:57Z

Removes most of the code for `StaticQuestion` and replaces it with a version of the `InteractiveQuestion` that is essentially read only.

There are two parts to this:
- Ensuring that `navigateToNewCardInsideQB` is `undefined`
- Adding a new `mode` called `StaticQuestionSdkMode` that is in the `enterprise` folder and has no drills or actions available. We could probably use the `PublicMode` instead if we want to reuse that instead

I'm not necessarily sure we _need_ to have the `mode`, since setting `navigateToNewCardInsideQB` already creates the setup where there won't be any click actions on the visualization. In the name of safety (since people may be using this component to ensure that questions aren't modified etc etc) I don't want to take that risk. Let me know what you think and if my fears are unfounded.

In terms of testing, use the `StaticQuestion` component within your own SDK app, in a local shoppy instance, or within Storybook. In terms of performance and everything else it has felt relatively identical.

To start storybook, run `yarn run storybook-embedding-sdk` and ensure that `yarn embedding-sdk:dev` is running in a different terminal.


Diff:
diff --git a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/ChartTypeSelectorList/ChartTypeDropdown.tsx b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/ChartTypeSelectorList/ChartTypeDropdown.tsx
index 9d2067a22619..93947fe8cb39 100644
--- a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/ChartTypeSelectorList/ChartTypeDropdown.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/ChartTypeSelectorList/ChartTypeDropdown.tsx
@@ -61,8 +61,9 @@ export const ChartTypeDropdown = (menuProps: MenuProps) => {
       <Menu.Target>
         <ToolbarButton
           data-testid="chart-type-selector-button"
-          label={selectedElem.label}
-          icon={selectedElem.iconName}
+          disabled={!selectedElem}
+          label={selectedElem?.label}
+          icon={selectedElem?.iconName}
           isHighlighted={false}
           variant="default"
           px={undefined}
diff --git a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
index 46c86b213fb5..4fe531643d12 100644
--- a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
@@ -4,9 +4,11 @@ import { t } from "ttag";
 import type { FlexibleSizeProps } from "embedding-sdk/components/private/FlexibleSizeComponent";
 import { FlexibleSizeComponent } from "embedding-sdk/components/private/FlexibleSizeComponent";
 import {
+  QuestionNotFoundError,
   SdkError,
   SdkLoader,
 } from "embedding-sdk/components/private/PublicComponentWrapper";
+import { shouldRunCardQuery } from "embedding-sdk/lib/interactive-question";
 import CS from "metabase/css/core/index.css";
 import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
 import type Question from "metabase-lib/v1/Question";
@@ -28,17 +30,26 @@ export const QuestionVisualization = ({
     navigateToNewCard,
     onNavigateBack,
     updateQuestion,
+    variant,
+    isCardIdError,
+    originalId,
   } = useInteractiveQuestionContext();
 
   // When visualizing a question for the first time, there is no query result yet.
-  const isQueryResultLoading = question && !queryResults;
+  const isQueryResultLoading =
+    question && shouldRunCardQuery(question) && !queryResults;
 
   if (isQuestionLoading || isQueryResultLoading) {
     return <SdkLoader />;
   }
 
-  if (!question) {
-    return <SdkError message={t`Question not found`} />;
+  // `isCardError: true` when the entity ID couldn't be resolved
+  if (!question || isCardIdError) {
+    if (originalId && originalId !== "new") {
+      return <QuestionNotFoundError id={originalId} />;
+    } else {
+      return <SdkError message={t`Question not found`}></SdkError>;
+    }
   }
 
   const [result] = queryResults ?? [];
@@ -62,7 +73,9 @@ export const QuestionVisualization = ({
         result={result}
         noHeader
         mode={mode}
-        navigateToNewCardInsideQB={navigateToNewCard}
+        navigateToNewCardInsideQB={
+          variant === "static" ? undefined : navigateToNewCard
+        }
         onNavigateBack={onNavigateBack}
         onUpdateQuestion={(question: Question) =>
           updateQuestion(question, { run: false })
diff --git a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx
index 6785ebe2f93d..b21777729ded 100644
--- a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx
@@ -1,5 +1,6 @@
 import { createContext, useContext, useEffect, useMemo } from "react";
 
+import { StaticQuestionSdkMode } from "embedding-sdk/components/public/StaticQuestion/mode";
 import { useLoadQuestion } from "embedding-sdk/hooks/private/use-load-question";
 import { transformSdkQuestion } from "embedding-sdk/lib/transform-question";
 import { useSdkSelector } from "embedding-sdk/store";
@@ -9,6 +10,7 @@ import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated
 import { useCreateQuestion } from "metabase/query_builder/containers/use-create-question";
 import { useSaveQuestion } from "metabase/query_builder/containers/use-save-question";
 import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
+import { EmbeddingSdkMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkMode";
 import type Question from "metabase-lib/v1/Question";
 
 import type {
@@ -56,6 +58,7 @@ export const InteractiveQuestionProvider = ({
   targetCollection,
   initialSqlParameters,
   withDownloads,
+  variant,
 }: InteractiveQuestionProviderProps) => {
   const {
     id: questionId,
@@ -133,10 +136,12 @@ export const InteractiveQuestionProvider = ({
       question &&
       getEmbeddingMode({
         question,
+        queryMode:
+          variant === "static" ? StaticQuestionSdkMode : EmbeddingSdkMode,
         plugins: combinedPlugins ?? undefined,
       })
     );
-  }, [question, combinedPlugins]);
+  }, [question, variant, combinedPlugins]);
 
   const questionContext: InteractiveQuestionContextType = {
     originalId: initialQuestionId,
@@ -161,6 +166,7 @@ export const InteractiveQuestionProvider = ({
     targetCollection,
     isCardIdError,
     withDownloads,
+    variant,
   };
 
   useEffect(() => {
diff --git a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/types.ts b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/types.ts
index aa83c720b5cf..94eb841eca96 100644
--- a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/types.ts
+++ b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/types.ts
@@ -52,6 +52,7 @@ export type InteractiveQuestionProviderProps = PropsWithChildren<
   InteractiveQuestionConfig &
     Omit<LoadSdkQuestionParams, "questionId"> & {
       questionId: InteractiveQuestionId;
+      variant?: "static" | "interactive";
     }
 >;
 
@@ -63,6 +64,7 @@ export type InteractiveQuestionContextType = Omit<
     InteractiveQuestionConfig,
     "onNavigateBack" | "isSaveEnabled" | "targetCollection" | "withDownloads"
   > &
+  Pick<InteractiveQuestionProviderProps, "variant"> &
   Pick<QBNotebookProps, "modelsFilterList"> & {
     plugins: InteractiveQuestionConfig["componentPlugins"] | null;
     mode: Mode | null | undefined;
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.unit.spec.tsx b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.unit.spec.tsx
index bcd273c1ac1a..650e861293af 100644
--- a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.unit.spec.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.unit.spec.tsx
@@ -1,4 +1,5 @@
 import userEvent from "@testing-library/user-event";
+import fetchMock from "fetch-mock";
 
 import {
   setupAlertsEndpoints,
@@ -16,7 +17,10 @@ import {
   waitForLoaderToBeRemoved,
   within,
 } from "__support__/ui";
-import { InteractiveQuestionDefaultView } from "embedding-sdk/components/private/InteractiveQuestionDefaultView";
+import {
+  InteractiveQuestionDefaultView,
+  type InteractiveQuestionDefaultViewProps,
+} from "embedding-sdk/components/private/InteractiveQuestionDefaultView";
 import { renderWithSDKProviders } from "embedding-sdk/test/__support__/ui";
 import { createMockAuthProviderUriConfig } from "embedding-sdk/test/mocks/config";
 import { setupSdkState } from "embedding-sdk/test/server-mocks/sdk-init";
@@ -28,13 +32,23 @@ import {
   createMockDatabase,
   createMockDataset,
   createMockDatasetData,
+  createMockParameter,
   createMockTable,
   createMockUser,
 } from "metabase-types/api/mocks";
 
 import { useInteractiveQuestionContext } from "../../private/InteractiveQuestion/context";
 
-import { InteractiveQuestion } from "./InteractiveQuestion";
+import {
+  InteractiveQuestion,
+  type InteractiveQuestionProps,
+} from "./InteractiveQuestion";
+
+const TEST_PARAM = createMockParameter({
+  type: "number/=",
+  slug: "product_id",
+  target: ["variable", ["template-tag", "product_id"]],
+});
 
 const TEST_USER = createMockUser();
 const TEST_DB_ID = 1;
@@ -55,6 +69,8 @@ const TEST_DATASET = createMockDataset({
   }),
 });
 
+const VISUALIZATION_TYPES = ["Table", "Number", "Gauge", "Detail", "Progress"];
+
 // Provides a button to re-run the query
 function InteractiveQuestionCustomLayout({
   title,
@@ -71,18 +87,27 @@ function InteractiveQuestionCustomLayout({
   );
 }
 
-const TEST_CARD = createMockCard({ name: "My Question" });
+const TEST_CARD = createMockCard({
+  name: "My Question",
+  parameters: [TEST_PARAM],
+});
+
 const setup = ({
   isValidCard = true,
   title,
   withCustomLayout = false,
   withChartTypeSelector = false,
-}: {
-  isValidCard?: boolean;
-  title?: SdkQuestionTitleProps;
-  withCustomLayout?: boolean;
-  withChartTypeSelector?: boolean;
-} = {}) => {
+  initialSqlParameters,
+}: Partial<
+  Pick<InteractiveQuestionProps, "initialSqlParameters"> &
+    Pick<
+      InteractiveQuestionDefaultViewProps,
+      "withChartTypeSelector" | "title"
+    > & {
+      isValidCard?: boolean;
+      withCustomLayout?: boolean;
+    }
+> = {}) => {
   const { state } = setupSdkState({
     currentUser: TEST_USER,
   });
@@ -110,6 +135,7 @@ const setup = ({
       questionId={TEST_CARD.id}
       title={title}
       withChartTypeSelector={withChartTypeSelector}
+      initialSqlParameters={initialSqlParameters}
     >
       {withCustomLayout ? <InteractiveQuestionCustomLayout /> : undefined}
     </InteractiveQuestion>,
@@ -260,6 +286,31 @@ describe("InteractiveQuestion", () => {
     ).not.toBeInTheDocument();
   });
 
+  it("should change the visualization if a different visualization is selected", async () => {
+    setup({ withChartTypeSelector: true });
+    await waitForLoaderToBeRemoved();
+    expect(
+      screen.getByTestId("chart-type-selector-button"),
+    ).toBeInTheDocument();
+
+    for (const visType of VISUALIZATION_TYPES) {
+      await userEvent.click(screen.getByTestId("chart-type-selector-button"));
+      await userEvent.click(
+        await within(screen.getByRole("menu")).findByText(visType),
+      );
+      expect(screen.queryByRole("menu")).not.toBeInTheDocument();
+      expect(
+        within(screen.getByTestId("chart-type-selector-button")).getByText(
+          visType,
+        ),
+      ).toBeInTheDocument();
+      expect(screen.getByTestId("visualization-root")).toHaveAttribute(
+        "data-viz-ui-name",
+        visType,
+      );
+    }
+  });
+
   // Obviously, we can't test every single permutation of chart settings right now, but tests in the core
   // app should cover most cases anyway.
   it("should allow user to use chart settings", async () => {
@@ -281,4 +332,22 @@ describe("InteractiveQuestion", () => {
       await screen.findByTestId("draggable-item-A New Test Column"),
     ).toBeInTheDocument();
   });
+
+  it("should query with the parameters in a parameterized question", async () => {
+    setup({ initialSqlParameters: { product_id: 1024 } });
+
+    await waitForLoaderToBeRemoved();
+
+    const lastQuery = fetchMock.lastCall(
+      `path:/api/card/${TEST_CARD.id}/query`,
+    );
+    const queryRequest = await lastQuery?.request?.json();
+
+    expect(queryRequest.parameters?.[0]).toMatchObject({
+      id: TEST_PARAM.id,
+      type: TEST_PARAM.type,
+      target: TEST_PARAM.target,
+      value: [1024],
+    });
+  });
 });
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.tsx b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.tsx
index 909b107f94fc..d8a2cb4c6c41 100644
--- a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.tsx
@@ -1,142 +1,42 @@
-import cx from "classnames";
-import { useMemo } from "react";
-import { t } from "ttag";
-
-import {
-  QuestionNotFoundError,
-  SdkError,
-  SdkLoader,
-  withPublicComponentWrapper,
-} from "embedding-sdk/components/private/PublicComponentWrapper";
-import { useLoadStaticQuestion } from "embedding-sdk/hooks/private/use-load-static-question";
-import { getDefaultVizHeight } from "embedding-sdk/lib/default-height";
-import CS from "metabase/css/core/index.css";
-import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated-entity-id";
+import type { FlexibleSizeProps } from "embedding-sdk/components/private/FlexibleSizeComponent";
 import {
-  getResponseErrorMessage,
-  isResourceNotFoundError,
-} from "metabase/lib/errors";
-import { useSelector } from "metabase/lib/redux";
-import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
-import {
-  ChartTypeSettings,
-  getSensibleVisualizations,
-  useQuestionVisualizationState,
-} from "metabase/query_builder/components/chart-type-selector";
-import { getMetadata } from "metabase/selectors/metadata";
-import { Box, Group } from "metabase/ui";
-import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
-import Question from "metabase-lib/v1/Question";
-import type { CardId, Dataset } from "metabase-types/api";
+  InteractiveQuestionProvider,
+  type InteractiveQuestionProviderProps,
+} from "embedding-sdk/components/private/InteractiveQuestion/context";
+import { withPublicComponentWrapper } from "embedding-sdk/components/private/PublicComponentWrapper";
+
+import { InteractiveQuestion } from "../InteractiveQuestion";
 
 export type StaticQuestionProps = {
-  questionId: CardId | string;
   withChartTypeSelector?: boolean;
-  height?: string | number;
-  initialSqlParameters?: Record<string, string | number>;
-};
-
-type StaticQuestionVisualizationSelectorProps = {
-  question: Question;
-  result: Dataset | null;
-  onUpdateQuestion: (question: Question) => void;
-};
-
-const StaticQuestionVisualizationSelector = ({
-  question,
-  result,
-  onUpdateQuestion,
-}: StaticQuestionVisualizationSelectorProps) => {
-  const { sensibleVisualizations, nonSensibleVisualizations } = useMemo(
-    () => getSensibleVisualizations({ result }),
-    [result],
-  );
-
-  const { selectedVisualization, updateQuestionVisualization } =
-    useQuestionVisualizationState({
-      question,
-      onUpdateQuestion,
-    });
-
-  return (
-    <Box w="355px">
-      <ChartTypeSettings
-        selectedVisualization={selectedVisualization}
-        onSelectVisualization={updateQuestionVisualization}
-        sensibleVisualizations={sensibleVisualizations}
-        nonSensibleVisualizations={nonSensibleVisualizations}
-      />
-    </Box>
-  );
-};
+} & Pick<
+  InteractiveQuestionProviderProps,
+  "questionId" | "initialSqlParameters"
+> &
+  FlexibleSizeProps;
 
 const StaticQuestionInner = ({
   questionId: initialQuestionId,
   withChartTypeSelector,
   height,
+  width,
+  className,
+  style,
   initialSqlParameters,
-}: StaticQuestionProps): JSX.Element | null => {
-  const { isLoading: isValidatingEntityId, id: questionId } =
-    useValidatedEntityId({
-      type: "card",
-      id: initialQuestionId,
-    });
-
-  const metadata = useSelector(getMetadata);
-
-  const { card, loading, result, error, updateQuestion } =
-    useLoadStaticQuestion(questionId, initialSqlParameters);
-
-  const isLoading = loading || (!result && !error) || isValidatingEntityId;
-
-  if (!questionId || isResourceNotFoundError(error)) {
-    return <QuestionNotFoundError id={initialQuestionId} />;
-  }
-
-  if (error) {
-    return (
-      <SdkError
-        message={getResponseErrorMessage(error) ?? t`Invalid question ID`}
-      />
-    );
-  }
-
-  if (isLoading) {
-    return <SdkLoader />;
-  }
-
-  const question = new Question(card, metadata);
-  const defaultHeight = card ? getDefaultVizHeight(card.display) : undefined;
-
-  return (
-    <Box
-      className={cx(CS.flexFull, CS.fullWidth)}
-      h={height ?? defaultHeight}
-      bg="var(--mb-color-bg-question)"
-    >
-      <Group h="100%" pos="relative" align="flex-start">
-        {withChartTypeSelector && (
-          <StaticQuestionVisualizationSelector
-            question={question}
-            result={result}
-            onUpdateQuestion={updateQuestion}
-          />
-        )}
-        <QueryVisualization
-          className={cx(CS.flexFull, CS.fullWidth, CS.fullHeight)}
-          question={question}
-          rawSeries={[{ card, data: result?.data }]}
-          isRunning={isLoading}
-          isObjectDetail={false}
-          isResultDirty={false}
-          isNativeEditorOpen={false}
-          result={result}
-          noHeader
-          mode={PublicMode}
-        />
-      </Group>
-    </Box>
-  );
-};
+}: StaticQuestionProps): JSX.Element | null => (
+  <InteractiveQuestionProvider
+    questionId={initialQuestionId}
+    variant="static"
+    initialSqlParameters={initialSqlParameters}
+  >
+    {withChartTypeSelector && <InteractiveQuestion.ChartTypeDropdown />}
+    <InteractiveQuestion.QuestionVisualization
+      height={height}
+      width={width}
+      className={className}
+      style={style}
+    />
+  </InteractiveQuestionProvider>
+);
 
 export const StaticQuestion = withPublicComponentWrapper(StaticQuestionInner);
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
deleted file mode 100644
index 187a145737e6..000000000000
--- a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import userEvent from "@testing-library/user-event";
-import fetchMock from "fetch-mock";
-
-import {
-  setupCardEndpoints,
-  setupCardQueryEndpoints,
-  setupUnauthorizedCardEndpoints,
-} from "__support__/server-mocks";
-import {
-  act,
-  mockGetBoundingClientRect,
-  screen,
-  waitForLoaderToBeRemoved,
-  within,
-} from "__support__/ui";
-import { renderWithSDKProviders } from "embedding-sdk/test/__support__/ui";
-import { createMockAuthProviderUriConfig } from "embedding-sdk/test/mocks/config";
-import type { Card } from "metabase-types/api";
-import {
-  createMockCard,
-  createMockColumn,
-  createMockDataset,
-  createMockDatasetData,
-  createMockParameter,
-} from "metabase-types/api/mocks";
-
-import type { StaticQuestionProps } from "./";
-import { StaticQuestion } from "./";
-
-const TEST_QUESTION_ID = 1;
-const TEST_COLUMN = createMockColumn({
-  display_name: "Test Column",
-  name: "Test Column",
-});
-const TEST_DATASET = createMockDataset({
-  data: createMockDatasetData({
-    cols: [TEST_COLUMN],
-    rows: [["Test Row"]],
-  }),
-});
-const TEST_PARAM = createMockParameter({
-  type: "number/=",
-  slug: "product_id",
-  target: ["variable", ["template-tag", "product_id"]],
-});
-
-const VISUALIZATION_TYPES: Record<
-  string,
-  {
-    container: string;
-    button: string;
-  }
-> = {
-  Table: { container: "Table-container", button: "Table-button" },
-  Number: {
-    container: "Number-container",
-    button: "Number-button",
-  },
-  Gauge: {
-    container: "Gauge-container",
-    button: "Gauge-button",
-  },
-  Detail: {
-    container: "Detail-container",
-    button: "Detail-button",
-  },
-  Progress: {
-    container: "Progress-container",
-    button: "Progress-button",
-  },
-};
-
-const setup = ({
-  withChartTypeSelector = false,
-  isValidCard = true,
-  card = createMockCard(),
-  initialSqlParameters,
-}: Partial<StaticQuestionProps> & {
-  card?: Card;
-  isValidCard?: boolean;
-} = {}) => {
-  if (isValidCard) {
-    setupCardEndpoints(card);
-  } else {
-    setupUnauthorizedCardEndpoints(card);
-  }
-
-  setupCardQueryEndpoints(card, TEST_DATASET);
-
-  return renderWithSDKProviders(
-    <StaticQuestion
-      questionId={TEST_QUESTION_ID}
-      withChartTypeSelector={withChartTypeSelector}
-      initialSqlParameters={initialSqlParameters}
-    />,
-    {
-      sdkProviderProps: {
-        authConfig: createMockAuthProviderUriConfig({
-          authProviderUri: "http://TEST_URI/sso/metabase",
-        }),
-      },
-    },
-  );
-};
-
-describe("StaticQuestion", () => {
-  it("should render a loader on initialization", () => {
-    setup();
-    expect(screen.getByTestId("loading-indicator")).toBeInTheDocument();
-  });
-
-  describe("table visualization", () => {
-    beforeAll(() => {
-      mockGetBoundingClientRect();
-    });
-
-    beforeEach(() => {
-      jest.useFakeTimers();
-    });
-
-    afterEach(() => {
-      jest.useRealTimers();
-    });
-
-    it("should render question if question is valid", async () => {
-      setup();
-
-      await waitForLoaderToBeRemoved();
-
-      act(() => {
-        jest.runAllTimers();
-      });
-
-      expect(
-        within(screen.getByTestId("header-cell")).getByText(TEST_COLUMN.name),
-      ).toBeInTheDocument();
-      expect(
-        within(screen.getByRole("gridcell")).getByText("Test Row"),
-      ).toBeInTheDocument();
-    });
-  });
-
-  it("should render an error if a question isn't found", async () => {
-    setup({ isValidCard: false });
-    await waitForLoaderToBeRemoved();
-    expect(
-      screen.getByText("You don't have permissions to do that."),
-    ).toBeInTheDocument();
-  });
-
-  it("should render a visualization selector if withChartTypeSelector is true", async () => {
-    setup({ withChartTypeSelector: true });
-    await waitForLoaderToBeRemoved();
-    expect(screen.getByTestId("chart-type-settings")).toBeInTheDocument();
-  });
-
-  it("should not render a visualization selector if withChartTypeSelector is false", async () => {
-    setup();
-    await waitForLoaderToBeRemoved();
-    expect(screen.queryByTestId("chart-type-settings")).not.toBeInTheDocument();
-  });
-
-  it("should change the visualization if a different visualization is selected", async () => {
-    setup({ withChartTypeSelector: true });
-    await waitForLoaderToBeRemoved();
-    expect(screen.getByTestId("chart-type-settings")).toBeInTheDocument();
-
-    for (const visType of Object.keys(VISUALIZATION_TYPES)) {
-      await userEvent.click(
-        screen.getByTestId(VISUALIZATION_TYPES[visType].button),
-      );
-
-      expect(
-        screen.getByTestId(VISUALIZATION_TYPES[visType].container),
-      ).toHaveAttribute("aria-selected", "true");
-    }
-  });
-
-  it("should query with the parameters in a parameterized question", async () => {
-    const card = createMockCard({ parameters: [TEST_PARAM] });
-    setup({ card, initialSqlParameters: { product_id: 1024 } });
-
-    await waitForLoaderToBeRemoved();
-
-    const lastQuery = fetchMock.lastCall(`path:/api/card/${card.id}/query`);
-    const queryRequest = await lastQuery?.request?.json();
-
-    expect(queryRequest.parameters?.[0]).toMatchObject({
-      id: TEST_PARAM.id,
-      type: TEST_PARAM.type,
-      target: TEST_PARAM.target,
-      value: 1024,
-    });
-  });
-
-  it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
-
-    const { unmount } = setup();
-    await act(async () => unmount());
-
-    // two requests should've been made initially
-    expect(fetchMock.calls(`path:/api/card/1`).length).toBe(1);
-    expect(fetchMock.calls(`path:/api/card/1/query`).length).toBe(1);
-
-    // consequently, two abort calls should've been made for the two requests
-    expect(abortSpy).toHaveBeenCalledTimes(2);
-    abortSpy.mockRestore();
-  });
-});
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/mode.ts b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/mode.ts
new file mode 100644
index 000000000000..6fb3b7a6ab23
--- /dev/null
+++ b/enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/mode.ts
@@ -0,0 +1,7 @@
+import type { QueryClickActionsMode } from "metabase/visualizations/types";
+
+export const StaticQuestionSdkMode: QueryClickActionsMode = {
+  name: "static-question-sdk",
+  hasDrills: false,
+  clickActions: [],
+};
diff --git a/enterprise/frontend/src/embedding-sdk/hooks/private/use-load-static-question.ts b/enterprise/frontend/src/embedding-sdk/hooks/private/use-load-static-question.ts
deleted file mode 100644
index 174c4b7d0d48..000000000000
--- a/enterprise/frontend/src/embedding-sdk/hooks/private/use-load-static-question.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { loadStaticQuestion } from "embedding-sdk/lib/load-static-question";
-import type { GenericErrorResponse } from "metabase/lib/errors";
-import { defer } from "metabase/lib/promise";
-import type Question from "metabase-lib/v1/Question";
-import type { Card, Dataset } from "metabase-types/api";
-
-type QuestionState = {
-  loading: boolean;
-  card: Card | null;
-  result: Dataset | null;
-  error: GenericErrorResponse | null;
-};
-
-export function useLoadStaticQuestion(
-  questionId: number | null,
-  initialSqlParameters?: Record<string, string | number>,
-) {
-  const [questionState, setQuestionState] = useState<QuestionState>({
-    loading: false,
-    card: null,
-    result: null,
-    error: null,
-  });
-
-  const updateQuestion = (newQuestion: Question) =>
-    setQuestionState(state => ({
-      ...state,
-      card: newQuestion.card(),
-      loading: false,
-      error: null,
-    }));
-
-  useEffect(() => {
-    const cancelDeferred = defer();
-    let ignore = false; // flag to ignore the result if the component unmounts: https://react.dev/learn/you-might-not-need-an-effect#fetching-data
-
-    async function loadCardData() {
-      setQuestionState(state => ({ ...state, loading: true }));
-
-      if (!questionId) {
-        return;
-      }
-
-      try {
-        const { card, result } = await loadStaticQuestion({
-          questionId,
-          sqlParameters: initialSqlParameters,
-          cancelDeferred,
-        });
-
-        if (!ignore) {
-          setQuestionState({
-            card,
-            result,
-            loading: false,
-            error: null,
-          });
-        }
-      } catch (error) {
-        if (typeof error === "object") {
-          if (!ignore) {
-            setQuestionState({
-              result: null,
-              card: null,
-              loading: false,
-              error,
-            });
-          }
-        } else {
-          console.error("error loading static question", error);
-        }
-      }
-    }
-
-    loadCardData();
-
-    return () => {
-      // cancel pending requests upon unmount
-      cancelDeferred.resolve();
-      ignore = true;
-    };
-  }, [questionId, initialSqlParameters]);
-
-  return { ...questionState, updateQuestion };
-}
diff --git a/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx b/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx
index 990d274601e6..ee1242772ea1 100644
--- a/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx
+++ b/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx
@@ -4,6 +4,7 @@ import fetchMock from "fetch-mock";
 import {
   setupCardEndpoints,
   setupCardQueryEndpoints,
+  setupCardQueryMetadataEndpoint,
   setupCurrentUserEndpoint,
   setupPropertiesEndpoints,
 } from "__support__/server-mocks";
@@ -17,6 +18,7 @@ import {
 import type { MetabaseAuthConfig } from "embedding-sdk/types";
 import {
   createMockCard,
+  createMockCardQueryMetadata,
   createMockSettings,
   createMockTokenFeatures,
   createMockUser,
@@ -74,6 +76,7 @@ describe("SDK auth and init flow", () => {
 
     setupCardEndpoints(MOCK_CARD);
     setupCardQueryEndpoints(MOCK_CARD, {} as any);
+    setupCardQueryMetadataEndpoint(MOCK_CARD, createMockCardQueryMetadata());
   });
 
   it("should initialize the auth flow only once, not on rerenders", async () => {
@@ -97,6 +100,7 @@ describe("SDK auth and init flow", () => {
     expect(fetchMock.calls(AUTH_PROVIDER_URL)).toHaveLength(1);
 
     expect(screen.queryByText("Initializing...")).not.toBeInTheDocument();
+
     expect(
       // this is just something we know it's on the screen when everything is ok
       screen.getByTestId("query-visualization-root"),
diff --git a/frontend/src/metabase/visualizations/components/Visualization/Visualization.tsx b/frontend/src/metabase/visualizations/components/Visualization/Visualization.tsx
index f03351d43c94..1af8532e8990 100644
--- a/frontend/src/metabase/visualizations/components/Visualization/Visualization.tsx
+++ b/frontend/src/metabase/visualizations/components/Visualization/Visualization.tsx
@@ -698,6 +698,7 @@ class Visualization extends PureComponent<
           className={className}
           style={style}
           data-testid="visualization-root"
+          data-viz-ui-name={visualization?.uiName}
           ref={this.props.forwardedRef}
         >
           {!!hasHeader && (


Reviews:
user: login: heypoom
body: Awesome work! That's one less code path to worry about 😄 

I tested on Storybook and Shoppy and I didn't notice any visual artifacts or layout shifts. Looks the same as before.

![CleanShot 2568-03-06 at 19 14 40@2x](https://github.com/user-attachments/assets/47dd0a82-2fc6-4c78-b523-b359ea1a5614)

![CleanShot 2568-03-06 at 19 14 46@2x](https://github.com/user-attachments/assets/7b1846af-35b2-43fc-8509-a13b19bb4690)

state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T12:11:53Z
user: login: oisincoveney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T15:56:58Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:15:46Z
user: login: oisincoveney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T17:44:53Z
user: login: oisincoveney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T17:54:20Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T18:03:44Z
user: login: oisincoveney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T18:14:20Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T18:21:39Z
user: login: npretto
body: I tested this locally and everything looked the same when comparing the old and new static question side by side, the only difference was in the default height 🚀🚀🚀🚀

I'll finish the review when the tests are green
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T14:01:19Z

Review comments:
diff_hunk: @@ -28,6 +28,7 @@ export const QuestionVisualization = ({
     navigateToNewCard,
     onNavigateBack,
     updateQuestion,
+    variant,
path: enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
user: login: heypoom
body: `variant` sounds a bit generic to me, especially as we only have two variants today. I think we can name it `isStaticQuestion` to be a bit more clear.
created_at: 2025-03-06T12:05:28Z
author_association: MEMBER
diff_hunk: @@ -9,6 +10,7 @@ import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated
 import { useCreateQuestion } from "metabase/query_builder/containers/use-create-question";
 import { useSaveQuestion } from "metabase/query_builder/containers/use-save-question";
 import { getEmbeddingMode } from "metabase/visualizations/click-actions/lib/modes";
+import { EmbeddingSdkMode } from "metabase/visualizations/click-actions/modes/EmbeddingSdkMode";
path: enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx
user: login: heypoom
body: nit: we could rename this to `InteractiveEmbeddingSdkMode` too to be consistent
created_at: 2025-03-06T12:06:15Z
author_association: MEMBER
diff_hunk: @@ -165,22 +156,30 @@ describe("StaticQuestion", () => {
       id: TEST_PARAM.id,
       type: TEST_PARAM.type,
       target: TEST_PARAM.target,
-      value: 1024,
+      value: [1024],
     });
   });
 
   it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
+    const mockResolve = jest.fn();
+    const mockReject = jest.fn();
+
+    // Mock the defer module
+    jest.mock("metabase/lib/promise", () => ({
+      defer: jest.fn().mockReturnValue({
+        promise: Promise.resolve(),
+        resolve: mockResolve,
+        reject: mockReject,
+      }),
+    }));
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: heypoom
body: I wonder why this was updated? Using `defer` to mock is fine by me, just curious whether spying on the AbortController no longer works.
created_at: 2025-03-06T12:11:22Z
author_association: MEMBER
diff_hunk: @@ -119,36 +98,48 @@ describe("StaticQuestion", () => {
   it("should render an error if a question isn't found", async () => {
     setup({ isValidCard: false });
     await waitForLoaderToBeRemoved();
-    expect(
-      screen.getByText("You don't have permissions to do that."),
-    ).toBeInTheDocument();
+    expect(screen.getByText("Question not found")).toBeInTheDocument();
+    expect(screen.getByLabelText("warning icon")).toBeInTheDocument();
   });
 
   it("should render a visualization selector if withChartTypeSelector is true", async () => {
     setup({ withChartTypeSelector: true });
     await waitForLoaderToBeRemoved();
-    expect(screen.getByTestId("chart-type-settings")).toBeInTheDocument();
+    expect(
+      screen.getByTestId("chart-type-selector-button"),
+    ).toBeInTheDocument();
   });
 
   it("should not render a visualization selector if withChartTypeSelector is false", async () => {
     setup();
     await waitForLoaderToBeRemoved();
-    expect(screen.queryByTestId("chart-type-settings")).not.toBeInTheDocument();
+    expect(
+      screen.queryByTestId("chart-type-selector-button"),
+    ).not.toBeInTheDocument();
   });
 
   it("should change the visualization if a different visualization is selected", async () => {
     setup({ withChartTypeSelector: true });
     await waitForLoaderToBeRemoved();
-    expect(screen.getByTestId("chart-type-settings")).toBeInTheDocument();
+    expect(
+      screen.getByTestId("chart-type-selector-button"),
+    ).toBeInTheDocument();
 
-    for (const visType of Object.keys(VISUALIZATION_TYPES)) {
+    for (const visType of VISUALIZATION_TYPES) {
+      await userEvent.click(screen.getByTestId("chart-type-selector-button"));
       await userEvent.click(
-        screen.getByTestId(VISUALIZATION_TYPES[visType].button),
+        await within(screen.getByRole("menu")).findByText(visType),
       );
-
+      expect(screen.queryByRole("menu")).not.toBeInTheDocument();
       expect(
-        screen.getByTestId(VISUALIZATION_TYPES[visType].container),
-      ).toHaveAttribute("aria-selected", "true");
+        within(screen.getByTestId("chart-type-selector-button")).getByText(
+          visType,
+        ),
+      ).toBeInTheDocument();
+      expect(screen.getByTestId("visualization-root")).toHaveAttribute(
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: heypoom
body: Nice test simplification!
created_at: 2025-03-06T12:11:38Z
author_association: MEMBER
diff_hunk: @@ -165,22 +156,30 @@ describe("StaticQuestion", () => {
       id: TEST_PARAM.id,
       type: TEST_PARAM.type,
       target: TEST_PARAM.target,
-      value: 1024,
+      value: [1024],
     });
   });
 
   it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
+    const mockResolve = jest.fn();
+    const mockReject = jest.fn();
+
+    // Mock the defer module
+    jest.mock("metabase/lib/promise", () => ({
+      defer: jest.fn().mockReturnValue({
+        promise: Promise.resolve(),
+        resolve: mockResolve,
+        reject: mockReject,
+      }),
+    }));
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: oisincoveney
body: It doesn't work. I'm trying to get the tests to get through, but with this experimental code it still fails. I know that instead of an AbortController in static question, we pass in the `defer` object from `useQuestionQuerySdk`, which causes this test to break. 

@heypoom do you have any ideas on how we can get this to pass? I think it's one of the few blockers left
created_at: 2025-03-06T15:56:58Z
author_association: NONE
diff_hunk: @@ -165,22 +156,30 @@ describe("StaticQuestion", () => {
       id: TEST_PARAM.id,
       type: TEST_PARAM.type,
       target: TEST_PARAM.target,
-      value: 1024,
+      value: [1024],
     });
   });
 
   it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
+    const mockResolve = jest.fn();
+    const mockReject = jest.fn();
+
+    // Mock the defer module
+    jest.mock("metabase/lib/promise", () => ({
+      defer: jest.fn().mockReturnValue({
+        promise: Promise.resolve(),
+        resolve: mockResolve,
+        reject: mockReject,
+      }),
+    }));
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: heypoom
body: @oisincoveney Hmm, since the normal InteractiveQuestion does not have the request cancellation logic that StaticQuestion has, we can either try to replicate this "cancel requests on unmount logic" or get rid of the test for now:

https://github.com/metabase/metabase/blob/2a9d1e14573988a7dbf484e18b6ec8435904f423/enterprise/frontend/src/embedding-sdk/hooks/private/use-load-static-question.ts#L79-L84

If we want to enable request cancellation, we need to go in the InteractiveQuestionProvider's `useEffect` and resolve the "cancel deferred" upon unmount

https://github.com/metabase/metabase/blob/2a9d1e14573988a7dbf484e18b6ec8435904f423/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/context/InteractiveQuestionProvider.tsx#L164-L167
created_at: 2025-03-06T16:15:46Z
author_association: MEMBER
diff_hunk: @@ -28,6 +28,7 @@ export const QuestionVisualization = ({
     navigateToNewCard,
     onNavigateBack,
     updateQuestion,
+    variant,
path: enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
user: login: oisincoveney
body: Maybe it's irrational but I just don't like having a boolean specifically for things like this - basically, in this case, it's implicit that the question is `interactive` when we use `isStaticQuestion`, and my first question is "what's the opposite of this?" and it's not immediately clear. Let me know what you think

I also feel like @npretto would have an opinion on this 😉
created_at: 2025-03-06T17:44:53Z
author_association: NONE
diff_hunk: @@ -165,22 +156,30 @@ describe("StaticQuestion", () => {
       id: TEST_PARAM.id,
       type: TEST_PARAM.type,
       target: TEST_PARAM.target,
-      value: 1024,
+      value: [1024],
     });
   });
 
   it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
+    const mockResolve = jest.fn();
+    const mockReject = jest.fn();
+
+    // Mock the defer module
+    jest.mock("metabase/lib/promise", () => ({
+      defer: jest.fn().mockReturnValue({
+        promise: Promise.resolve(),
+        resolve: mockResolve,
+        reject: mockReject,
+      }),
+    }));
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: oisincoveney
body: I think it makes sense to remove this code for now and test it directly within interactive question (maybe we don't even need these tests and we move them into the appropriate files for interactive question components). I'll do that and add the tests in a different place
created_at: 2025-03-06T17:54:20Z
author_association: NONE
diff_hunk: @@ -28,6 +28,7 @@ export const QuestionVisualization = ({
     navigateToNewCard,
     onNavigateBack,
     updateQuestion,
+    variant,
path: enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
user: login: npretto
body: I'm about to EOD and I haven't reviewed the PR _yet_ , I will do a proper review tomorrow.
But from a quick look: I think we may want to just have a prop to enable/disable drills in this case.
I'm not a super fan of passing "static" around, static feels like a combination of properties, not just one. What if we want to disable drills but keep the visualization selector for example? 
created_at: 2025-03-06T18:03:44Z
author_association: MEMBER
diff_hunk: @@ -28,6 +28,7 @@ export const QuestionVisualization = ({
     navigateToNewCard,
     onNavigateBack,
     updateQuestion,
+    variant,
path: enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestion/components/Visualization.tsx
user: login: oisincoveney
body: Idk if that's a good example. We can use the visualization selector while disabling drills - the selector isn't tied to the `static` mode. The only thing that happens when `variant=static` is that we disable click actions and drills 

As I explain this it's fairly obvious that we'll need to rename the variable because it's not very good at describing what it's doing to the component. Maybe a `disableDrills` or something would work, but I want to keep the API as simple as possible
created_at: 2025-03-06T18:14:20Z
author_association: NONE
diff_hunk: @@ -165,22 +156,30 @@ describe("StaticQuestion", () => {
       id: TEST_PARAM.id,
       type: TEST_PARAM.type,
       target: TEST_PARAM.target,
-      value: 1024,
+      value: [1024],
     });
   });
 
   it("should cancel the request when the component unmounts", async () => {
-    const abortSpy = jest.spyOn(AbortController.prototype, "abort");
+    const mockResolve = jest.fn();
+    const mockReject = jest.fn();
+
+    // Mock the defer module
+    jest.mock("metabase/lib/promise", () => ({
+      defer: jest.fn().mockReturnValue({
+        promise: Promise.resolve(),
+        resolve: mockResolve,
+        reject: mockReject,
+      }),
+    }));
path: enterprise/frontend/src/embedding-sdk/components/public/StaticQuestion/StaticQuestion.unit.spec.tsx
user: login: heypoom
body: Sounds good, you can skip the tests here first and add it in later.
created_at: 2025-03-06T18:21:39Z
author_association: MEMBER

PR comments:
user: login: oisincoveney
created_at: 2025-03-05T01:38:53Z
body: This change is part of the following stack:

- #54700 ◀

<sub>Change managed by [git-spice](https://abhinav.github.io/git-spice/).</sub>
<!-- gs:navigation comment -->

author_association: NONE
user: login: trunk-io[bot]
created_at: 2025-03-05T02:24:02Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/14007_tests-29a86ad1-purple)](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/3-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=QUARANTINED)


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > visualizations > line/bar chart > tooltips should be enterable and scollable to view all rows in long tooltips (metabase\#53586) (metab...</code> | The test timed out while trying to find an element with data-testid "echarts-tooltip". | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=FAILURE&runId=14ef7fe4-6b79-556f-a1e2-e2325ee4b9ce) |
| <code>issue 39487 calendar has constant size when using date range picker filter (metabase\#39487)</code> | The test timed out while trying to find an element with specific text, possibly due to the text being broken up by multiple elements. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=FAILURE&runId=b474be8f-63e4-51b4-bf65-bc3470671f93) |
| <code>issue 39487 calendar has constant size when using single date picker filter (metabase\#39487)</code> | The test timed out while trying to find an element with specific text, possibly due to the text being broken up by multiple elements. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment&runConclusion=FAILURE&runId=22696da9-f337-5fe2-a875-91c0afc9a23e) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54700?repo=metabase/metabase&commitHash=29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: heypoom
created_at: 2025-03-06T12:03:17Z
body: @oisincoveney Could you link a Linear ticket to the PR if you have one? Thanks!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-14T13:52:30Z
body: 
  ## e2e tests failed on `29a86ad1898ce8e8f35e2a1ee25733ac94d2ab00-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/13857764483?pr=54700)

  File | Test Name
  ---- | ---------
  `reproductions.cy.spec.ts` | (_flaky_) issue 39487 > calendar has constant size when using single date picker filter (metabase#39487)
`reproductions.cy.spec.ts` | (_flaky_) issue 39487 > calendar has constant size when using date range picker filter (metabase#39487)
`line-bar-tooltips.cy.spec.js` | (_flaky_) scenarios > visualizations > line/bar chart > tooltips > should be enterable and scollable to view all rows in long tooltips (metabase#53586) (metabase#48347)

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-14T14:06:57Z
body: @oisincoveney Manual conflict resolution is required on #55169
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-14T14:07:06Z
body: @oisincoveney Manual conflict resolution is required on #55170
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T12:34:15Z
body: 🚀 This should also be released by [v0.53.8](https://github.com/metabase/metabase/milestone/319)
author_association: CONTRIBUTOR