Repository: metabase/metabase
Pull request #54845: Control visible entities in multi-stage data picker
Author: login: WiNloSt
Labels: name: no-backport
description: Do not backport this PR to any branch
name: .Team/Embedding
Created: 2025-03-07T14:29:08Z | Merged: 2025-03-18T12:49:50Z

Closes EMB-228

### Description

Control the visibility of Tables/models in the multi-stage (legacy) data picker

### How to verify

I wouldn't recommend testing this for real, as you need an instance with more than 99 of combined tables and models. You can [see this PR](https://github.com/metabase/metabase/pull/54489) on how to test the multi-stage data picker for real if you're still curious.

1. Change this line locally to `if (false) {`
       https://github.com/metabase/metabase/blob/cad3316cd19cf3b554470823893a8a4776a7d154/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx#L72
1. Embed this in interactive embedding.
1. You can now try different versions of the `entity_types` parameter. You can see the expectations from [the tests](https://github.com/metabase/metabase/blob/cad3316cd19cf3b554470823893a8a4776a7d154/frontend/src/metabase/redux/embed/embed.unit.spec.ts#L44-L159).
    - `entity_types=table`
    - `entity_types=table&entity_types=model`
    - `entity_types=table,model` / `entity_types= table , model `
    - `entity_types=invalid`
    - `entity_types=`
    - or not passing the entity types at all.
1. Other data selectors should behave the same e.g. database selector in SQL query editor
1. The SDK still works as expected. We won't be able to set the desired `entityTypes` yet, it will be completed in EMB-231
### Demo

#### Tables and Models
![image](https://github.com/user-attachments/assets/97bc607c-823b-40dd-83db-72f0b93a64a8)

#### Only tables (shows the database step instantly)
![image](https://github.com/user-attachments/assets/5aebb4c6-2cd7-4f83-9b4e-c7b4d8fbd596)

#### Only models (still shows the data bucket step as a limitation)
![image](https://github.com/user-attachments/assets/246fe4b8-25b5-4e56-add8-c1b6a808f484)


### Checklist

- [x] Tests have been added/updated to cover changes in this PR



Diff:
diff --git a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
index 3fadc3d15a1f..d4d15eb3fd47 100644
--- a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
+++ b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
@@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
+     * @param {boolean} [option.isMultiStageDataPicker]
+     * @param {import("metabase-types/store").InteractiveEmbeddingOptions} [option.searchParameters]
+     */
     function startNewEmbeddingQuestion({
       isMultiStageDataPicker = false,
+      searchParameters,
     } = {}) {
       if (isMultiStageDataPicker) {
         cy.intercept("GET", "/api/search*", req => {
@@ -789,6 +796,7 @@ describe("scenarios > embedding > full app", () => {
         url: "/",
         qs: {
           new_button: true,
+          ...searchParameters,
         },
       });
       cy.button("New").click();
@@ -1238,6 +1246,45 @@ describe("scenarios > embedding > full app", () => {
         });
       });
     });
+
+    describe('"entity_types" query parameter', () => {
+      it('should show only the provided "entity_types"', () => {
+        startNewEmbeddingQuestion({
+          isMultiStageDataPicker: true,
+          searchParameters: {
+            entity_types: "table",
+          },
+        });
+        H.popover().within(() => {
+          cy.findByText("Models").should("not.exist");
+          cy.findByText("Sample Database").should("be.visible");
+          cy.findByRole("option", { name: "Orders" }).should("be.visible");
+        });
+      });
+
+      it('should show models and tables as a default value when not providing "entity_types"', () => {
+        cy.log("Test providing `entity_types` as an empty string");
+        startNewEmbeddingQuestion({
+          isMultiStageDataPicker: true,
+          searchParameters: {
+            entity_types: "",
+          },
+        });
+        H.popover().within(() => {
+          cy.findByText("Models").should("be.visible");
+          cy.findByText("Raw Data").should("be.visible");
+        });
+
+        cy.log("Test not providing `entity_types`");
+        startNewEmbeddingQuestion({
+          isMultiStageDataPicker: true,
+        });
+        H.popover().within(() => {
+          cy.findByText("Models").should("be.visible");
+          cy.findByText("Raw Data").should("be.visible");
+        });
+      });
+    });
   });
 
   describe("dashboards", () => {
diff --git a/enterprise/frontend/src/embedding-sdk/store/index.ts b/enterprise/frontend/src/embedding-sdk/store/index.ts
index cabf7521ad41..ed996523ce6b 100644
--- a/enterprise/frontend/src/embedding-sdk/store/index.ts
+++ b/enterprise/frontend/src/embedding-sdk/store/index.ts
@@ -2,6 +2,7 @@
 import type { AnyAction, Reducer, Store } from "@reduxjs/toolkit";
 import { useContext } from "react";
 
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
 import {
   MetabaseReduxContext,
   useDispatch,
@@ -21,7 +22,9 @@ export const sdkReducers = {
 export const getSdkStore = () =>
   getStore(sdkReducers, null, {
     embed: {
-      options: {},
+      options: {
+        entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+      },
       isEmbeddingSdk: true,
     },
     app: {
diff --git a/frontend/src/metabase-types/store/embed.ts b/frontend/src/metabase-types/store/embed.ts
index 314ccf418d59..0f7242bf91bf 100644
--- a/frontend/src/metabase-types/store/embed.ts
+++ b/frontend/src/metabase-types/store/embed.ts
@@ -1,17 +1,21 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
+  top_nav: boolean;
+  search: boolean;
+  new_button: boolean;
+  breadcrumbs: boolean;
+  logo: boolean;
+  side_nav: boolean | "default";
+  header: boolean;
+  additional_info: boolean;
+  action_buttons: boolean;
+  entity_types: EmbeddingEntityType[];
 }
 
+type EmptyObject = Record<string, never>;
 export interface EmbedState {
-  options: InteractiveEmbeddingOptions;
+  options: InteractiveEmbeddingOptions | EmptyObject;
   isEmbeddingSdk?: boolean;
 }
diff --git a/frontend/src/metabase-types/store/mocks/embed.ts b/frontend/src/metabase-types/store/mocks/embed.ts
index d712433c2b9b..040c928cea67 100644
--- a/frontend/src/metabase-types/store/mocks/embed.ts
+++ b/frontend/src/metabase-types/store/mocks/embed.ts
@@ -1,3 +1,4 @@
+import { DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS } from "metabase/redux/embed";
 import type {
   EmbedState,
   InteractiveEmbeddingOptions,
@@ -6,6 +7,7 @@ import type {
 export const createMockEmbedOptions = (
   opts?: Partial<InteractiveEmbeddingOptions>,
 ) => ({
+  ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
   ...opts,
 });
 
diff --git a/frontend/src/metabase/embedding-sdk/store.ts b/frontend/src/metabase/embedding-sdk/store.ts
new file mode 100644
index 000000000000..5e788ea011cd
--- /dev/null
+++ b/frontend/src/metabase/embedding-sdk/store.ts
@@ -0,0 +1,6 @@
+export const DEFAULT_EMBEDDING_ENTITY_TYPES: EmbeddingEntityType[] = [
+  "model",
+  "table",
+];
+
+export type EmbeddingEntityType = "model" | "table";
diff --git a/frontend/src/metabase/nav/containers/AppBar/AppBar.unit.spec.tsx b/frontend/src/metabase/nav/containers/AppBar/AppBar.unit.spec.tsx
index 3679cd41597c..6d97817a2004 100644
--- a/frontend/src/metabase/nav/containers/AppBar/AppBar.unit.spec.tsx
+++ b/frontend/src/metabase/nav/containers/AppBar/AppBar.unit.spec.tsx
@@ -9,7 +9,7 @@ import {
   setupDashboardEndpoints,
 } from "__support__/server-mocks";
 import { renderWithProviders } from "__support__/ui";
-import { DEFAULT_EMBED_OPTIONS } from "metabase/redux/embed";
+import { DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS } from "metabase/redux/embed";
 import type { Card } from "metabase-types/api";
 import {
   createMockCard,
@@ -291,7 +291,7 @@ function setup({
       app: createMockAppState({ isNavbarOpen: false }),
       embed: createMockEmbedState({
         options: createMockEmbedOptions({
-          ...DEFAULT_EMBED_OPTIONS,
+          ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
           ...embedOptions,
         }),
       }),
diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
index 109c1281b6ef..f1f6b60f124d 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
@@ -164,6 +164,8 @@ export class UnconnectedDataSelector extends Component {
     hasTableSearch: PropTypes.bool,
     canChangeDatabase: PropTypes.bool,
     containerClassName: PropTypes.string,
+    canSelectModel: PropTypes.bool,
+    canSelectTable: PropTypes.bool,
     canSelectMetric: PropTypes.bool,
     canSelectSavedQuestion: PropTypes.bool,
 
@@ -197,6 +199,8 @@ export class UnconnectedDataSelector extends Component {
     hasTriggerExpandControl: true,
     isPopover: true,
     isMantine: false,
+    canSelectModel: true,
+    canSelectTable: true,
     canSelectMetric: false,
     canSelectSavedQuestion: true,
   };
@@ -456,8 +460,8 @@ export class UnconnectedDataSelector extends Component {
   }
 
   hasModels = () => {
-    const { models, loaded } = this.props;
-    return loaded && models && models.length > 0;
+    const { models, loaded, canSelectModel } = this.props;
+    return loaded && models && models.length > 0 && canSelectModel;
   };
 
   hasUsableModels = () => {
@@ -494,7 +498,9 @@ export class UnconnectedDataSelector extends Component {
     // "Saved Questions" are presented in a different picker step
     // So it should be excluded from a regular databases list
     const shouldRemoveSavedQuestionDatabaseFromList =
-      !this.props.hasNestedQueriesEnabled || this.hasUsableModelsOrMetrics();
+      !this.props.hasNestedQueriesEnabled ||
+      this.hasUsableModelsOrMetrics() ||
+      !this.props.canSelectSavedQuestion;
 
     return shouldRemoveSavedQuestionDatabaseFromList
       ? databases.filter(db => !db.is_saved_questions)
@@ -906,6 +912,7 @@ export class UnconnectedDataSelector extends Component {
             <DataBucketPicker
               dataTypes={getDataTypes({
                 hasModels: this.hasModels(),
+                hasTables: this.props.canSelectTable,
                 hasNestedQueriesEnabled,
                 hasSavedQuestions: this.hasSavedQuestions(),
                 hasMetrics: this.hasMetrics(),
diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx
index 93b5bafe417c..cb2bde3c1f59 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx
@@ -17,8 +17,9 @@ const setup = (dataTypes: DataTypeInfoItem[]) => {
 describe("DataSelectorDataBucketPicker", () => {
   it("should display all buckets", () => {
     const dataTypes = getDataTypes({
-      hasMetrics: false,
       hasModels: true,
+      hasTables: true,
+      hasMetrics: false,
       hasSavedQuestions: true,
       hasNestedQueriesEnabled: true,
     });
diff --git a/frontend/src/metabase/query_builder/components/DataSelector/utils.ts b/frontend/src/metabase/query_builder/components/DataSelector/utils.ts
index 2ce8fa01a8f4..b9fec0d536dd 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector/utils.ts
+++ b/frontend/src/metabase/query_builder/components/DataSelector/utils.ts
@@ -8,10 +8,12 @@ import type { DataTypeInfoItem } from "./types";
 
 export function getDataTypes({
   hasModels,
+  hasTables,
   hasNestedQueriesEnabled,
   hasSavedQuestions,
   hasMetrics,
 }: {
+  hasTables: boolean;
   hasModels: boolean;
   hasNestedQueriesEnabled: boolean;
   hasSavedQuestions: boolean;
@@ -23,7 +25,9 @@ export function getDataTypes({
     dataTypes.push(MODELS_INFO_ITEM);
   }
 
-  dataTypes.push(RAW_DATA_INFO_ITEM);
+  if (hasTables) {
+    dataTypes.push(RAW_DATA_INFO_ITEM);
+  }
 
   if (hasNestedQueriesEnabled && hasSavedQuestions) {
     dataTypes.push(SAVED_QUESTIONS_INFO_ITEM);
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/DataPickerTarget.tsx b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/DataPickerTarget.tsx
new file mode 100644
index 000000000000..b97b3c084ec3
--- /dev/null
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/DataPickerTarget.tsx
@@ -0,0 +1,60 @@
+import { type MouseEvent, type Ref, forwardRef } from "react";
+
+import type { IconName } from "metabase/ui";
+import { Flex, Icon, UnstyledButton } from "metabase/ui";
+import type * as Lib from "metabase-lib";
+
+import { NotebookCell } from "../NotebookCell";
+
+type DataPickerTargetProps = {
+  tableInfo?: Lib.TableDisplayInfo;
+  placeholder: string;
+  isDisabled?: boolean;
+  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
+  onAuxClick?: (event: MouseEvent<HTMLButtonElement>) => void;
+  getTableIcon?: (tableInfo: Lib.TableDisplayInfo) => IconName;
+};
+
+export const DataPickerTarget = forwardRef(function DataPickerTarget(
+  {
+    tableInfo,
+    placeholder,
+    isDisabled,
+    onClick,
+    onAuxClick,
+    getTableIcon = defaultGetTableIcon,
+  }: DataPickerTargetProps,
+  ref: Ref<HTMLButtonElement>,
+) {
+  return (
+    <UnstyledButton
+      ref={ref}
+      c="inherit"
+      fz="inherit"
+      fw="inherit"
+      p={NotebookCell.CONTAINER_PADDING}
+      disabled={isDisabled}
+      onClick={onClick}
+      onAuxClick={onAuxClick}
+    >
+      <Flex align="center" gap="xs">
+        {tableInfo && (
+          <Icon name={getTableIcon(tableInfo)} style={{ flexShrink: 0 }} />
+        )}
+        {tableInfo?.displayName ?? placeholder}
+      </Flex>
+    </UnstyledButton>
+  );
+});
+function defaultGetTableIcon(tableInfo: Lib.TableDisplayInfo): IconName {
+  switch (true) {
+    case tableInfo.isQuestion:
+      return "table2";
+    case tableInfo.isModel:
+      return "model";
+    case tableInfo.isMetric:
+      return "metric";
+    default:
+      return "table";
+  }
+}
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx
new file mode 100644
index 000000000000..6850ffa17a48
--- /dev/null
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx
@@ -0,0 +1,120 @@
+import { useMemo } from "react";
+
+import { skipToken, useGetCardQuery, useSearchQuery } from "metabase/api";
+import { useSelector } from "metabase/lib/redux";
+import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
+import { DataSourceSelector } from "metabase/query_builder/components/DataSelector";
+import { getEmbedOptions } from "metabase/selectors/embed";
+import { getMetadata } from "metabase/selectors/metadata";
+import * as Lib from "metabase-lib";
+import type { TableId } from "metabase-types/api";
+
+import { DataPickerTarget } from "../DataPickerTarget";
+
+type EmbeddingDataPickerProps = {
+  query: Lib.Query;
+  stageIndex: number;
+  table: Lib.TableMetadata | Lib.CardMetadata | undefined;
+  placeholder: string;
+  canChangeDatabase: boolean;
+  isDisabled: boolean;
+  onChange: (tableId: TableId) => void;
+};
+export function EmbeddingDataPicker({
+  query,
+  stageIndex,
+  table,
+  placeholder,
+  canChangeDatabase,
+  isDisabled,
+  onChange,
+}: EmbeddingDataPickerProps) {
+  const { data: dataSourceCountData, isLoading: isDataSourceCountLoading } =
+    useSearchQuery({
+      models: ["dataset", "table"],
+      limit: 0,
+    });
+
+  const databaseId = Lib.databaseID(query);
+  const tableInfo =
+    table != null ? Lib.displayInfo(query, stageIndex, table) : undefined;
+  const pickerInfo = table != null ? Lib.pickerInfo(query, table) : undefined;
+  const { data: card } = useGetCardQuery(
+    pickerInfo?.cardId != null ? { id: pickerInfo.cardId } : skipToken,
+  );
+
+  const metadata = useSelector(getMetadata);
+  const databases = useMemo(() => {
+    // We're joining data
+    if (!canChangeDatabase) {
+      return [
+        metadata.database(databaseId),
+        metadata.savedQuestionsDatabase(),
+      ].filter(Boolean);
+    }
+
+    /**
+     * When not joining data, we want to use all databases loaded inside `DataSourceSelector`
+     *
+     * @see https://github.com/metabase/metabase/blob/fb25682fe8dbafe2062e37bce832f62440872ab7/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx#L1163-L1168
+     */
+    return undefined;
+  }, [canChangeDatabase, databaseId, metadata]);
+
+  const entityTypes = useSelector(state => getEmbedOptions(state).entity_types);
+
+  if (isDataSourceCountLoading) {
+    return null;
+  }
+
+  const shouldUseSimpleDataPicker =
+    dataSourceCountData != null && dataSourceCountData.total < 100;
+  if (shouldUseSimpleDataPicker) {
+    return (
+      <PLUGIN_EMBEDDING_SDK.SimpleDataPicker
+        filterByDatabaseId={canChangeDatabase ? null : databaseId}
+        selectedEntity={pickerInfo?.tableId}
+        isInitiallyOpen={!table}
+        triggerElement={
+          <DataPickerTarget
+            /**
+             * We try to blur the line between models and tables for embedding users.
+             * this property will change the way icons are displayed in the data picker trigger,
+             * so we need to remove it. Treating it as a table.
+             */
+            getTableIcon={() => "table"}
+            tableInfo={tableInfo}
+            placeholder={placeholder}
+            isDisabled={isDisabled}
+          />
+        }
+        setSourceTableFn={onChange}
+      />
+    );
+  }
+
+  return (
+    <DataSourceSelector
+      key={pickerInfo?.tableId}
+      isInitiallyOpen={!table}
+      databases={databases}
+      canChangeDatabase={canChangeDatabase}
+      selectedDatabaseId={databaseId}
+      selectedTableId={pickerInfo?.tableId}
+      selectedCollectionId={card?.collection_id}
+      databaseQuery={{ saved: true }}
+      canSelectModel={entityTypes.includes("model")}
+      canSelectTable={entityTypes.includes("table")}
+      canSelectMetric={false}
+      canSelectSavedQuestion={false}
+      triggerElement={
+        <DataPickerTarget
+          tableInfo={tableInfo}
+          placeholder={placeholder}
+          isDisabled={isDisabled}
+        />
+      }
+      setSourceTableFn={onChange}
+    />
+  );
+}
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/index.ts b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/index.ts
new file mode 100644
index 000000000000..8f16b0ccf32f
--- /dev/null
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/index.ts
@@ -0,0 +1 @@
+export { EmbeddingDataPicker } from "./EmbeddingDataPicker";
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx
new file mode 100644
index 000000000000..ec978711d8f8
--- /dev/null
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx
@@ -0,0 +1,92 @@
+import userEvent from "@testing-library/user-event";
+
+import { screen } from "__support__/ui";
+
+import { setup } from "./setup";
+
+describe("EmbeddingDataPicker", () => {
+  describe("multi-stage data picker", () => {
+    it("should show tables when there is no models", async () => {
+      setup({ hasModels: false });
+
+      const databaseOption = await screen.findByText("Sample Database");
+      expect(databaseOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Models")).not.toBeInTheDocument();
+      expect(screen.queryByText("Raw Data")).not.toBeInTheDocument();
+
+      await userEvent.click(databaseOption);
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    it('should show "BUCKET" step when there are both models and tables', async () => {
+      setup();
+
+      expect(await screen.findByText("Models")).toBeInTheDocument();
+      const rawDataOption = screen.getByText("Raw Data");
+      expect(rawDataOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Sample Database")).not.toBeInTheDocument();
+
+      await userEvent.click(rawDataOption);
+      await userEvent.click(screen.getByText("Sample Database"));
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    describe("entity_types", () => {
+      it('should show only models when `entity_types=["models"]`', async () => {
+        setup({
+          entityTypes: ["model"],
+        });
+
+        expect(await screen.findByText("Models")).toBeInTheDocument();
+
+        expect(screen.queryByText("Raw Data")).not.toBeInTheDocument();
+        expect(screen.queryByText("Sample Database")).not.toBeInTheDocument();
+      });
+
+      it('should show only tables when `entity_types=["table"]`', async () => {
+        setup({
+          entityTypes: ["table"],
+        });
+
+        expect(await screen.findByText("Sample Database")).toBeInTheDocument();
+
+        expect(screen.queryByText("Models")).not.toBeInTheDocument();
+        expect(screen.queryByText("Raw Data")).not.toBeInTheDocument();
+      });
+
+      it('should not show "saved questions" in the database list when `entity_types=["table"]`', async () => {
+        setup({
+          entityTypes: ["table"],
+        });
+
+        expect(await screen.findByText("Sample Database")).toBeInTheDocument();
+        expect(screen.queryByText("Saved Questions")).not.toBeInTheDocument();
+      });
+
+      it('should show both models and tables when `entity_types=["models", "table"]`', async () => {
+        setup({
+          entityTypes: ["model", "table"],
+        });
+
+        expect(await screen.findByText("Models")).toBeInTheDocument();
+        expect(screen.getByText("Raw Data")).toBeInTheDocument();
+
+        expect(screen.queryByText("Sample Database")).not.toBeInTheDocument();
+      });
+
+      /**
+       * We don't test invalid `entityTypes` values here as the redux state is set via a slice which has proper validations and tests in place.
+       *
+       * @see frontend/src/metabase/redux/embed/embed.unit.spec.ts
+       */
+    });
+  });
+});
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/setup.tsx b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/setup.tsx
new file mode 100644
index 000000000000..6963f28e13fc
--- /dev/null
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/setup.tsx
@@ -0,0 +1,114 @@
+import fetchMock from "fetch-mock";
+
+import {
+  setupDatabasesEndpoints,
+  setupSearchEndpoints,
+} from "__support__/server-mocks";
+import { renderWithProviders } from "__support__/ui";
+import { createMockModelResult } from "metabase/browse/models/test-utils";
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+import type { Query } from "metabase-lib";
+import Question from "metabase-lib/v1/Question";
+import {
+  createOrdersTable,
+  createPeopleTable,
+  createProductsTable,
+  createReviewsTable,
+  createSampleDatabase,
+} from "metabase-types/api/mocks/presets";
+import {
+  createMockEmbedOptions,
+  createMockEmbedState,
+  createMockState,
+} from "metabase-types/store/mocks";
+
+import { EmbeddingDataPicker } from "../EmbeddingDataPicker";
+
+interface SetupOpts {
+  hasModels?: boolean;
+  entityTypes?: EmbeddingEntityType[];
+}
+
+const DEFAULT_OPTS: Partial<SetupOpts> = {
+  hasModels: true,
+};
+
+export function setup({
+  hasModels = DEFAULT_OPTS.hasModels,
+  entityTypes,
+}: SetupOpts = {}) {
+  const query = createEmptyQuery();
+
+  fetchMock.get(
+    {
+      name: "entity-count",
+      url: "path:/api/search",
+      query: {
+        models: ["dataset", "table"],
+        limit: 0,
+      },
+    },
+    {
+      total: 100,
+    },
+  );
+
+  if (hasModels) {
+    setupSearchEndpoints(createSearchResults());
+  } else {
+    setupSearchEndpoints([]);
+  }
+  setupDatabasesEndpoints([createDatabase()]);
+
+  renderWithProviders(
+    <EmbeddingDataPicker
+      query={query}
+      stageIndex={0}
+      canChangeDatabase={true}
+      isDisabled={false}
+      onChange={jest.fn()}
+      placeholder="Pick your starting data"
+      table={undefined}
+    />,
+    entityTypes
+      ? {
+          storeInitialState: createMockState({
+            embed: createMockEmbedState({
+              options: createMockEmbedOptions({
+                entity_types: entityTypes,
+              }),
+            }),
+          }),
+        }
+      : undefined,
+  );
+}
+
+function createDatabase() {
+  return createSampleDatabase({
+    tables: [
+      createOrdersTable(),
+      createPeopleTable(),
+      createProductsTable(),
+      createReviewsTable(),
+    ],
+  });
+}
+
+function createSearchResults() {
+  return [
+    createMockModelResult({
+      id: 1,
+      name: "Orders model",
+    }),
+    createMockModelResult({
+      id: 2,
+      name: "People model",
+    }),
+  ];
+}
+
+function createEmptyQuery(): Query {
+  const question = Question.create();
+  return question.query();
+}
diff --git a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/NotebookDataPicker.tsx b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/NotebookDataPicker.tsx
index 4cbbd307ff42..5f005136e5c9 100644
--- a/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/NotebookDataPicker.tsx
+++ b/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/NotebookDataPicker.tsx
@@ -1,14 +1,7 @@
-import {
-  type MouseEvent,
-  type Ref,
-  forwardRef,
-  useMemo,
-  useState,
-} from "react";
+import { type MouseEvent, useState } from "react";
 import { useLatest } from "react-use";
 import { t } from "ttag";
 
-import { skipToken, useGetCardQuery, useSearchQuery } from "metabase/api";
 import {
   DataPickerModal,
   getDataPickerValue,
@@ -17,13 +10,10 @@ import { METAKEY } from "metabase/lib/browser";
 import { useDispatch, useSelector, useStore } from "metabase/lib/redux";
 import { checkNotNull } from "metabase/lib/types";
 import * as Urls from "metabase/lib/urls";
-import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
-import { DataSourceSelector } from "metabase/query_builder/components/DataSelector";
 import { loadMetadataForTable } from "metabase/questions/actions";
 import { getIsEmbedding } from "metabase/selectors/embed";
 import { getMetadata } from "metabase/selectors/metadata";
-import type { IconName } from "metabase/ui";
-import { Flex, Icon, Tooltip, UnstyledButton } from "metabase/ui";
+import { Tooltip } from "metabase/ui";
 import * as Lib from "metabase-lib";
 import type { TableId } from "metabase-types/api";
 
@@ -31,8 +21,9 @@ import {
   type NotebookContextType,
   useNotebookContext,
 } from "../Notebook/context";
-import { NotebookCell } from "../NotebookCell";
 
+import { DataPickerTarget } from "./DataPickerTarget";
+import { EmbeddingDataPicker } from "./EmbeddingDataPicker";
 import { getUrl } from "./utils";
 
 interface NotebookDataPickerProps {
@@ -193,165 +184,6 @@ function ModernDataPicker({
   );
 }
 
-type EmbeddingDataPickerProps = {
-  query: Lib.Query;
-  stageIndex: number;
-  table: Lib.TableMetadata | Lib.CardMetadata | undefined;
-  placeholder: string;
-  canChangeDatabase: boolean;
-  isDisabled: boolean;
-  onChange: (tableId: TableId) => void;
-};
-
-function EmbeddingDataPicker({
-  query,
-  stageIndex,
-  table,
-  placeholder,
-  canChangeDatabase,
-  isDisabled,
-  onChange,
-}: EmbeddingDataPickerProps) {
-  const { data: dataSourceCountData, isLoading: isDataSourceCountLoading } =
-    useSearchQuery({
-      models: ["dataset", "table"],
-      limit: 0,
-    });
-
-  const databaseId = Lib.databaseID(query);
-  const tableInfo =
-    table != null ? Lib.displayInfo(query, stageIndex, table) : undefined;
-  const pickerInfo = table != null ? Lib.pickerInfo(query, table) : undefined;
-  const { data: card } = useGetCardQuery(
-    pickerInfo?.cardId != null ? { id: pickerInfo.cardId } : skipToken,
-  );
-
-  const metadata = useSelector(getMetadata);
-  const databases = useMemo(() => {
-    // We're joining data
-    if (!canChangeDatabase) {
-      return [
-        metadata.database(databaseId),
-        metadata.savedQuestionsDatabase(),
-      ].filter(Boolean);
-    }
-
-    /**
-     * When not joining data, we want to use all databases loaded inside `DataSourceSelector`
-     *
-     * @see https://github.com/metabase/metabase/blob/fb25682fe8dbafe2062e37bce832f62440872ab7/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx#L1163-L1168
-     */
-    return undefined;
-  }, [canChangeDatabase, databaseId, metadata]);
-
-  if (isDataSourceCountLoading) {
-    return null;
-  }
-
-  const shouldUseSimpleDataPicker =
-    dataSourceCountData != null && dataSourceCountData.total < 100;
-  if (shouldUseSimpleDataPicker) {
-    return (
-      <PLUGIN_EMBEDDING_SDK.SimpleDataPicker
-        filterByDatabaseId={canChangeDatabase ? null : databaseId}
-        selectedEntity={pickerInfo?.tableId}
-        isInitiallyOpen={!table}
-        triggerElement={
-          <DataPickerTarget
-            /**
-             * We try to blur the line between models and tables for embedding users.
-             * this property will change the way icons are displayed in the data picker trigger,
-             * so we need to remove it. Treating it as a table.
-             */
-            getTableIcon={() => "table"}
-            tableInfo={tableInfo}
-            placeholder={placeholder}
-            isDisabled={isDisabled}
-          />
-        }
-        setSourceTableFn={onChange}
-      />
-    );
-  }
-
-  return (
-    <DataSourceSelector
-      key={pickerInfo?.tableId}
-      isInitiallyOpen={!table}
-      databases={databases}
-      canChangeDatabase={canChangeDatabase}
-      selectedDatabaseId={databaseId}
-      selectedTableId={pickerInfo?.tableId}
-      selectedCollectionId={card?.collection_id}
-      databaseQuery={{ saved: true }}
-      canSelectMetric={false}
-      canSelectSavedQuestion={false}
-      triggerElement={
-        <DataPickerTarget
-          tableInfo={tableInfo}
-          placeholder={placeholder}
-          isDisabled={isDisabled}
-        />
-      }
-      setSourceTableFn={onChange}
-    />
-  );
-}
-
-type DataPickerTargetProps = {
-  tableInfo?: Lib.TableDisplayInfo;
-  placeholder: string;
-  isDisabled?: boolean;
-  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
-  onAuxClick?: (event: MouseEvent<HTMLButtonElement>) => void;
-  getTableIcon?: (tableInfo: Lib.TableDisplayInfo) => IconName;
-};
-
-const DataPickerTarget = forwardRef(function DataPickerTarget(
-  {
-    tableInfo,
-    placeholder,
-    isDisabled,
-    onClick,
-    onAuxClick,
-    getTableIcon = defaultGetTableIcon,
-  }: DataPickerTargetProps,
-  ref: Ref<HTMLButtonElement>,
-) {
-  return (
-    <UnstyledButton
-      ref={ref}
-      c="inherit"
-      fz="inherit"
-      fw="inherit"
-      p={NotebookCell.CONTAINER_PADDING}
-      disabled={isDisabled}
-      onClick={onClick}
-      onAuxClick={onAuxClick}
-    >
-      <Flex align="center" gap="xs">
-        {tableInfo && (
-          <Icon name={getTableIcon(tableInfo)} style={{ flexShrink: 0 }} />
-        )}
-        {tableInfo?.displayName ?? placeholder}
-      </Flex>
-    </UnstyledButton>
-  );
-});
-
-function defaultGetTableIcon(tableInfo: Lib.TableDisplayInfo): IconName {
-  switch (true) {
-    case tableInfo.isQuestion:
-      return "table2";
-    case tableInfo.isModel:
-      return "model";
-    case tableInfo.isMetric:
-      return "metric";
-    default:
-      return "table";
-  }
-}
-
 function getModelFilterList(
   { modelsFilterList }: NotebookContextType,
   hasMetrics: boolean,
diff --git a/frontend/src/metabase/reducers-common.ts b/frontend/src/metabase/reducers-common.ts
index 020c6f1ba946..0333d224bbea 100644
--- a/frontend/src/metabase/reducers-common.ts
+++ b/frontend/src/metabase/reducers-common.ts
@@ -8,7 +8,7 @@ import * as parameters from "metabase/parameters/reducers";
 import app from "metabase/redux/app";
 import { reducer as auth } from "metabase/redux/auth";
 import { reducer as downloads } from "metabase/redux/downloads";
-import embed from "metabase/redux/embed";
+import { embed } from "metabase/redux/embed";
 import entities, { enhanceRequestsReducer } from "metabase/redux/entities";
 import requests from "metabase/redux/requests";
 import { settings } from "metabase/redux/settings";
diff --git a/frontend/src/metabase/redux/embed.ts b/frontend/src/metabase/redux/embed.ts
deleted file mode 100644
index 71d469701199..000000000000
--- a/frontend/src/metabase/redux/embed.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
-import { pick } from "underscore";
-
-import { parseSearchOptions } from "metabase/lib/browser";
-import type { InteractiveEmbeddingOptions } from "metabase-types/store";
-
-export const DEFAULT_EMBED_OPTIONS: InteractiveEmbeddingOptions = {
-  top_nav: true,
-  side_nav: "default",
-  search: false,
-  new_button: false,
-  breadcrumbs: true,
-  logo: true,
-  header: true,
-  additional_info: true,
-  action_buttons: true,
-} as const;
-
-const allowedEmbedOptions = Object.keys(DEFAULT_EMBED_OPTIONS);
-
-export const urlParameterToBoolean = (
-  urlParameter: string | string[] | boolean | undefined,
-) => {
-  if (urlParameter === undefined) {
-    return undefined;
-  }
-  if (Array.isArray(urlParameter)) {
-    return Boolean(urlParameter.at(-1));
-  } else {
-    return Boolean(urlParameter);
-  }
-};
-
-const interactiveEmbedSlice = createSlice({
-  name: "interactiveEmbed",
-  initialState: {
-    options: {} as InteractiveEmbeddingOptions,
-    isEmbeddingSdk: false,
-  },
-  reducers: {
-    setInitialUrlOptions: (
-      state,
-      action: PayloadAction<{ search: string }>,
-    ) => {
-      const searchOptions = parseSearchOptions(action.payload.search);
-
-      state.options = {
-        ...DEFAULT_EMBED_OPTIONS,
-        ...pick(searchOptions, allowedEmbedOptions),
-      };
-    },
-    setOptions: (
-      state,
-      action: PayloadAction<Partial<InteractiveEmbeddingOptions>>,
-    ) => {
-      state.options = {
-        ...state.options,
-        ...action.payload,
-      };
-    },
-  },
-});
-
-export const { setInitialUrlOptions, setOptions } =
-  interactiveEmbedSlice.actions;
-
-// eslint-disable-next-line import/no-default-export
-export default interactiveEmbedSlice.reducer;
diff --git a/frontend/src/metabase/redux/embed.unit.spec.ts b/frontend/src/metabase/redux/embed.unit.spec.ts
deleted file mode 100644
index 042c9c0cb4b9..000000000000
--- a/frontend/src/metabase/redux/embed.unit.spec.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { type Dispatch, configureStore } from "@reduxjs/toolkit";
-
-import embedReduer, {
-  DEFAULT_EMBED_OPTIONS,
-  setInitialUrlOptions,
-} from "./embed";
-
-describe("embed reducer", () => {
-  describe("setInitialUrlOptions", () => {
-    it("should set default options", () => {
-      const store = createMockStore();
-
-      store.dispatch(setInitialUrlOptions({ search: "" }));
-
-      expect(store.getState().embed.options).toEqual(DEFAULT_EMBED_OPTIONS);
-    });
-
-    it("should set options from search", () => {
-      const store = createMockStore();
-
-      store.dispatch(
-        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
-      );
-
-      expect(store.getState().embed.options.top_nav).toBe(false);
-      expect(store.getState().embed.options.new_button).toBe(true);
-    });
-
-    it("should ignore invalid options", () => {
-      const store = createMockStore();
-
-      store.dispatch(
-        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
-      );
-
-      expect(store.getState().embed.options).not.toHaveProperty(
-        "invalid_option",
-      );
-    });
-  });
-});
-
-const createMockStore = () => {
-  const store = configureStore({
-    reducer: { embed: embedReduer },
-  });
-  return store as typeof store & { dispatch: Dispatch };
-};
diff --git a/frontend/src/metabase/redux/embed/embed.ts b/frontend/src/metabase/redux/embed/embed.ts
new file mode 100644
index 000000000000..67de2cc68426
--- /dev/null
+++ b/frontend/src/metabase/redux/embed/embed.ts
@@ -0,0 +1,151 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  };
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeEntityTypesCommaSeparatedSearchParameter,
+      )(action.payload.search);
+
+      state.options = {
+        ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+        ...searchOptions,
+      };
+    },
+    setOptions: (
+      state,
+      action: PayloadAction<Partial<InteractiveEmbeddingOptions>>,
+    ) => {
+      state.options = {
+        ...state.options,
+        ...action.payload,
+      };
+    },
+  },
+});
+
+/**
+ * this functions turns a string like `entity_types=value1,value2` into `entity_types=value1&entity_types=value2` that matches the URLSearchParams format
+ */
+function normalizeEntityTypesCommaSeparatedSearchParameter(
+  search: string,
+): string {
+  const searchParams = new URLSearchParams(search);
+
+  const PARAMETER = "entity_types";
+  const [optionValues] = searchParams.getAll(PARAMETER);
+  if (optionValues && isArrayString(optionValues)) {
+    searchParams.delete(PARAMETER);
+    optionValues.split(",").forEach(value => {
+      const normalizedValue = value.trim();
+      if (normalizedValue) {
+        searchParams.append(PARAMETER, normalizedValue);
+      }
+    });
+  }
+  return searchParams.toString();
+}
+
+function isArrayString(string: string) {
+  return string.includes(",");
+}
+
+function excludeNonInteractiveEmbeddingOptions(
+  embeddingOptions: Record<string, any>,
+): Partial<InteractiveEmbeddingOptions> {
+  return pick(embeddingOptions, ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS);
+}
+
+/**
+ * this function is key to ensure that we won't end up with invalid `entity_types` values.
+ * As it could not be empty, it needs at least a single valid value. e.g. `["model"]`, or `["model", "table"]`,
+ * but never `[]`.
+ */
+function normalizeEntityTypes(
+  searchOptions: Partial<InteractiveEmbeddingOptions>,
+): Partial<InteractiveEmbeddingOptions> {
+  const ALLOWED_ENTITY_TYPES: InteractiveEmbeddingOptions["entity_types"] = [
+    "model",
+    "table",
+  ];
+
+  /**
+   * `parseSearchOptions` would return either a string or an array of strings.
+   */
+  const { entity_types: entityTypesValueOrArray } = searchOptions;
+  if (entityTypesValueOrArray) {
+    const entityTypes = Array.isArray(entityTypesValueOrArray)
+      ? entityTypesValueOrArray
+      : [entityTypesValueOrArray];
+    const filteredEntityTypes = entityTypes.filter(type =>
+      ALLOWED_ENTITY_TYPES.includes(type),
+    );
+
+    if (filteredEntityTypes.length === 0) {
+      return {
+        ...searchOptions,
+        entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+      };
+    }
+
+    return {
+      ...searchOptions,
+      entity_types: filteredEntityTypes,
+    };
+  }
+
+  return searchOptions;
+}
+
+export const { setInitialUrlOptions, setOptions } =
+  interactiveEmbedSlice.actions;
+
+export const embed = interactiveEmbedSlice.reducer;
diff --git a/frontend/src/metabase/redux/embed/embed.unit.spec.ts b/frontend/src/metabase/redux/embed/embed.unit.spec.ts
new file mode 100644
index 000000000000..eb08d4983a92
--- /dev/null
+++ b/frontend/src/metabase/redux/embed/embed.unit.spec.ts
@@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
+
+    describe("entity_types", () => {
+      it('should accept multiple "entity_types" options', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=table&entity_types=model",
+          }),
+        );
+
+        // The default value is `["model", "table"]`, so we know these 2 types are set correctly.
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "table",
+          "model",
+        ]);
+      });
+
+      it('should accept single "entity_types" option', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=model",
+          }),
+        );
+
+        expect(store.getState().embed.options.entity_types).toEqual(["model"]);
+      });
+
+      it('should accept comma separated "entity_types" option', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=model,table",
+          }),
+        );
+
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "model",
+          "table",
+        ]);
+      });
+
+      it('should accept comma separated "entity_types" option with space', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types= table , model",
+          }),
+        );
+
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "table",
+          "model",
+        ]);
+      });
+
+      it('should ignore invalid "entity_types" option', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=not_a_valid_type",
+          }),
+        );
+
+        // Default value
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "model",
+          "table",
+        ]);
+      });
+
+      it('should set "entity_types" option to the default value `["model", "table"]` when "entity_types" are empty', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=",
+          }),
+        );
+
+        // Default value
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "model",
+          "table",
+        ]);
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "",
+          }),
+        );
+
+        // Default value
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "model",
+          "table",
+        ]);
+      });
+
+      it('should ignore "entity_types" that is not the first option when the first option has comma', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types= table, invalid&entity_types=model",
+          }),
+        );
+
+        // Default value
+        expect(store.getState().embed.options.entity_types).toEqual(["table"]);
+      });
+    });
+  });
+});
+
+const createMockStore = () => {
+  const store = configureStore({
+    reducer: { embed: embedReducer },
+  });
+  return store as typeof store & { dispatch: Dispatch };
+};
diff --git a/frontend/src/metabase/redux/embed/index.ts b/frontend/src/metabase/redux/embed/index.ts
new file mode 100644
index 000000000000..12b3c5295ba6
--- /dev/null
+++ b/frontend/src/metabase/redux/embed/index.ts
@@ -0,0 +1 @@
+export * from "./embed";
diff --git a/frontend/src/metabase/selectors/embed.ts b/frontend/src/metabase/selectors/embed.ts
index 9a446d686eaa..55d43cee9656 100644
--- a/frontend/src/metabase/selectors/embed.ts
+++ b/frontend/src/metabase/selectors/embed.ts
@@ -7,7 +7,10 @@ export const getIsEmbeddingIframe = (_state?: State): boolean => {
   return isWithinIframe();
 };
 
-export const getEmbedOptions = (state: State): InteractiveEmbeddingOptions => {
+type EmptyObject = Record<string, never>;
+export const getEmbedOptions = (
+  state: State,
+): InteractiveEmbeddingOptions | EmptyObject => {
   return state.embed.options;
 };
 


Reviews:
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:02:48Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T11:03:33Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T11:39:46Z
user: login: heypoom
body: Tests and code looks correct to me overall! (I haven't tested this PR locally yet though as I'm nearing EoD)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T12:20:10Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:28:03Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:29:06Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:32:15Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:32:58Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:36:39Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:48:31Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:50:04Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:57:55Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T15:01:14Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T15:33:07Z
user: login: npretto
body: tested locally and works as described 👍 
Thank you for the instructions to how to test it
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T16:56:44Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T10:37:26Z
user: login: romeovs
body: I have some small nits, but the code looks ok overall!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T13:50:03Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T14:24:22Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T14:29:32Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T12:02:07Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T12:02:10Z

Review comments:
diff_hunk: @@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: WiNloSt
body: for auto-completion
created_at: 2025-03-11T09:57:57Z
author_association: MEMBER
diff_hunk: @@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
+     * @param {boolean} [option.isMultiStageDataPicker]
+     * @param {import("metabase-types/store").InteractiveEmbeddingOptions} [option.qs]
+     */
     function startNewEmbeddingQuestion({
       isMultiStageDataPicker = false,
+      qs,
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: WiNloSt
body: I pass `entity_types` here.
created_at: 2025-03-11T09:58:11Z
author_association: MEMBER
diff_hunk: @@ -21,7 +22,9 @@ export const sdkReducers = {
 export const getSdkStore = () =>
   getStore(sdkReducers, null, {
     embed: {
-      options: {},
+      options: {
+        entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
path: enterprise/frontend/src/embedding-sdk/store/index.ts
user: login: WiNloSt
body: this is to simplify the code, we'll be using the same slice on both the SDK and interactive embedding for the time being.

I'll figure something out when implementing the SDK.
created_at: 2025-03-11T11:21:45Z
author_association: MEMBER
diff_hunk: @@ -1238,6 +1246,35 @@ describe("scenarios > embedding > full app", () => {
         });
       });
     });
+
+    describe('"entity_types" query parameter', () => {
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: WiNloSt
body: https://www.notion.so/metabase/Testing-Let-embedders-hide-tables-1ae69354c901807794d2f232d701f037?pvs=4#1ae69354c9018043ae74f924164cf62e
created_at: 2025-03-11T11:22:09Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: Correct the type. In interactive embedding, these values are presented. They're not optional.

As you can see here. When we initialize this slice, we set all properties.

https://github.com/metabase/metabase/blob/17bc5eaf31cc47420faf7b62005e5c0ba7056b69/frontend/src/metabase/redux/embed.ts#L47-L50

Also, since we're setting all key accept `font` I don't see why we don't initialize font as `undefined`, so I also make that change here, so the type could be `  font: string | undefined;`

created_at: 2025-03-11T11:25:45Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
+  top_nav: boolean;
+  search: boolean;
+  new_button: boolean;
+  breadcrumbs: boolean;
+  logo: boolean;
+  side_nav: boolean | "default";
+  header: boolean;
+  additional_info: boolean;
+  action_buttons: boolean;
+  entity_types: EmbeddingEntityType[];
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: This is added. It's used to configure what kind of entity we want to show in multi-stage embedding data picker. The default value is `["model", "table"]`
created_at: 2025-03-11T11:25:55Z
author_association: MEMBER
diff_hunk: @@ -6,6 +7,7 @@ import type {
 export const createMockEmbedOptions = (
   opts?: Partial<InteractiveEmbeddingOptions>,
 ) => ({
+  ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
path: frontend/src/metabase-types/store/mocks/embed.ts
user: login: WiNloSt
body: This change is to match the actual reducer behavior. As the data is initialized with default values by default on interactive embedding.
created_at: 2025-03-11T11:27:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+export const DEFAULT_EMBEDDING_ENTITY_TYPES: EmbeddingEntityType[] = [
path: frontend/src/metabase/embedding-sdk/store.ts
user: login: WiNloSt
body: This is on its own file since it's imported by both interactive embed slice and SDK slice.

Please note that I might not use the same reducer in the SDK in the next PRs. This is to keep the diff simple for a starting task.
created_at: 2025-03-11T11:28:25Z
author_association: MEMBER
diff_hunk: @@ -9,7 +9,7 @@ import {
   setupDashboardEndpoints,
 } from "__support__/server-mocks";
 import { renderWithProviders } from "__support__/ui";
-import { DEFAULT_EMBED_OPTIONS } from "metabase/redux/embed";
+import { DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS } from "metabase/redux/embed";
path: frontend/src/metabase/nav/containers/AppBar/AppBar.unit.spec.tsx
user: login: WiNloSt
body: jsut a rename
created_at: 2025-03-11T11:28:36Z
author_association: MEMBER
diff_hunk: @@ -494,7 +498,9 @@ export class UnconnectedDataSelector extends Component {
     // "Saved Questions" are presented in a different picker step
     // So it should be excluded from a regular databases list
     const shouldRemoveSavedQuestionDatabaseFromList =
-      !this.props.hasNestedQueriesEnabled || this.hasUsableModelsOrMetrics();
+      !this.props.hasNestedQueriesEnabled ||
+      this.hasUsableModelsOrMetrics() ||
+      !this.props.canSelectSavedQuestion;
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
user: login: WiNloSt
body: Without this change, we'll see `Saved questions` when the `entity_type` is `["table"]` only.
![Screenshot 2025-03-07 at 6 46 47 PM](https://github.com/user-attachments/assets/1cf68130-9c09-4af7-ae07-2315077610da)

This is coreved in https://github.com/metabase/metabase/blob/d99b053fd03dd38f8922ac80933af24b76c49c03/frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx#L47-L54
created_at: 2025-03-11T11:29:49Z
author_association: MEMBER
diff_hunk: @@ -197,6 +199,8 @@ export class UnconnectedDataSelector extends Component {
     hasTriggerExpandControl: true,
     isPopover: true,
     isMantine: false,
+    canSelectModel: true,
+    canSelectTable: true,
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
user: login: WiNloSt
body: Add 2 new props as per [the tech design](https://www.notion.so/metabase/Tech-Let-embedders-hide-tables-1ae69354c9018057bc1ddcf987443164?pvs=4#1ae69354c90180e3bc99c625bbbc7c3c). These are true by default because we don't want to hide anything. For other data selectors. Other data selectors should behave the same without having to specify any of these new props.
created_at: 2025-03-11T12:15:50Z
author_association: MEMBER
diff_hunk: @@ -456,8 +460,8 @@ export class UnconnectedDataSelector extends Component {
   }
 
   hasModels = () => {
-    const { models, loaded } = this.props;
-    return loaded && models && models.length > 0;
+    const { models, loaded, canSelectModel } = this.props;
+    return loaded && models && models.length > 0 && canSelectModel;
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
user: login: WiNloSt
body: The change makes sure it works the same way as other `hasXxxx` functions e.g. `hasMetrics` or `hasSavedQusetions`
created_at: 2025-03-11T12:17:04Z
author_association: MEMBER
diff_hunk: @@ -906,6 +912,7 @@ export class UnconnectedDataSelector extends Component {
             <DataBucketPicker
               dataTypes={getDataTypes({
                 hasModels: this.hasModels(),
+                hasTables: this.props.canSelectTable,
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx
user: login: WiNloSt
body: I have considered creating `this.hasTables()`, but it'll just return `this.props.canSelectTable`, so I inlined it instead.
created_at: 2025-03-11T12:17:46Z
author_association: MEMBER
diff_hunk: @@ -17,8 +17,9 @@ const setup = (dataTypes: DataTypeInfoItem[]) => {
 describe("DataSelectorDataBucketPicker", () => {
   it("should display all buckets", () => {
     const dataTypes = getDataTypes({
-      hasMetrics: false,
       hasModels: true,
+      hasTables: true,
+      hasMetrics: false,
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx
user: login: WiNloSt
body: reorder to match the actual bucket view. Here in the new version metrics and saved questions are hidden, but this is the actual order when they're still visible.
![image](https://github.com/user-attachments/assets/a235ae3e-12fa-4ec5-af6d-7adbee26f08e)

created_at: 2025-03-11T12:23:02Z
author_association: MEMBER
diff_hunk: @@ -17,8 +17,9 @@ const setup = (dataTypes: DataTypeInfoItem[]) => {
 describe("DataSelectorDataBucketPicker", () => {
   it("should display all buckets", () => {
     const dataTypes = getDataTypes({
-      hasMetrics: false,
       hasModels: true,
+      hasTables: true,
path: frontend/src/metabase/query_builder/components/DataSelector/DataSelectorDataBucketPicker/DataSelectorDataBucketPicker.unit.spec.tsx
user: login: WiNloSt
body: this is the new option.
created_at: 2025-03-11T12:23:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { type MouseEvent, type Ref, forwardRef } from "react";
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/DataPickerTarget.tsx
user: login: WiNloSt
body: Nothing has changed, I just moved it to a separate file because `EmbeddingDataPicker` aka `LegacyDataPicker` is also moved to its own file.
created_at: 2025-03-11T12:24:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+import { useMemo } from "react";
+
+import { skipToken, useGetCardQuery, useSearchQuery } from "metabase/api";
+import { useSelector } from "metabase/lib/redux";
+import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
+import { DataSourceSelector } from "metabase/query_builder/components/DataSelector";
+import { getEmbedOptions } from "metabase/selectors/embed";
+import { getMetadata } from "metabase/selectors/metadata";
+import * as Lib from "metabase-lib";
+import type { TableId } from "metabase-types/api";
+
+import { DataPickerTarget } from "../NotebookDataPicker";
+
+type EmbeddingDataPickerProps = {
+  query: Lib.Query;
+  stageIndex: number;
+  table: Lib.TableMetadata | Lib.CardMetadata | undefined;
+  placeholder: string;
+  canChangeDatabase: boolean;
+  isDisabled: boolean;
+  onChange: (tableId: TableId) => void;
+};
+export function EmbeddingDataPicker({
+  query,
+  stageIndex,
+  table,
+  placeholder,
+  canChangeDatabase,
+  isDisabled,
+  onChange,
+}: EmbeddingDataPickerProps) {
+  const { data: dataSourceCountData, isLoading: isDataSourceCountLoading } =
+    useSearchQuery({
+      models: ["dataset", "table"],
+      limit: 0,
+    });
+
+  const databaseId = Lib.databaseID(query);
+  const tableInfo =
+    table != null ? Lib.displayInfo(query, stageIndex, table) : undefined;
+  const pickerInfo = table != null ? Lib.pickerInfo(query, table) : undefined;
+  const { data: card } = useGetCardQuery(
+    pickerInfo?.cardId != null ? { id: pickerInfo.cardId } : skipToken,
+  );
+
+  const metadata = useSelector(getMetadata);
+  const databases = useMemo(() => {
+    // We're joining data
+    if (!canChangeDatabase) {
+      return [
+        metadata.database(databaseId),
+        metadata.savedQuestionsDatabase(),
+      ].filter(Boolean);
+    }
+
+    /**
+     * When not joining data, we want to use all databases loaded inside `DataSourceSelector`
+     *
+     * @see https://github.com/metabase/metabase/blob/fb25682fe8dbafe2062e37bce832f62440872ab7/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx#L1163-L1168
+     */
+    return undefined;
+  }, [canChangeDatabase, databaseId, metadata]);
+
+  const entityTypes = useSelector(state => getEmbedOptions(state).entity_types);
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx
user: login: WiNloSt
body: This is the 1 of the only 3 lines that has changed.
created_at: 2025-03-11T12:27:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,120 @@
+import { useMemo } from "react";
+
+import { skipToken, useGetCardQuery, useSearchQuery } from "metabase/api";
+import { useSelector } from "metabase/lib/redux";
+import { PLUGIN_EMBEDDING_SDK } from "metabase/plugins";
+import { DataSourceSelector } from "metabase/query_builder/components/DataSelector";
+import { getEmbedOptions } from "metabase/selectors/embed";
+import { getMetadata } from "metabase/selectors/metadata";
+import * as Lib from "metabase-lib";
+import type { TableId } from "metabase-types/api";
+
+import { DataPickerTarget } from "../NotebookDataPicker";
+
+type EmbeddingDataPickerProps = {
+  query: Lib.Query;
+  stageIndex: number;
+  table: Lib.TableMetadata | Lib.CardMetadata | undefined;
+  placeholder: string;
+  canChangeDatabase: boolean;
+  isDisabled: boolean;
+  onChange: (tableId: TableId) => void;
+};
+export function EmbeddingDataPicker({
+  query,
+  stageIndex,
+  table,
+  placeholder,
+  canChangeDatabase,
+  isDisabled,
+  onChange,
+}: EmbeddingDataPickerProps) {
+  const { data: dataSourceCountData, isLoading: isDataSourceCountLoading } =
+    useSearchQuery({
+      models: ["dataset", "table"],
+      limit: 0,
+    });
+
+  const databaseId = Lib.databaseID(query);
+  const tableInfo =
+    table != null ? Lib.displayInfo(query, stageIndex, table) : undefined;
+  const pickerInfo = table != null ? Lib.pickerInfo(query, table) : undefined;
+  const { data: card } = useGetCardQuery(
+    pickerInfo?.cardId != null ? { id: pickerInfo.cardId } : skipToken,
+  );
+
+  const metadata = useSelector(getMetadata);
+  const databases = useMemo(() => {
+    // We're joining data
+    if (!canChangeDatabase) {
+      return [
+        metadata.database(databaseId),
+        metadata.savedQuestionsDatabase(),
+      ].filter(Boolean);
+    }
+
+    /**
+     * When not joining data, we want to use all databases loaded inside `DataSourceSelector`
+     *
+     * @see https://github.com/metabase/metabase/blob/fb25682fe8dbafe2062e37bce832f62440872ab7/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx#L1163-L1168
+     */
+    return undefined;
+  }, [canChangeDatabase, databaseId, metadata]);
+
+  const entityTypes = useSelector(state => getEmbedOptions(state).entity_types);
+
+  if (isDataSourceCountLoading) {
+    return null;
+  }
+
+  const shouldUseSimpleDataPicker =
+    dataSourceCountData != null && dataSourceCountData.total < 100;
+  if (shouldUseSimpleDataPicker) {
+    return (
+      <PLUGIN_EMBEDDING_SDK.SimpleDataPicker
+        filterByDatabaseId={canChangeDatabase ? null : databaseId}
+        selectedEntity={pickerInfo?.tableId}
+        isInitiallyOpen={!table}
+        triggerElement={
+          <DataPickerTarget
+            /**
+             * We try to blur the line between models and tables for embedding users.
+             * this property will change the way icons are displayed in the data picker trigger,
+             * so we need to remove it. Treating it as a table.
+             */
+            getTableIcon={() => "table"}
+            tableInfo={tableInfo}
+            placeholder={placeholder}
+            isDisabled={isDisabled}
+          />
+        }
+        setSourceTableFn={onChange}
+      />
+    );
+  }
+
+  return (
+    <DataSourceSelector
+      key={pickerInfo?.tableId}
+      isInitiallyOpen={!table}
+      databases={databases}
+      canChangeDatabase={canChangeDatabase}
+      selectedDatabaseId={databaseId}
+      selectedTableId={pickerInfo?.tableId}
+      selectedCollectionId={card?.collection_id}
+      databaseQuery={{ saved: true }}
+      canSelectModel={entityTypes.includes("model")}
+      canSelectTable={entityTypes.includes("table")}
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/EmbeddingDataPicker.tsx
user: login: WiNloSt
body: These are the rest 2 lines that are added. For other data selectors, they don't need to specify these props since they default to `true`
created_at: 2025-03-11T12:29:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,74 @@
+import { screen } from "__support__/ui";
+
+import { setup } from "./setup";
+
+describe("EmbeddingDataPicker", () => {
+  describe("multi-stage data picker", () => {
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx
user: login: WiNloSt
body: This only covers the multi-stage data picker. We don't have to test the simple data picker (new embedding & SDK data picker) since those are covered in `enterprise/frontend/src/embedding-sdk/components/private/SimpleDataPicker/SimpleDataPicker.unit.spec.tsx`
created_at: 2025-03-11T12:32:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+import fetchMock from "fetch-mock";
+
+import {
+  setupDatabasesEndpoints,
+  setupSearchEndpoints,
+} from "__support__/server-mocks";
+import { renderWithProviders } from "__support__/ui";
+import { createMockModelResult } from "metabase/browse/models/test-utils";
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+import type { Query } from "metabase-lib";
+import Question from "metabase-lib/v1/Question";
+import {
+  createOrdersTable,
+  createPeopleTable,
+  createProductsTable,
+  createReviewsTable,
+  createSampleDatabase,
+} from "metabase-types/api/mocks/presets";
+import {
+  createMockEmbedOptions,
+  createMockEmbedState,
+  createMockState,
+} from "metabase-types/store/mocks";
+
+import { EmbeddingDataPicker } from "../EmbeddingDataPicker";
+
+interface SetupOpts {
+  hasModels?: boolean;
+  entityTypes?: EmbeddingEntityType[];
+}
+
+const DEFAULT_OPTS: Partial<SetupOpts> = {
+  hasModels: true,
+};
+
+export function setup({
+  hasModels = DEFAULT_OPTS.hasModels,
+  entityTypes,
+}: SetupOpts = {}) {
+  const query = createEmptyQuery();
+
+  fetchMock.get(
+    {
+      name: "entity-count",
+      url: "path:/api/search",
+      query: {
+        models: ["dataset", "table"],
+        limit: 0,
+      },
+    },
+    {
+      total: 100,
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/setup.tsx
user: login: WiNloSt
body: the multi-stage data picker would only be rendered when there are more than 99 (100 and more) combined tables and models, so I need to mock this. And using `setupSearchEndpoints` isn't ideal because I need to create an array with 100 items.
created_at: 2025-03-11T12:33:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+import fetchMock from "fetch-mock";
+
+import {
+  setupDatabasesEndpoints,
+  setupSearchEndpoints,
+} from "__support__/server-mocks";
+import { renderWithProviders } from "__support__/ui";
+import { createMockModelResult } from "metabase/browse/models/test-utils";
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+import type { Query } from "metabase-lib";
+import Question from "metabase-lib/v1/Question";
+import {
+  createOrdersTable,
+  createPeopleTable,
+  createProductsTable,
+  createReviewsTable,
+  createSampleDatabase,
+} from "metabase-types/api/mocks/presets";
+import {
+  createMockEmbedOptions,
+  createMockEmbedState,
+  createMockState,
+} from "metabase-types/store/mocks";
+
+import { EmbeddingDataPicker } from "../EmbeddingDataPicker";
+
+interface SetupOpts {
+  hasModels?: boolean;
+  entityTypes?: EmbeddingEntityType[];
+}
+
+const DEFAULT_OPTS: Partial<SetupOpts> = {
+  hasModels: true,
+};
+
+export function setup({
+  hasModels = DEFAULT_OPTS.hasModels,
+  entityTypes,
+}: SetupOpts = {}) {
+  const query = createEmptyQuery();
+
+  fetchMock.get(
+    {
+      name: "entity-count",
+      url: "path:/api/search",
+      query: {
+        models: ["dataset", "table"],
+        limit: 0,
+      },
+    },
+    {
+      total: 100,
+    },
+  );
+
+  if (hasModels) {
+    setupSearchEndpoints(createSearchResults());
+  } else {
+    setupSearchEndpoints([]);
+  }
+  setupDatabasesEndpoints([createDatabase()]);
+
+  renderWithProviders(
+    <EmbeddingDataPicker
+      query={query}
+      stageIndex={0}
+      canChangeDatabase={true}
+      isDisabled={false}
+      onChange={jest.fn()}
+      placeholder="Pick your starting data"
+      table={undefined}
+    />,
+    entityTypes
+      ? {
+          storeInitialState: createMockState({
+            embed: createMockEmbedState({
+              options: createMockEmbedOptions({
+                entity_types: entityTypes,
+              }),
+            }),
+          }),
+        }
+      : undefined,
+  );
+}
+
+function createDatabase() {
+  return createSampleDatabase({
+    tables: [
+      createOrdersTable(),
+      createPeopleTable(),
+      createProductsTable(),
+      createReviewsTable(),
+    ],
+  });
+}
+
+function createSearchResults() {
+  return [
+    createMockModelResult({
+      id: 1,
+      name: "Orders model",
+    }),
+    createMockModelResult({
+      id: 2,
+      name: "People model",
+    }),
+  ];
+}
+
+function createEmptyQuery(): Query {
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/setup.tsx
user: login: WiNloSt
body: So it's like we're starting a new question instead of rendering a data picker that already have a data source selected.
created_at: 2025-03-11T12:44:50Z
author_association: MEMBER
diff_hunk: @@ -195,165 +186,6 @@ function ModernDataPicker({
   );
 }
 
-type EmbeddingDataPickerProps = {
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/NotebookDataPicker.tsx
user: login: WiNloSt
body: these 2 components are moved to their own file.
created_at: 2025-03-11T12:47:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: I hate this kind of change, I mean this is a rename/move from `frontend/src/metabase/redux/embed.ts`, I'm not creating 142 new lines from scratch.
created_at: 2025-03-11T12:48:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: `font` and `entity_types` are added.
created_at: 2025-03-11T12:50:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,142 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  } as const;
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeCommaSeparatedSearchOptions(["entity_types"]),
+      )(action.payload.search);
+
+      state.options = {
+        ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+        ...searchOptions,
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: instead of picking only allowed options here, I do that as a part of the `searchOptions` pipeline instead.
created_at: 2025-03-11T12:51:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,154 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  } as const;
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeCommaSeparatedSearchOptions(["entity_types"]),
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: I have comments for some of these functions.
created_at: 2025-03-11T12:59:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
path: frontend/src/metabase/redux/embed/embed.unit.spec.ts
user: login: WiNloSt
body: these 3 are the old tests. And the rest are new ones.
created_at: 2025-03-11T13:00:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
+
+    describe("entity_types", () => {
path: frontend/src/metabase/redux/embed/embed.unit.spec.ts
user: login: WiNloSt
body: https://www.notion.so/metabase/Testing-Let-embedders-hide-tables-1ae69354c901807794d2f232d701f037?pvs=4#1b369354c9018074ace3d3ba13070091
created_at: 2025-03-11T13:01:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
+
+    describe("entity_types", () => {
+      it('should accept multiple "entity_types" options', () => {
+        const store = createMockStore();
+
+        store.dispatch(
+          setInitialUrlOptions({
+            search: "entity_types=table&entity_types=model",
+          }),
+        );
+
+        // The default value is `["model", "table"]`, so we know these 2 types are set correctly.
+        expect(store.getState().embed.options.entity_types).toEqual([
+          "table",
+          "model",
path: frontend/src/metabase/redux/embed/embed.unit.spec.ts
user: login: WiNloSt
body: if you still haven't noticed, the values are swapped from the default value (`table` first vs `model` first)
created_at: 2025-03-11T13:01:56Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: I think it's still correct that all the fields are optional though, because they're not valued outside of interactive embedding
created_at: 2025-03-13T11:03:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+import userEvent from "@testing-library/user-event";
+
+import { screen } from "__support__/ui";
+
+import { setup } from "./setup";
+
+describe("EmbeddingDataPicker", () => {
+  describe("multi-stage data picker", () => {
+    it("should show tables when there is no models", async () => {
+      setup({ hasModels: false });
+
+      const databaseOption = await screen.findByText("Sample Database");
+      expect(databaseOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Models")).not.toBeInTheDocument();
+      expect(screen.queryByText("Raw Data")).not.toBeInTheDocument();
+
+      await userEvent.click(databaseOption);
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    it('should show "BUCKET" step when there are both models and tables', async () => {
+      setup();
+
+      expect(await screen.findByText("Models")).toBeInTheDocument();
+      const rawDataOption = screen.getByText("Raw Data");
+      expect(rawDataOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Sample Database")).not.toBeInTheDocument();
+
+      await userEvent.click(rawDataOption);
+      await userEvent.click(screen.getByText("Sample Database"));
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    describe("entity_types", () => {
+      it('should show only models when `entity_types=["models", "table"]`', async () => {
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx
user: login: npretto
body: ```suggestion
      it('should show only models when `entity_types=["models"]`', async () => {
```
is that what you meant? the rest of the test seems to test that
created_at: 2025-03-13T11:39:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
+
+    describe("entity_types", () => {
path: frontend/src/metabase/redux/embed/embed.unit.spec.ts
user: login: heypoom
body: Looks like in the above document we support both of these scenarios:

```
- comma-separated e.g. `entity_types=model,table`
- multiple `entity_types` e.g. `entity_types=table&entity_types=model`
```

In the usage docs, I would encourage people to use comma-separated form since that is less confusing (I think many people don't even know that URL parameters doesn't have to be unique)
created_at: 2025-03-13T12:18:34Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: Hmm, you're right in that case. I actually hate that we do this. Why not just initialize everything, but with default value? Missing properties are harder to deal with imo. Imagine we have 4 different environments, SDK, Interactive embedding, static/public embeding, and internal. Each have their own slice that is not shared, each unique slice would be optional, but they're actually not optional in their respective environment.

Do we need to care if slices that we don't care have default values?

The problem with optional type is that in the code that expects these types, they would be optional, but semantically in such environments, they're not optional at all. This could be misleading, or would require us to type cast it to be `NonNullable` or so.
created_at: 2025-03-13T12:28:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+import { type Dispatch, configureStore } from "@reduxjs/toolkit";
+
+import {
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+  embed as embedReducer,
+  setInitialUrlOptions,
+} from "./embed";
+
+describe("embed reducer", () => {
+  describe("setInitialUrlOptions", () => {
+    it("should set default options", () => {
+      const store = createMockStore();
+
+      store.dispatch(setInitialUrlOptions({ search: "" }));
+
+      expect(store.getState().embed.options).toEqual(
+        DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+      );
+    });
+
+    it("should set options from search", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&new_button=true" }),
+      );
+
+      expect(store.getState().embed.options.top_nav).toBe(false);
+      expect(store.getState().embed.options.new_button).toBe(true);
+    });
+
+    it("should ignore invalid options", () => {
+      const store = createMockStore();
+
+      store.dispatch(
+        setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }),
+      );
+
+      expect(store.getState().embed.options).not.toHaveProperty(
+        "invalid_option",
+      );
+    });
+
+    describe("entity_types", () => {
path: frontend/src/metabase/redux/embed/embed.unit.spec.ts
user: login: WiNloSt
body: true, multiple params are the web standard, though. I think supporting it isn't bad, and it's a by product anyway. We won't be listing it in the doc. As the project only says we support comma-separated value.
created_at: 2025-03-13T12:29:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+import userEvent from "@testing-library/user-event";
+
+import { screen } from "__support__/ui";
+
+import { setup } from "./setup";
+
+describe("EmbeddingDataPicker", () => {
+  describe("multi-stage data picker", () => {
+    it("should show tables when there is no models", async () => {
+      setup({ hasModels: false });
+
+      const databaseOption = await screen.findByText("Sample Database");
+      expect(databaseOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Models")).not.toBeInTheDocument();
+      expect(screen.queryByText("Raw Data")).not.toBeInTheDocument();
+
+      await userEvent.click(databaseOption);
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    it('should show "BUCKET" step when there are both models and tables', async () => {
+      setup();
+
+      expect(await screen.findByText("Models")).toBeInTheDocument();
+      const rawDataOption = screen.getByText("Raw Data");
+      expect(rawDataOption).toBeInTheDocument();
+
+      expect(screen.queryByText("Sample Database")).not.toBeInTheDocument();
+
+      await userEvent.click(rawDataOption);
+      await userEvent.click(screen.getByText("Sample Database"));
+      expect(await screen.findByText("Orders")).toBeInTheDocument();
+      expect(screen.getByText("People")).toBeInTheDocument();
+      expect(screen.getByText("Products")).toBeInTheDocument();
+      expect(screen.getByText("Reviews")).toBeInTheDocument();
+    });
+
+    describe("entity_types", () => {
+      it('should show only models when `entity_types=["models", "table"]`', async () => {
path: frontend/src/metabase/querying/notebook/components/NotebookDataPicker/EmbeddingDataPicker/tests/EmbeddingDataPicker.unit.spec.tsx
user: login: WiNloSt
body: Fixed in 3adfee28c4dbfaf9f72c4e037fc64d07ff68549d

created_at: 2025-03-13T12:32:15Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: > Why not just initialize everything, but with default value?

I think that's because sometimes we consider the presence of them as "value" sometimes

> Missing properties are harder to deal with imo. Imagine we have 4 different environments, SDK, Interactive embedding, static/public embeding, and internal. Each have their own slice that is not shared, each unique slice would be optional, but they're actually not optional in their respective environment."

100% agree with you on this, I think the various contexts (static/public/internal/sdk) should "instruct" the same "feature slice", ie: we should have one place where we set the filters for the entities and all the code should use that, defaulting to the internal/noop behaviour
created_at: 2025-03-13T12:32:58Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: if we were to initialize the default value, we'd put them here
https://github.com/metabase/metabase/blob/3adfee28c4dbfaf9f72c4e037fc64d07ff68549d/frontend/src/metabase/redux/embed/embed.ts#L43

What could be the cons of setting a default value here? I think it could be confusing if people inspect the redux store. On the other hand we don't need to deal with value being nullable in the place where we use them.
created_at: 2025-03-13T12:36:39Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: I think that conceptually they shouldn't be there unless we're in interactive embedding, since the slice is about interactive embedding options. But if the defaults are no-op it probably doesn't hurt and it's actually closer to what I was suggesting above, so this may be a first step in that direction.
We could add a comment so that we know that the default values *must* be no-op


created_at: 2025-03-13T12:48:31Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: Can you elaborate more on the no-op bit? Concretely, if I set the `options` as `DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS` would that be considered a no-op?

https://github.com/metabase/metabase/blob/3adfee28c4dbfaf9f72c4e037fc64d07ff68549d/frontend/src/metabase/redux/embed/embed.ts#L8-L21

I was thinking if we have all propreties, but they could be null, then in the call site, they're not any different than not nullable but being optional.

Practically, we really don't use values from `embed` slice outside interactive embedding (and SDK for now, just for `entity_types`, I might change this in the up-coming tasks), so whatever value we set there doesn't make any difference. We not only don't read, the value, but we don't set the `embed` slice value via actions outside interactive embedding either. We do however, in this case, set the default `entity_types` value for the SDK `embed` slice. In a sense, would this be considered no-op? It doesn't affect other environments outside interactive embedding since we don't read `embed` state.
created_at: 2025-03-13T14:50:04Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: I meant "no-op" as "even if they're used it shouldn't change the behaviour", so if the default entity_types allows everything, as in the core app, then yes
In the code I've seen we manually check that we're in embedding before using those values, so that's not needed, but i still don't like the idea of valuing those context-specific things when we're not in that context, just to make the typing a bit better
We could maybe make some typescript thing and store `isInteractiveEmbed` in the store too, and if that's true than the values need to be present, in that way we could get automatically the right types
created_at: 2025-03-13T14:57:55Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: But still,  i think the "even better" approach is to de-couple the things, if we need to filter entities, then let's have a piece of state for that, instead of the components needing to know about sdk/interactive embedding


created_at: 2025-03-13T15:01:14Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: npretto
body: (I'm not strongly opposing this btw, I'll test the pr locally and approve it after that)
created_at: 2025-03-13T15:33:07Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,17 @@
+import type { EmbeddingEntityType } from "metabase/embedding-sdk/store";
+
 export interface InteractiveEmbeddingOptions {
-  font?: string;
-  top_nav?: boolean;
-  search?: boolean;
-  new_button?: boolean;
-  breadcrumbs?: boolean;
-  logo?: boolean;
-  side_nav?: boolean | "default";
-  header?: boolean;
-  additional_info?: boolean;
-  action_buttons?: boolean;
+  font: string | undefined;
path: frontend/src/metabase-types/store/embed.ts
user: login: WiNloSt
body: I'll mark this as resolved for now, since it's kind of fixed in c5a2531c23f4c1e75ad3724c8f5122f128325568.

I think the type `DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS` it self makes sense to not have optional properties. If we want to refer to a state outside embedding context then we should probably use `EmptyObject` (`Record<string, never>` suggested by TypeScript instead of `{}`) instead, so I don't see the situation where the key could be optional.
created_at: 2025-03-17T10:37:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,154 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  };
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeCommaSeparatedSearchOptions(["entity_types"]),
+      )(action.payload.search);
+
+      state.options = {
+        ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+        ...searchOptions,
+      };
+    },
+    setOptions: (
+      state,
+      action: PayloadAction<Partial<InteractiveEmbeddingOptions>>,
+    ) => {
+      state.options = {
+        ...state.options,
+        ...action.payload,
+      };
+    },
+  },
+});
+
+/**
+ * this functions turns a string like `param=value1,value2` into `param=value1&param=value2` that matches the URLSearchParams format
path: frontend/src/metabase/redux/embed/embed.ts
user: login: romeovs
body: Nit: from reading the function definition that's not really what it does. It only does that for the keys passed as through `options`. Maybe the comment should be clearer?

Additionally, this function seems to only be called once which a specific argument, maybe it doesn't need to be this generic?

Since you are excluding non-option type later on with `excludeNonInteractiveEmbeddingOptions` anyway, maybe this function does not need an input at all and can just normalize all params in the search string?
created_at: 2025-03-17T13:03:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
+     * @param {boolean} [option.isMultiStageDataPicker]
+     * @param {import("metabase-types/store").InteractiveEmbeddingOptions} [option.qs]
+     */
     function startNewEmbeddingQuestion({
       isMultiStageDataPicker = false,
+      qs,
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: romeovs
body: I don't love this parameter name. Maybe we can call it `queryString`?
created_at: 2025-03-17T13:47:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
+     * @param {boolean} [option.isMultiStageDataPicker]
+     * @param {import("metabase-types/store").InteractiveEmbeddingOptions} [option.qs]
+     */
     function startNewEmbeddingQuestion({
       isMultiStageDataPicker = false,
+      qs,
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: WiNloSt
body: that's fair, it's just a passthrough [to Cypress' `visit`](https://docs.cypress.io/api/commands/visit#Arguments) (it uses `qs`), but let's see if that makes things better.
created_at: 2025-03-17T14:24:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,154 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  };
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeCommaSeparatedSearchOptions(["entity_types"]),
+      )(action.payload.search);
+
+      state.options = {
+        ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+        ...searchOptions,
+      };
+    },
+    setOptions: (
+      state,
+      action: PayloadAction<Partial<InteractiveEmbeddingOptions>>,
+    ) => {
+      state.options = {
+        ...state.options,
+        ...action.payload,
+      };
+    },
+  },
+});
+
+/**
+ * this functions turns a string like `param=value1,value2` into `param=value1&param=value2` that matches the URLSearchParams format
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: > maybe this function does not need an input at all and can just normalize all params in the search string?

I'd love to have everything behave consistently. But we only have this new param that could be a list of values, other parameter are only a single value

https://github.com/metabase/metabase/blob/c5a2531c23f4c1e75ad3724c8f5122f128325568/frontend/src/metabase-types/store/embed.ts#L3-L14

so I don't think we should normalize all parameters, we should just specifically do this for `entity_types`

I'll make this non-generic and only normalize comma separated values on `entity_values`
created_at: 2025-03-17T14:29:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,154 @@
+import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { compose, pick } from "underscore";
+
+import { DEFAULT_EMBEDDING_ENTITY_TYPES } from "metabase/embedding-sdk/store";
+import { parseSearchOptions } from "metabase/lib/browser";
+import type { InteractiveEmbeddingOptions } from "metabase-types/store";
+
+export const DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS: InteractiveEmbeddingOptions =
+  {
+    font: undefined,
+    top_nav: true,
+    side_nav: "default",
+    search: false,
+    new_button: false,
+    breadcrumbs: true,
+    logo: true,
+    header: true,
+    additional_info: true,
+    action_buttons: true,
+    entity_types: DEFAULT_EMBEDDING_ENTITY_TYPES,
+  };
+
+const ALLOWED_INTERACTIVE_EMBEDDING_OPTIONS = Object.keys(
+  DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+);
+
+export const urlParameterToBoolean = (
+  urlParameter: string | string[] | boolean | undefined,
+) => {
+  if (urlParameter === undefined) {
+    return undefined;
+  }
+  if (Array.isArray(urlParameter)) {
+    return Boolean(urlParameter.at(-1));
+  } else {
+    return Boolean(urlParameter);
+  }
+};
+
+const interactiveEmbedSlice = createSlice({
+  name: "interactiveEmbed",
+  initialState: {
+    options: {} as InteractiveEmbeddingOptions,
+    isEmbeddingSdk: false,
+  },
+  reducers: {
+    setInitialUrlOptions: (
+      state,
+      action: PayloadAction<{ search: string }>,
+    ) => {
+      const searchOptions = compose(
+        normalizeEntityTypes,
+        excludeNonInteractiveEmbeddingOptions,
+        parseSearchOptions,
+        normalizeCommaSeparatedSearchOptions(["entity_types"]),
+      )(action.payload.search);
+
+      state.options = {
+        ...DEFAULT_INTERACTIVE_EMBEDDING_OPTIONS,
+        ...searchOptions,
+      };
+    },
+    setOptions: (
+      state,
+      action: PayloadAction<Partial<InteractiveEmbeddingOptions>>,
+    ) => {
+      state.options = {
+        ...state.options,
+        ...action.payload,
+      };
+    },
+  },
+});
+
+/**
+ * this functions turns a string like `param=value1,value2` into `param=value1&param=value2` that matches the URLSearchParams format
path: frontend/src/metabase/redux/embed/embed.ts
user: login: WiNloSt
body: addressed in 0977fdd97fbdb7070b8ccadf1f7681b1e31fc7b9

created_at: 2025-03-18T12:02:07Z
author_association: MEMBER
diff_hunk: @@ -772,8 +772,15 @@ describe("scenarios > embedding > full app", () => {
       metric: "Metrics",
     };
 
+    /**
+     *
+     * @param {object} option
+     * @param {boolean} [option.isMultiStageDataPicker]
+     * @param {import("metabase-types/store").InteractiveEmbeddingOptions} [option.qs]
+     */
     function startNewEmbeddingQuestion({
       isMultiStageDataPicker = false,
+      qs,
path: e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js
user: login: WiNloSt
body: addressed in 0977fdd97fbdb7070b8ccadf1f7681b1e31fc7b9

created_at: 2025-03-18T12:02:10Z
author_association: MEMBER

PR comments:
user: login: trunk-io[bot]
created_at: 2025-03-10T10:23:11Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/23655_tests-c5a2531c-purple)](https://app.trunk.io/metabase/flaky-tests/pr/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&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/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&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/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&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/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&utm_source=gh-comment&runConclusion=FAILURE&runId=0ca58776-f7e2-5cd9-9d18-460171d4a1a1) |
| <code>issue 39487 calendar has constant size when using single date picker filter (metabase\#39487)</code> | Unable to find an element with the text "Specific dates" within 4 seconds, possibly due to the text being broken up by multiple elements. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&utm_source=gh-comment&runConclusion=FAILURE&runId=08c825aa-7d47-55cc-ba58-f8685af32c33) |
| <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/54845?repo=metabase/metabase&commitHash=c5a2531c23f4c1e75ad3724c8f5122f128325568&utm_source=gh-comment&runConclusion=FAILURE&runId=53075058-9fa4-573c-ac45-2baefac45d1d) |
<sub>

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

</sub>
author_association: NONE