From a0d9e1383d84362b81fbd9c8df63115970de70dc Mon Sep 17 00:00:00 2001
From: Anton Kulyk <kuliks.anton@gmail.com>
Date: Tue, 13 Dec 2022 10:15:47 +0000
Subject: [PATCH] Cherry pick new data picker component to master (#27174)

* Add basic raw data picker for app scaffolding (#25997)

* Show data app page ID params in editing mode (#25943)

* Data apps layout updates (#25930)

* play with grid sizes to make apps feel appier [ci skip]

* move app nav to bar inside app

* only hide other header buttons outside of edit mode

* tweak overflow issue

* fix header width on data apps

* add control to see app contents

* set data apps layout constants contextually [ci skip]

* remove hardcoded value [ci skip]

* extract conextual header styles [ci skip]

* set max-width on paramaters for data apps [ci skip]

* move data apps nav deeper, only show if not editing [ci skip]

* add spacing to contents trigger, rm old code [ci skip]

* rm table thing for now [ci skip]

* Fixes for data app layout updates (#25962)

* Reorder import

* Fix type

* Fix missing translation, move out URL

* Remove not used import

* Rework `getSelectedItems` utility for data apps

* Fix selected data app page highlighting

* Don't reload app navbar on page change

* Change nav item links look

* Remove `DataAppPageSidebarLink`

* Turn nav into HTML list

* Use styled components

* Fix edit button covered by visualizations

* Fix opening homepage

* Remove redundant prop

Co-authored-by: Anton Kulyk <kuliks.anton@gmail.com>

* First take at parameterized data app page titles (#25938)

* Bring `DataAppContext` back

* Pass `onFocus` and `onBlur` to `EditableText`

* Export `EditableTextProps`

* Add basic `DataAppPageTitle` component

* Use `DataAppPageTitle`

* Fix component name

* Add primitive autocompletion to page title input

* Add `title_template` to `DataAppNavItem` type

* Tweak value management in `DataAppPageTitle`

* Handle `null` values in titles more gracefully

* Don't show suggestions without detail cards

* Don't add whitespace when pasting a token

* Don't update app's collection all the time

* Add data app related selectors to dashboard selectors

* Add redux state to keep title template changes

* Update page title template on page save

* Wire up `DataAppPageTitle` with dashboard code

* Fix unit tests

* Reset state on exiting editing mode

* Add back button to template data picker

* Add `PanePicker` UI component

* Add basic `RawDataPanePicker` component

* displays databases, schemas, tables
* allows picking a database and a schema (no tables yet)

* Allow picking tables

Supports single-selection and multi-selection modes

* Auto-select single database schema

* Use `.ts` instead of `.tsx` for hook file

* Style `PanePicker`

* Add `onTablesChange` callback prop to data picker

* Use new data picker in new app modal

* Use new data picker in add data modal

* Remove `DataAppDataPicker`

Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>

* Make `DataPicker` controlled (#26018)

* Add data type picker to scaffolding data picker (#26019)

* Move `useSelectedTables` one level up

* Extract `DataPickerSelectedItem` type

* Add `type` to `DataPickerValue` type

* Handle `type` in `useDataPickerValue` hook

* Add data type picker step

* Add back button to raw data picker

* Add `CardPicker`

* Tweak spacing

* Move `VirtualTable` to shared picker types

* Temporary disable saved questions data type

* Simplify schema loading for models and questions

* Remove redundant `useMemo`

* Rename `RawDataPanePicker` to `RawDataPicker` (#26020)

* Add safety checks to scaffolding data picker (#26023)

* Pass available data types as a prop to the picker

* Use nested queries settings for data types list

* Offer models only if instance has at least one

* Check data access

* Break `DataPicker` into container and view

* Make `DataPicker` cover the whole container

* Add items filters API to data app scaffolding data picker (#26024)

* Uncomment saved questions data type

* Add data picker filters API types

* Apply data type filters

* Auto pick a data type when only one is available

* Export data picker types

* Add `DataAppScaffoldingDataPicker` component

* Use `DataAppScaffoldingDataPicker`

* Improve collection selection for scaffolding data picker (#26033)

* Add collection ID to data picker value type

* Handle collection ID in `onChange`

* Handle collection ID in `CardPicker`

* Automatically open root collection

* Add basic search to scaffolding data picker (#26039)

* Add data picker context

* Add global search to data picker

* Add basic search UI to new app modal

* Don't use database ID in search

* Scope search depending on selected data type

* Expose more data picker types

* Style search input

* Keep filtered out data types in mind for search

* Move `MIN_SEARCH_LENGTH` to constants file

* Fix can't select nested collection in card picker (#26122)

* Fix conflicts

Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>
---
 .../metadata/utils/saved-questions.js         |   4 +
 .../SelectList/BaseSelectListItem.jsx         |   2 +-
 .../src/metabase/components/tree/Tree.tsx     |   1 +
 .../components/tree/TreeNodeList.styled.tsx   |   3 +
 .../metabase/components/tree/TreeNodeList.tsx |  17 +-
 .../CardPicker/CardPicker.styled.tsx          |   8 +
 .../CardPicker/CardPickerContainer.tsx        | 161 ++++++++++++++++
 .../DataPicker/CardPicker/CardPickerView.tsx  | 128 +++++++++++++
 .../containers/DataPicker/CardPicker/index.ts |   1 +
 .../containers/DataPicker/CardPicker/utils.ts |  78 ++++++++
 .../DataPicker/DataPickerContainer.tsx        | 149 +++++++++++++++
 .../DataPickerContext/DataPickerContext.ts    |  20 ++
 .../DataPickerContextProvider.tsx             |  28 +++
 .../DataPicker/DataPickerContext/index.ts     |   2 +
 .../DataPicker/DataPickerView.styled.tsx      |  13 ++
 .../containers/DataPicker/DataPickerView.tsx  |  89 +++++++++
 .../DataPicker/DataSearch/DataSearch.tsx      | 120 ++++++++++++
 .../containers/DataPicker/DataSearch/index.ts |   1 +
 .../DataTypePicker/DataTypePicker.styled.tsx  |  47 +++++
 .../DataTypePicker/DataTypePicker.tsx         |  59 ++++++
 .../DataPicker/DataTypePicker/index.ts        |   1 +
 .../PanePicker/PanePicker.styled.tsx          |  57 ++++++
 .../DataPicker/PanePicker/PanePicker.tsx      |  50 +++++
 .../containers/DataPicker/PanePicker/index.ts |   1 +
 .../RawDataPicker/RawDataPicker.styled.tsx    |   8 +
 .../RawDataPicker/RawDataPickerContainer.tsx  | 180 ++++++++++++++++++
 .../RawDataPicker/RawDataPickerView.tsx       | 157 +++++++++++++++
 .../DataPicker/RawDataPicker/index.ts         |   1 +
 .../containers/DataPicker/constants.ts        |   1 +
 .../metabase/containers/DataPicker/index.ts   |  11 ++
 .../metabase/containers/DataPicker/types.ts   |  43 +++++
 .../DataPicker/useDataPickerValue.ts          |  68 +++++++
 .../DataPicker/useSelectedTables.ts           |  75 ++++++++
 .../metabase/containers/DataPicker/utils.ts   |  54 ++++++
 .../entities/containers/EntityListLoader.jsx  |   4 +
 .../data-search/SearchResults.jsx             |   2 +-
 .../DataAppDataPicker/DataAppDataPicker.tsx   |  23 ---
 .../components/DataAppDataPicker/index.ts     |   1 -
 .../DataAppScaffoldingDataPicker.tsx          |  20 ++
 .../DataAppScaffoldingDataPicker/index.ts     |   1 +
 .../CreateDataAppModal.styled.tsx             |  44 ++++-
 .../CreateDataAppModal/CreateDataAppModal.tsx |  84 +++++---
 .../ScaffoldDataAppPagesModal.styled.tsx      |   2 +-
 .../ScaffoldDataAppPagesModal.tsx             |  20 +-
 44 files changed, 1777 insertions(+), 62 deletions(-)
 create mode 100644 frontend/src/metabase/components/tree/TreeNodeList.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerContainer.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/CardPicker/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/CardPicker/utils.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerContainer.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContext.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContextProvider.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerContext/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerView.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataPickerView.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataSearch/DataSearch.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataSearch/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/DataTypePicker/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/PanePicker/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerContainer.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx
 create mode 100644 frontend/src/metabase/containers/DataPicker/RawDataPicker/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/constants.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/index.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/types.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/useDataPickerValue.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/useSelectedTables.ts
 create mode 100644 frontend/src/metabase/containers/DataPicker/utils.ts
 delete mode 100644 frontend/src/metabase/writeback/components/DataAppDataPicker/DataAppDataPicker.tsx
 delete mode 100644 frontend/src/metabase/writeback/components/DataAppDataPicker/index.ts
 create mode 100644 frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/DataAppScaffoldingDataPicker.tsx
 create mode 100644 frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/index.ts

diff --git a/frontend/src/metabase-lib/metadata/utils/saved-questions.js b/frontend/src/metabase-lib/metadata/utils/saved-questions.js
index 594418a90e6..1edeb3c6d34 100644
--- a/frontend/src/metabase-lib/metadata/utils/saved-questions.js
+++ b/frontend/src/metabase-lib/metadata/utils/saved-questions.js
@@ -24,6 +24,10 @@ export function getCollectionVirtualSchemaId(collection, { isDatasets } = {}) {
   );
 }
 
+export function getRootCollectionVirtualSchemaId({ isModels }) {
+  return getCollectionVirtualSchemaId(null, { isDatasets: isModels });
+}
+
 export function getQuestionVirtualTableId(card) {
   return `card__${card.id}`;
 }
diff --git a/frontend/src/metabase/components/SelectList/BaseSelectListItem.jsx b/frontend/src/metabase/components/SelectList/BaseSelectListItem.jsx
index da32bcafecc..0b76b63232e 100644
--- a/frontend/src/metabase/components/SelectList/BaseSelectListItem.jsx
+++ b/frontend/src/metabase/components/SelectList/BaseSelectListItem.jsx
@@ -6,7 +6,7 @@ import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
 import { BaseItemRoot } from "./SelectListItem.styled";
 
 const propTypes = {
-  id: PropTypes.string.isRequired,
+  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
   name: PropTypes.string.isRequired,
   onSelect: PropTypes.func.isRequired,
   isSelected: PropTypes.bool,
diff --git a/frontend/src/metabase/components/tree/Tree.tsx b/frontend/src/metabase/components/tree/Tree.tsx
index 03abd09617e..e38b4a0244c 100644
--- a/frontend/src/metabase/components/tree/Tree.tsx
+++ b/frontend/src/metabase/components/tree/Tree.tsx
@@ -73,4 +73,5 @@ function BaseTree({
 
 export const Tree = Object.assign(BaseTree, {
   Node: DefaultTreeNode,
+  NodeList: TreeNodeList,
 });
diff --git a/frontend/src/metabase/components/tree/TreeNodeList.styled.tsx b/frontend/src/metabase/components/tree/TreeNodeList.styled.tsx
new file mode 100644
index 00000000000..2c7a1960c83
--- /dev/null
+++ b/frontend/src/metabase/components/tree/TreeNodeList.styled.tsx
@@ -0,0 +1,3 @@
+import styled from "@emotion/styled";
+
+export const ListRoot = styled.ul``;
diff --git a/frontend/src/metabase/components/tree/TreeNodeList.tsx b/frontend/src/metabase/components/tree/TreeNodeList.tsx
index dd84ac36d41..f371c086a0f 100644
--- a/frontend/src/metabase/components/tree/TreeNodeList.tsx
+++ b/frontend/src/metabase/components/tree/TreeNodeList.tsx
@@ -1,6 +1,9 @@
 import React from "react";
+
 import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
+
 import { ITreeNodeItem, TreeNodeComponent } from "./types";
+import { ListRoot } from "./TreeNodeList.styled";
 
 interface TreeNodeListProps {
   items: ITreeNodeItem[];
@@ -8,14 +11,16 @@ interface TreeNodeListProps {
   selectedId?: ITreeNodeItem["id"];
   depth: number;
   role?: string;
+  className?: string;
   onToggleExpand: (id: ITreeNodeItem["id"]) => void;
   onSelect?: (item: ITreeNodeItem) => void;
   TreeNode: TreeNodeComponent;
 }
 
-export function TreeNodeList({
+function BaseTreeNodeList({
   items,
   role,
+  className,
   expandedIds,
   selectedId,
   depth,
@@ -26,7 +31,7 @@ export function TreeNodeList({
   const selectedRef = useScrollOnMount();
 
   return (
-    <ul role={role}>
+    <ListRoot className={className} role={role}>
       {items.map(item => {
         const isSelected = selectedId === item.id;
         const hasChildren =
@@ -49,7 +54,7 @@ export function TreeNodeList({
               depth={depth}
             />
             {isExpanded && (
-              <TreeNodeList
+              <BaseTreeNodeList
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 items={item.children!}
                 expandedIds={expandedIds}
@@ -63,6 +68,10 @@ export function TreeNodeList({
           </React.Fragment>
         );
       })}
-    </ul>
+    </ListRoot>
   );
 }
+
+export const TreeNodeList = Object.assign(BaseTreeNodeList, {
+  Root: ListRoot,
+});
diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx
new file mode 100644
index 00000000000..98464679d16
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx
@@ -0,0 +1,8 @@
+import styled from "@emotion/styled";
+
+import SelectList from "metabase/components/SelectList";
+
+export const StyledSelectList = styled(SelectList)`
+  width: 100%;
+  padding: 0 1rem;
+`;
diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerContainer.tsx b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerContainer.tsx
new file mode 100644
index 00000000000..1a58178e15a
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerContainer.tsx
@@ -0,0 +1,161 @@
+import React, { useCallback, useMemo } from "react";
+import _ from "underscore";
+import { connect } from "react-redux";
+
+import Collections from "metabase/entities/collections";
+import Schemas from "metabase/entities/schemas";
+
+import { getUser } from "metabase/selectors/user";
+
+import type { Collection, User } from "metabase-types/api";
+import type { State } from "metabase-types/store";
+import type Table from "metabase-lib/metadata/Table";
+import type Schema from "metabase-lib/metadata/Schema";
+
+import { getCollectionVirtualSchemaId } from "metabase-lib/metadata/utils/saved-questions";
+
+import type {
+  DataPickerProps,
+  DataPickerSelectedItem,
+  VirtualTable,
+} from "../types";
+import useSelectedTables from "../useSelectedTables";
+
+import { buildCollectionTree } from "./utils";
+
+import CardPickerView from "./CardPickerView";
+
+interface CardPickerOwnProps extends DataPickerProps {
+  targetModel: "model" | "question";
+  onBack?: () => void;
+}
+
+interface CardPickerStateProps {
+  currentUser: User;
+}
+
+interface CollectionsLoaderProps {
+  collectionTree: Collection[];
+  collections: Collection[];
+  rootCollection: Collection;
+}
+
+interface SchemaLoaderProps {
+  schema?: Schema & { tables: VirtualTable[] };
+}
+
+type CardPickerProps = CardPickerOwnProps &
+  CardPickerStateProps &
+  CollectionsLoaderProps &
+  SchemaLoaderProps;
+
+function mapStateToProps(state: State) {
+  return {
+    currentUser: getUser(state),
+  };
+}
+
+function CardPickerContainer({
+  value,
+  collections,
+  collectionTree,
+  rootCollection,
+  schema: selectedSchema,
+  currentUser,
+  targetModel,
+  onChange,
+  onBack,
+}: CardPickerProps) {
+  const { collectionId } = value;
+
+  const { selectedTableIds, toggleTableIdSelection } = useSelectedTables({
+    initialValues: value.tableIds,
+    mode: "multiple",
+  });
+
+  const collectionsMap = useMemo(
+    () => _.indexBy(collections, "id"),
+    [collections],
+  );
+
+  const tree = useMemo(
+    () =>
+      buildCollectionTree({
+        collections: collectionTree,
+        rootCollection,
+        currentUser,
+        targetModel,
+      }),
+    [collectionTree, rootCollection, currentUser, targetModel],
+  );
+
+  const selectedItems = useMemo(() => {
+    const items: DataPickerSelectedItem[] = [];
+
+    if (collectionId) {
+      items.push({ type: "collection", id: collectionId });
+    }
+
+    const tables: DataPickerSelectedItem[] = selectedTableIds.map(id => ({
+      type: "table",
+      id,
+    }));
+
+    items.push(...tables);
+
+    return items;
+  }, [collectionId, selectedTableIds]);
+
+  const handleSelectedCollectionChange = useCallback(
+    (id: Collection["id"]) => {
+      const collection = id === "root" ? rootCollection : collectionsMap[id];
+      if (collection) {
+        const schemaId = getCollectionVirtualSchemaId(collection, {
+          isDatasets: targetModel === "model",
+        });
+        onChange({ ...value, schemaId, collectionId: id, tableIds: [] });
+      }
+    },
+    [value, collectionsMap, rootCollection, targetModel, onChange],
+  );
+
+  const handleSelectedTablesChange = useCallback(
+    (tableId: Table["id"]) => {
+      const tableIds = toggleTableIdSelection(tableId);
+      onChange({ ...value, tableIds });
+    },
+    [value, toggleTableIdSelection, onChange],
+  );
+
+  return (
+    <CardPickerView
+      collectionTree={tree}
+      virtualTables={selectedSchema?.tables}
+      selectedItems={selectedItems}
+      targetModel={targetModel}
+      onSelectCollection={handleSelectedCollectionChange}
+      onSelectedVirtualTable={handleSelectedTablesChange}
+      onBack={onBack}
+    />
+  );
+}
+
+export default _.compose(
+  Collections.load({
+    id: "root",
+    entityAlias: "rootCollection",
+    loadingAndErrorWrapper: false,
+  }),
+  Collections.loadList({
+    query: () => ({ tree: true }),
+    listName: "collectionTree",
+  }),
+  Collections.loadList({
+    listName: "collections",
+  }),
+  Schemas.load({
+    id: (state: State, props: CardPickerOwnProps) => props.value.schemaId,
+    loadingAndErrorWrapper: false,
+  }),
+  connect(mapStateToProps),
+)(CardPickerContainer);
diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx
new file mode 100644
index 00000000000..c009227033b
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx
@@ -0,0 +1,128 @@
+import React, { useCallback, useMemo } from "react";
+import _ from "underscore";
+
+import SelectList from "metabase/components/SelectList";
+
+import { canonicalCollectionId } from "metabase/collections/utils";
+
+import type { ITreeNodeItem } from "metabase/components/tree/types";
+import type { Collection } from "metabase-types/api";
+import type Table from "metabase-lib/metadata/Table";
+
+import type { DataPickerSelectedItem, VirtualTable } from "../types";
+import PanePicker from "../PanePicker";
+
+import { StyledSelectList } from "./CardPicker.styled";
+
+type TargetModel = "model" | "question";
+
+interface CardPickerViewProps {
+  collectionTree: ITreeNodeItem[];
+  virtualTables?: VirtualTable[];
+  selectedItems: DataPickerSelectedItem[];
+  targetModel: TargetModel;
+  onSelectCollection: (id: Collection["id"]) => void;
+  onSelectedVirtualTable: (id: Table["id"]) => void;
+  onBack?: () => void;
+}
+
+function getTableIcon({
+  isSelected,
+  targetModel,
+}: {
+  isSelected: boolean;
+  targetModel: TargetModel;
+}) {
+  if (isSelected) {
+    return "check";
+  }
+  return targetModel === "model" ? "model" : "table2";
+}
+
+function TableSelectListItem({
+  table,
+  targetModel,
+  isSelected,
+  onSelect,
+}: {
+  table: VirtualTable;
+  targetModel: "model" | "question";
+  isSelected: boolean;
+  onSelect: (id: Table["id"]) => void;
+}) {
+  return (
+    <SelectList.Item
+      id={table.id}
+      name={table.display_name}
+      isSelected={isSelected}
+      icon={getTableIcon({ isSelected, targetModel })}
+      onSelect={onSelect}
+    >
+      {table.display_name}
+    </SelectList.Item>
+  );
+}
+
+function formatCollectionId(id: string | number | null) {
+  const canonicalId = canonicalCollectionId(id);
+  return canonicalId === null ? "root" : canonicalId;
+}
+
+function CardPickerView({
+  collectionTree,
+  virtualTables,
+  selectedItems,
+  targetModel,
+  onSelectCollection,
+  onSelectedVirtualTable,
+  onBack,
+}: CardPickerViewProps) {
+  const { selectedCollectionId, selectedVirtualTableIds } = useMemo(() => {
+    const { collection: collections = [], table: tables = [] } = _.groupBy(
+      selectedItems,
+      "type",
+    );
+
+    const [collection] = collections;
+
+    return {
+      selectedCollectionId: collection?.id,
+      selectedVirtualTableIds: tables.map(table => table.id),
+    };
+  }, [selectedItems]);
+
+  const handlePanePickerSelect = useCallback(
+    (item: ITreeNodeItem) => {
+      onSelectCollection(formatCollectionId(item.id));
+    },
+    [onSelectCollection],
+  );
+
+  const renderVirtualTable = useCallback(
+    (table: VirtualTable) => (
+      <TableSelectListItem
+        key={table.id}
+        table={table}
+        targetModel={targetModel}
+        isSelected={selectedVirtualTableIds.includes(table.id)}
+        onSelect={onSelectedVirtualTable}
+      />
+    ),
+    [selectedVirtualTableIds, targetModel, onSelectedVirtualTable],
+  );
+
+  return (
+    <PanePicker
+      data={collectionTree}
+      selectedId={selectedCollectionId}
+      onSelect={handlePanePickerSelect}
+      onBack={onBack}
+    >
+      <StyledSelectList>
+        {virtualTables?.map?.(renderVirtualTable)}
+      </StyledSelectList>
+    </PanePicker>
+  );
+}
+
+export default CardPickerView;
diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/index.ts b/frontend/src/metabase/containers/DataPicker/CardPicker/index.ts
new file mode 100644
index 00000000000..35e03aa494d
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/CardPicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./CardPickerContainer";
diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/utils.ts b/frontend/src/metabase/containers/DataPicker/CardPicker/utils.ts
new file mode 100644
index 00000000000..70e2428047c
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/CardPicker/utils.ts
@@ -0,0 +1,78 @@
+import _ from "underscore";
+
+import {
+  PERSONAL_COLLECTIONS,
+  buildCollectionTree as _buildCollectionTree,
+} from "metabase/entities/collections";
+import {
+  isPersonalCollection,
+  nonPersonalOrArchivedCollection,
+  currentUserPersonalCollections,
+} from "metabase/collections/utils";
+
+import type {
+  Collection,
+  CollectionContentModel,
+  User,
+} from "metabase-types/api";
+
+function getOurAnalyticsCollection(collectionEntity: any) {
+  return {
+    ...collectionEntity,
+    schemaName: "Everything else",
+    icon: "folder",
+  };
+}
+
+const ALL_PERSONAL_COLLECTIONS_ROOT = {
+  ...PERSONAL_COLLECTIONS,
+};
+
+export function buildCollectionTree({
+  collections,
+  rootCollection,
+  currentUser,
+  targetModel = "question",
+}: {
+  collections: Collection[];
+  rootCollection: Collection;
+  currentUser: User;
+  targetModel?: "model" | "question";
+}) {
+  const preparedCollections: Collection[] = [];
+  const userPersonalCollections = currentUserPersonalCollections(
+    collections,
+    currentUser.id,
+  );
+  const nonPersonalOrArchivedCollections = collections.filter(
+    nonPersonalOrArchivedCollection,
+  );
+
+  preparedCollections.push(...userPersonalCollections);
+  preparedCollections.push(...nonPersonalOrArchivedCollections);
+
+  if (currentUser.is_superuser) {
+    const otherPersonalCollections = collections.filter(
+      collection =>
+        isPersonalCollection(collection) &&
+        collection.personal_owner_id !== currentUser.id,
+    );
+
+    if (otherPersonalCollections.length > 0) {
+      preparedCollections.push({
+        ...ALL_PERSONAL_COLLECTIONS_ROOT,
+        children: otherPersonalCollections,
+      } as Collection);
+    }
+  }
+
+  const targetModels: CollectionContentModel[] =
+    targetModel === "model" ? ["dataset"] : ["card"];
+  const tree = _buildCollectionTree(preparedCollections, { targetModels });
+
+  if (rootCollection) {
+    tree.unshift(getOurAnalyticsCollection(rootCollection));
+  }
+
+  return tree;
+}
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerContainer.tsx b/frontend/src/metabase/containers/DataPicker/DataPickerContainer.tsx
new file mode 100644
index 00000000000..548803224ef
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerContainer.tsx
@@ -0,0 +1,149 @@
+import React, { useCallback, useMemo } from "react";
+import { connect } from "react-redux";
+import _ from "underscore";
+
+import { getHasDataAccess } from "metabase/selectors/data";
+import { getSetting } from "metabase/selectors/settings";
+
+import { useOnMount } from "metabase/hooks/use-on-mount";
+
+import Databases from "metabase/entities/databases";
+import Search from "metabase/entities/search";
+
+import type { State } from "metabase-types/store";
+
+import {
+  getRootCollectionVirtualSchemaId,
+  SAVED_QUESTIONS_VIRTUAL_DB_ID,
+} from "metabase-lib/metadata/utils/saved-questions";
+
+import type {
+  DataPickerProps as DataPickerOwnProps,
+  DataPickerDataType,
+} from "./types";
+
+import { DataPickerContextProvider, useDataPicker } from "./DataPickerContext";
+import { getDataTypes, DEFAULT_DATA_PICKER_FILTERS } from "./utils";
+
+import DataPickerView from "./DataPickerView";
+
+interface DataPickerStateProps {
+  hasNestedQueriesEnabled: boolean;
+  hasDataAccess: boolean;
+}
+
+interface SearchListLoaderProps {
+  search: unknown[];
+}
+
+type DataPickerProps = DataPickerOwnProps &
+  DataPickerStateProps &
+  SearchListLoaderProps;
+
+function mapStateToProps(state: State) {
+  return {
+    hasNestedQueriesEnabled: getSetting(state, "enable-nested-queries"),
+    hasDataAccess: getHasDataAccess(state),
+  };
+}
+
+function DataPicker({
+  search: modelLookupResult,
+  filters: customFilters = {},
+  hasNestedQueriesEnabled,
+  hasDataAccess,
+  ...props
+}: DataPickerProps) {
+  const { onChange } = props;
+
+  const { search } = useDataPicker();
+
+  const filters = useMemo(
+    () => ({
+      ...DEFAULT_DATA_PICKER_FILTERS,
+      ...customFilters,
+    }),
+    [customFilters],
+  );
+
+  const dataTypes = useMemo(
+    () =>
+      getDataTypes({
+        hasModels: modelLookupResult.length > 0,
+        hasNestedQueriesEnabled,
+      }).filter(type => filters.types(type.id)),
+    [filters, modelLookupResult, hasNestedQueriesEnabled],
+  );
+
+  const handleDataTypeChange = useCallback(
+    (type: DataPickerDataType) => {
+      const isModels = type === "models";
+      const isUsingVirtualTables = isModels || type === "questions";
+
+      // When switching to models or questions,
+      // we want to automatically open Our analytics collection
+      const databaseId = isUsingVirtualTables
+        ? SAVED_QUESTIONS_VIRTUAL_DB_ID
+        : undefined;
+      const schemaId = isUsingVirtualTables
+        ? getRootCollectionVirtualSchemaId({ isModels })
+        : undefined;
+      const collectionId = isUsingVirtualTables ? "root" : undefined;
+
+      onChange({
+        type,
+        databaseId,
+        schemaId,
+        collectionId,
+        tableIds: [],
+      });
+    },
+    [onChange],
+  );
+
+  useOnMount(() => {
+    if (dataTypes.length === 1) {
+      handleDataTypeChange(dataTypes[0].id);
+    }
+  });
+
+  const handleBack = useCallback(() => {
+    onChange({
+      type: undefined,
+      databaseId: undefined,
+      schemaId: undefined,
+      tableIds: [],
+    });
+  }, [onChange]);
+
+  return (
+    <DataPickerView
+      {...props}
+      dataTypes={dataTypes}
+      searchQuery={search.query}
+      hasDataAccess={hasDataAccess}
+      onDataTypeChange={handleDataTypeChange}
+      onBack={handleBack}
+    />
+  );
+}
+
+const DataPickerContainer = _.compose(
+  // Required for `hasDataAccess` check
+  Databases.loadList(),
+
+  // Lets the picker check there is
+  // at least one model, to offer for selection
+  Search.loadList({
+    query: {
+      models: "dataset",
+      limit: 1,
+    },
+  }),
+
+  connect(mapStateToProps),
+)(DataPicker);
+
+export default Object.assign(DataPickerContainer, {
+  Provider: DataPickerContextProvider,
+});
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContext.ts b/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContext.ts
new file mode 100644
index 00000000000..8b8e0d84950
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContext.ts
@@ -0,0 +1,20 @@
+import { createContext, useContext } from "react";
+import _ from "underscore";
+
+export interface IDataPickerContext {
+  search: {
+    query: string;
+    setQuery: (query: string) => void;
+  };
+}
+
+export const DataPickerContext = createContext<IDataPickerContext>({
+  search: {
+    query: "",
+    setQuery: _.noop,
+  },
+});
+
+export function useDataPicker() {
+  return useContext(DataPickerContext);
+}
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContextProvider.tsx b/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContextProvider.tsx
new file mode 100644
index 00000000000..419af8eed7f
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerContext/DataPickerContextProvider.tsx
@@ -0,0 +1,28 @@
+import React, { useMemo, useState } from "react";
+import { DataPickerContext, IDataPickerContext } from "./DataPickerContext";
+
+function DataPickerContextProvider({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  const [searchQuery, setSearchQuery] = useState("");
+
+  const value: IDataPickerContext = useMemo(
+    () => ({
+      search: {
+        query: searchQuery,
+        setQuery: setSearchQuery,
+      },
+    }),
+    [searchQuery],
+  );
+
+  return (
+    <DataPickerContext.Provider value={value}>
+      {children}
+    </DataPickerContext.Provider>
+  );
+}
+
+export default DataPickerContextProvider;
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerContext/index.ts b/frontend/src/metabase/containers/DataPicker/DataPickerContext/index.ts
new file mode 100644
index 00000000000..89c2831f5f6
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerContext/index.ts
@@ -0,0 +1,2 @@
+export { useDataPicker } from "./DataPickerContext";
+export { default as DataPickerContextProvider } from "./DataPickerContextProvider";
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerView.styled.tsx b/frontend/src/metabase/containers/DataPicker/DataPickerView.styled.tsx
new file mode 100644
index 00000000000..563b43aea8c
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerView.styled.tsx
@@ -0,0 +1,13 @@
+import styled from "@emotion/styled";
+
+export const Root = styled.div`
+  display: flex;
+  flex: 1;
+`;
+
+export const EmptyStateContainer = styled.div`
+  display: flex;
+  flex: 1;
+  align-items: center;
+  justify-content: center;
+`;
diff --git a/frontend/src/metabase/containers/DataPicker/DataPickerView.tsx b/frontend/src/metabase/containers/DataPicker/DataPickerView.tsx
new file mode 100644
index 00000000000..fedf1acdc10
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataPickerView.tsx
@@ -0,0 +1,89 @@
+import React, { useMemo } from "react";
+import { t } from "ttag";
+
+import EmptyState from "metabase/components/EmptyState";
+
+import { MIN_SEARCH_LENGTH } from "./constants";
+
+import type { DataPickerProps, DataPickerDataType } from "./types";
+import type { DataTypeInfoItem } from "./utils";
+
+import CardPicker from "./CardPicker";
+import DataTypePicker from "./DataTypePicker";
+import DataSearch from "./DataSearch";
+import RawDataPicker from "./RawDataPicker";
+
+import { Root, EmptyStateContainer } from "./DataPickerView.styled";
+
+interface DataPickerViewProps extends DataPickerProps {
+  dataTypes: DataTypeInfoItem[];
+  searchQuery: string;
+  hasDataAccess: boolean;
+  onDataTypeChange: (type: DataPickerDataType) => void;
+  onBack?: () => void;
+}
+
+function DataPickerViewContent({
+  dataTypes,
+  searchQuery,
+  hasDataAccess,
+  onDataTypeChange,
+  ...props
+}: DataPickerViewProps) {
+  const { value, onChange } = props;
+
+  const availableDataTypes = useMemo(
+    () => dataTypes.map(type => type.id),
+    [dataTypes],
+  );
+
+  if (!hasDataAccess) {
+    return (
+      <EmptyStateContainer>
+        <EmptyState
+          message={t`To pick some data, you'll need to add some first`}
+          icon="database"
+        />
+      </EmptyStateContainer>
+    );
+  }
+
+  if (searchQuery.trim().length > MIN_SEARCH_LENGTH) {
+    return (
+      <DataSearch
+        value={value}
+        searchQuery={searchQuery}
+        availableDataTypes={availableDataTypes}
+        onChange={onChange}
+      />
+    );
+  }
+
+  if (!value.type) {
+    return <DataTypePicker types={dataTypes} onChange={onDataTypeChange} />;
+  }
+
+  if (value.type === "raw-data") {
+    return <RawDataPicker {...props} />;
+  }
+
+  if (value.type === "models") {
+    return <CardPicker {...props} targetModel="model" />;
+  }
+
+  if (value.type === "questions") {
+    return <CardPicker {...props} targetModel="question" />;
+  }
+
+  return null;
+}
+
+function DataPickerView(props: DataPickerViewProps) {
+  return (
+    <Root>
+      <DataPickerViewContent {...props} />
+    </Root>
+  );
+}
+
+export default DataPickerView;
diff --git a/frontend/src/metabase/containers/DataPicker/DataSearch/DataSearch.tsx b/frontend/src/metabase/containers/DataPicker/DataSearch/DataSearch.tsx
new file mode 100644
index 00000000000..f4e93c90ffa
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataSearch/DataSearch.tsx
@@ -0,0 +1,120 @@
+import React, { useCallback, useMemo } from "react";
+
+import { SearchResults } from "metabase/query_builder/components/DataSelector/data-search";
+
+import type { Collection } from "metabase-types/api";
+
+import {
+  getCollectionVirtualSchemaId,
+  getQuestionVirtualTableId,
+  SAVED_QUESTIONS_VIRTUAL_DB_ID,
+} from "metabase-lib/metadata/utils/saved-questions";
+import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
+
+import { useDataPicker } from "../DataPickerContext";
+
+import type { DataPickerValue, DataPickerDataType } from "../types";
+
+interface DataSearchProps {
+  value: DataPickerValue;
+  searchQuery: string;
+  availableDataTypes: DataPickerDataType[];
+  onChange: (value: DataPickerValue) => void;
+}
+
+type TableSearchResult = {
+  database_id: number;
+  table_schema: string;
+  table_id: number;
+  model: "table" | "dataset" | "card";
+  collection: Collection | null;
+};
+
+type SearchModel = "card" | "dataset" | "table";
+
+const DATA_TYPE_SEARCH_MODEL_MAP: Record<DataPickerDataType, SearchModel> = {
+  "raw-data": "table",
+  models: "dataset",
+  questions: "card",
+};
+
+function getDataTypeForSearchResult(
+  table: TableSearchResult,
+): DataPickerDataType {
+  switch (table.model) {
+    case "table":
+      return "raw-data";
+    case "card":
+      return "questions";
+    case "dataset":
+      return "models";
+  }
+}
+
+function getValueForRawTable(table: TableSearchResult): DataPickerValue {
+  return {
+    type: "raw-data",
+    databaseId: table.database_id,
+    schemaId: generateSchemaId(table.database_id, table.table_schema),
+    collectionId: undefined,
+    tableIds: [table.table_id],
+  };
+}
+
+function getValueForVirtualTable(table: TableSearchResult): DataPickerValue {
+  const type = getDataTypeForSearchResult(table);
+  const schemaId = getCollectionVirtualSchemaId(table.collection, {
+    isDatasets: type === "models",
+  });
+  return {
+    type: "models",
+    databaseId: SAVED_QUESTIONS_VIRTUAL_DB_ID,
+    schemaId,
+    collectionId: table.collection?.id || "root",
+    tableIds: [getQuestionVirtualTableId(table)],
+  };
+}
+
+function getNextValue(table: TableSearchResult): DataPickerValue {
+  const type = getDataTypeForSearchResult(table);
+  const isVirtualTable = type === "models" || type === "questions";
+  return isVirtualTable
+    ? getValueForVirtualTable(table)
+    : getValueForRawTable(table);
+}
+
+function DataSearch({
+  value,
+  searchQuery,
+  availableDataTypes,
+  onChange,
+}: DataSearchProps) {
+  const { search } = useDataPicker();
+  const { setQuery } = search;
+
+  const searchModels: SearchModel[] = useMemo(() => {
+    if (!value.type) {
+      return availableDataTypes.map(type => DATA_TYPE_SEARCH_MODEL_MAP[type]);
+    }
+    return [DATA_TYPE_SEARCH_MODEL_MAP[value.type]];
+  }, [value.type, availableDataTypes]);
+
+  const onSelect = useCallback(
+    (table: TableSearchResult) => {
+      const nextValue = getNextValue(table);
+      onChange(nextValue);
+      setQuery("");
+    },
+    [onChange, setQuery],
+  );
+
+  return (
+    <SearchResults
+      searchModels={searchModels}
+      searchQuery={searchQuery.trim()}
+      onSelect={onSelect}
+    />
+  );
+}
+
+export default DataSearch;
diff --git a/frontend/src/metabase/containers/DataPicker/DataSearch/index.ts b/frontend/src/metabase/containers/DataPicker/DataSearch/index.ts
new file mode 100644
index 00000000000..79f6d02b6e8
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataSearch/index.ts
@@ -0,0 +1 @@
+export { default } from "./DataSearch";
diff --git a/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.styled.tsx
new file mode 100644
index 00000000000..16b5a3ba021
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.styled.tsx
@@ -0,0 +1,47 @@
+import styled from "@emotion/styled";
+
+import Icon from "metabase/components/Icon";
+import SelectList from "metabase/components/SelectList";
+
+import { color } from "metabase/lib/colors";
+import { space } from "metabase/styled-components/theme";
+
+export const List = styled(SelectList)`
+  padding: ${space(0)} ${space(1)} 12px ${space(1)};
+`;
+
+export const ItemIcon = styled(Icon)`
+  color: ${color("text-dark")};
+`;
+
+export const TitleContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const ItemTitle = styled.span`
+  color: ${color("text-dark")};
+  font-weight: 700;
+  font-size: 14px;
+  margin-left: ${space(1)};
+`;
+
+export const ItemDescriptionContainer = styled.div`
+  margin-top: ${space(0)};
+`;
+
+export const ItemDescription = styled.span`
+  color: ${color("text-light")};
+  font-weight: 700;
+  font-size: 12px;
+`;
+
+export const ItemContainer = styled(SelectList.BaseItem as any)`
+  &:hover {
+    ${ItemIcon},
+    ${ItemTitle},
+    ${ItemDescription} {
+      color: ${color("text-white")};
+    }
+  }
+`;
diff --git a/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.tsx b/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.tsx
new file mode 100644
index 00000000000..d3ee7d2b0c5
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataTypePicker/DataTypePicker.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+
+import type { DataTypeInfoItem } from "../utils";
+import type { DataPickerDataType } from "../types";
+
+import {
+  List,
+  ItemContainer,
+  TitleContainer,
+  ItemIcon,
+  ItemTitle,
+  ItemDescriptionContainer,
+  ItemDescription,
+} from "./DataTypePicker.styled";
+
+interface DataTypePickerProps {
+  types: DataTypeInfoItem[];
+  onChange: (value: DataPickerDataType) => void;
+}
+
+interface ListItemProps extends DataTypeInfoItem {
+  onSelect: () => void;
+}
+
+function DataTypePickerListItem({
+  id,
+  name,
+  icon,
+  description,
+  onSelect,
+}: ListItemProps) {
+  return (
+    <ItemContainer id={id} name={name} onSelect={onSelect}>
+      <TitleContainer>
+        <ItemIcon name={icon} size={18} />
+        <ItemTitle>{name}</ItemTitle>
+      </TitleContainer>
+      <ItemDescriptionContainer>
+        <ItemDescription>{description}</ItemDescription>
+      </ItemDescriptionContainer>
+    </ItemContainer>
+  );
+}
+
+function DataTypePicker({ types, onChange }: DataTypePickerProps) {
+  return (
+    <List>
+      {types.map(dataType => (
+        <DataTypePickerListItem
+          {...dataType}
+          key={dataType.id}
+          onSelect={() => onChange(dataType.id)}
+        />
+      ))}
+    </List>
+  );
+}
+
+export default DataTypePicker;
diff --git a/frontend/src/metabase/containers/DataPicker/DataTypePicker/index.ts b/frontend/src/metabase/containers/DataPicker/DataTypePicker/index.ts
new file mode 100644
index 00000000000..e148b996fab
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/DataTypePicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./DataTypePicker";
diff --git a/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.styled.tsx
new file mode 100644
index 00000000000..01edf4030bb
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.styled.tsx
@@ -0,0 +1,57 @@
+import styled from "@emotion/styled";
+
+import { Tree } from "metabase/components/tree";
+
+import { color } from "metabase/lib/colors";
+
+import { breakpointMaxSmall } from "metabase/styled-components/theme/media-queries";
+
+export const Root = styled.div`
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+
+  ${breakpointMaxSmall} {
+    flex-direction: column;
+    overflow: auto;
+  }
+`;
+
+export const LeftPaneContainer = styled.div`
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  overflow: auto;
+
+  border-right: 1px solid ${color("border")};
+
+  ${Tree.Node.Root} {
+    border-radius: 6px;
+  }
+
+  ${Tree.NodeList.Root} {
+    padding: 0 1rem;
+  }
+`;
+
+export const BackButton = styled.a`
+  display: flex;
+  align-items: center;
+
+  color: ${color("text-dark")};
+  font-weight: 700;
+
+  margin-left: 1rem;
+  padding-bottom: 1rem;
+
+  &:hover {
+    color: ${color("brand")};
+  }
+`;
+
+export const TreeContainer = styled.div``;
+
+export const RightPaneContainer = styled.div`
+  display: flex;
+  flex: 1;
+`;
diff --git a/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.tsx b/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.tsx
new file mode 100644
index 00000000000..e3232f587dc
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/PanePicker/PanePicker.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import { t } from "ttag";
+
+import Icon from "metabase/components/Icon";
+import { Tree } from "metabase/components/tree";
+
+import type { ITreeNodeItem } from "metabase/components/tree/types";
+
+import {
+  Root,
+  LeftPaneContainer,
+  TreeContainer,
+  BackButton,
+  RightPaneContainer,
+} from "./PanePicker.styled";
+
+interface PanePickerProps {
+  data: ITreeNodeItem[];
+  selectedId?: ITreeNodeItem["id"];
+  onSelect: (item: ITreeNodeItem) => void;
+  onBack?: () => void;
+  children?: React.ReactNode;
+}
+
+function PanePicker({
+  data,
+  selectedId,
+  onSelect,
+  onBack,
+  children,
+}: PanePickerProps) {
+  return (
+    <Root>
+      <LeftPaneContainer>
+        {onBack && (
+          <BackButton onClick={onBack}>
+            <Icon name="chevronleft" className="mr1" />
+            {t`Back`}
+          </BackButton>
+        )}
+        <TreeContainer>
+          <Tree selectedId={selectedId} data={data} onSelect={onSelect} />
+        </TreeContainer>
+      </LeftPaneContainer>
+      <RightPaneContainer>{children}</RightPaneContainer>
+    </Root>
+  );
+}
+
+export default PanePicker;
diff --git a/frontend/src/metabase/containers/DataPicker/PanePicker/index.ts b/frontend/src/metabase/containers/DataPicker/PanePicker/index.ts
new file mode 100644
index 00000000000..deef2443d79
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/PanePicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./PanePicker";
diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx
new file mode 100644
index 00000000000..98464679d16
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx
@@ -0,0 +1,8 @@
+import styled from "@emotion/styled";
+
+import SelectList from "metabase/components/SelectList";
+
+export const StyledSelectList = styled(SelectList)`
+  width: 100%;
+  padding: 0 1rem;
+`;
diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerContainer.tsx b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerContainer.tsx
new file mode 100644
index 00000000000..77a70e39306
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerContainer.tsx
@@ -0,0 +1,180 @@
+import React, { useCallback, useMemo } from "react";
+import _ from "underscore";
+
+import Databases from "metabase/entities/databases";
+import Schemas from "metabase/entities/schemas";
+import Tables from "metabase/entities/tables";
+
+import type Database from "metabase-lib/metadata/Database";
+import type Table from "metabase-lib/metadata/Table";
+
+import { DataPickerProps, DataPickerSelectedItem } from "../types";
+
+import useSelectedTables from "../useSelectedTables";
+
+import RawDataPickerView from "./RawDataPickerView";
+
+interface DatabaseListLoaderProps {
+  databases: Database[];
+}
+
+interface TableListLoaderProps {
+  tables: Table[];
+}
+
+interface RawDataPickerOwnProps extends DataPickerProps {
+  onBack?: () => void;
+}
+
+type RawDataPickerProps = RawDataPickerOwnProps & DatabaseListLoaderProps;
+
+function RawDataPicker({
+  value,
+  databases,
+  onChange,
+  onBack,
+}: RawDataPickerProps) {
+  const { databaseId: selectedDatabaseId, schemaId: selectedSchemaId } = value;
+
+  const { selectedTableIds, toggleTableIdSelection } = useSelectedTables({
+    initialValues: value.tableIds,
+    mode: "multiple",
+  });
+
+  const selectedDatabase = useMemo(() => {
+    if (!selectedDatabaseId) {
+      return;
+    }
+    return databases.find(db => db.id === selectedDatabaseId);
+  }, [databases, selectedDatabaseId]);
+
+  const selectedSchema = useMemo(() => {
+    if (!selectedDatabase) {
+      return;
+    }
+    const schemas = selectedDatabase.getSchemas();
+    return schemas.find(schema => schema.id === selectedSchemaId);
+  }, [selectedDatabase, selectedSchemaId]);
+
+  const selectedItems = useMemo(() => {
+    const items: DataPickerSelectedItem[] = [];
+
+    if (selectedDatabaseId) {
+      items.push({ type: "database", id: selectedDatabaseId });
+    }
+
+    if (selectedSchemaId) {
+      items.push({ type: "schema", id: selectedSchemaId });
+    }
+
+    const tables: DataPickerSelectedItem[] = selectedTableIds.map(id => ({
+      type: "table",
+      id,
+    }));
+
+    items.push(...tables);
+
+    return items;
+  }, [selectedDatabaseId, selectedSchemaId, selectedTableIds]);
+
+  const handleSelectedSchemaIdChange = useCallback(
+    (schemaId?: string) => {
+      onChange({ ...value, schemaId, tableIds: [] });
+    },
+    [value, onChange],
+  );
+
+  const handleSelectedDatabaseIdChange = useCallback(
+    (databaseId: Database["id"]) => {
+      const database = databases.find(db => db.id === databaseId);
+      if (!database) {
+        return;
+      }
+      let nextSchemaId = undefined;
+      const schemas = database.getSchemas() ?? [];
+      const hasSchemasLoaded = schemas.length > 0;
+      if (hasSchemasLoaded) {
+        const hasSingleSchema = schemas.length === 1;
+        nextSchemaId = hasSingleSchema ? schemas[0].id : undefined;
+      }
+      onChange({ ...value, databaseId, schemaId: nextSchemaId, tableIds: [] });
+    },
+    [value, databases, onChange],
+  );
+
+  const handleSelectedTablesChange = useCallback(
+    (tableId: Table["id"]) => {
+      const tableIds = toggleTableIdSelection(tableId);
+      onChange({ ...value, tableIds });
+    },
+    [value, toggleTableIdSelection, onChange],
+  );
+
+  const onDatabaseSchemasLoaded = useCallback(() => {
+    if (!selectedSchemaId) {
+      const schemas = selectedDatabase?.getSchemas() ?? [];
+      const hasSingleSchema = schemas.length === 1;
+      if (hasSingleSchema) {
+        const [schema] = schemas;
+        handleSelectedSchemaIdChange(schema.id);
+      }
+    }
+  }, [selectedDatabase, selectedSchemaId, handleSelectedSchemaIdChange]);
+
+  const renderPicker = useCallback(
+    ({ tables }: { tables?: Table[] } = {}) => {
+      return (
+        <RawDataPickerView
+          databases={databases}
+          tables={tables}
+          selectedItems={selectedItems}
+          onSelectDatabase={handleSelectedDatabaseIdChange}
+          onSelectSchema={handleSelectedSchemaIdChange}
+          onSelectedTable={handleSelectedTablesChange}
+          onBack={onBack}
+        />
+      );
+    },
+    [
+      databases,
+      selectedItems,
+      handleSelectedDatabaseIdChange,
+      handleSelectedSchemaIdChange,
+      handleSelectedTablesChange,
+      onBack,
+    ],
+  );
+
+  if (selectedDatabaseId) {
+    return (
+      <Schemas.ListLoader
+        query={{ dbId: selectedDatabaseId }}
+        loadingAndErrorWrapper={false}
+        onLoaded={onDatabaseSchemasLoaded}
+      >
+        {() => {
+          if (!selectedSchema) {
+            return renderPicker();
+          }
+          return (
+            <Tables.ListLoader
+              query={{
+                dbId: selectedDatabaseId,
+                schemaName: selectedSchema.name,
+              }}
+              loadingAndErrorWrapper={false}
+            >
+              {({ tables }: TableListLoaderProps) => renderPicker({ tables })}
+            </Tables.ListLoader>
+          );
+        }}
+      </Schemas.ListLoader>
+    );
+  }
+
+  return renderPicker();
+}
+
+export default Databases.loadList({ loadingAndErrorWrapper: false })(
+  RawDataPicker,
+);
diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx
new file mode 100644
index 00000000000..e411bdc88ef
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx
@@ -0,0 +1,157 @@
+import React, { useCallback, useMemo } from "react";
+import _ from "underscore";
+
+import SelectList from "metabase/components/SelectList";
+
+import type { ITreeNodeItem } from "metabase/components/tree/types";
+
+import type Database from "metabase-lib/metadata/Database";
+import type Table from "metabase-lib/metadata/Table";
+import type Schema from "metabase-lib/metadata/Schema";
+
+import type { DataPickerSelectedItem } from "../types";
+
+import PanePicker from "../PanePicker";
+import { StyledSelectList } from "./RawDataPicker.styled";
+
+interface RawDataPickerViewProps {
+  databases: Database[];
+  tables?: Table[];
+  selectedItems: DataPickerSelectedItem[];
+  onSelectDatabase: (id: Database["id"]) => void;
+  onSelectSchema: (id: Schema["id"]) => void;
+  onSelectedTable: (id: Table["id"]) => void;
+  onBack?: () => void;
+}
+
+function schemaToTreeItem(schema: Schema): ITreeNodeItem {
+  return {
+    id: String(schema.id),
+    name: schema.name,
+    icon: "folder",
+  };
+}
+
+function dbToTreeItem(database: Database): ITreeNodeItem {
+  const schemas = database.getSchemas();
+  const hasSingleSchema = schemas.length === 1;
+  return {
+    id: database.id,
+    name: database.name,
+    icon: "database",
+
+    // If a database has a single schema,
+    // we just want to automatically select it
+    // and exclude it from the tree picker
+    children: hasSingleSchema ? [] : schemas.map(schemaToTreeItem),
+  };
+}
+
+function TableSelectListItem({
+  table,
+  isSelected,
+  onSelect,
+}: {
+  table: Table;
+  isSelected: boolean;
+  onSelect: (id: Table["id"]) => void;
+}) {
+  const name = table.displayName();
+  return (
+    <SelectList.Item
+      id={table.id}
+      name={name}
+      isSelected={isSelected}
+      icon={isSelected ? "check" : "table2"}
+      onSelect={onSelect}
+    >
+      {name}
+    </SelectList.Item>
+  );
+}
+
+function RawDataPickerView({
+  databases,
+  tables,
+  selectedItems,
+  onSelectDatabase,
+  onSelectSchema,
+  onSelectedTable,
+  onBack,
+}: RawDataPickerViewProps) {
+  const treeData = useMemo(() => databases.map(dbToTreeItem), [databases]);
+
+  const { selectedDatabaseId, selectedSchemaId, selectedTableIds } =
+    useMemo(() => {
+      const {
+        database: databases = [],
+        schema: schemas = [],
+        table: tables = [],
+      } = _.groupBy(selectedItems, "type");
+
+      const [db] = databases;
+      const [schema] = schemas;
+
+      return {
+        selectedDatabaseId: db?.id,
+        selectedSchemaId: schema?.id,
+        selectedTableIds: tables.map(table => table.id),
+      };
+    }, [selectedItems]);
+
+  const selectedDatabase = useMemo(
+    () => databases.find(db => db.id === selectedDatabaseId),
+    [databases, selectedDatabaseId],
+  );
+
+  const isSelectedDatabaseSingleSchema = useMemo(
+    () => selectedDatabase?.getSchemas().length === 1,
+    [selectedDatabase],
+  );
+
+  const selectedTreeItemId = useMemo(() => {
+    if (selectedSchemaId) {
+      return isSelectedDatabaseSingleSchema
+        ? selectedDatabaseId
+        : selectedSchemaId;
+    }
+    return selectedDatabaseId;
+  }, [selectedDatabaseId, selectedSchemaId, isSelectedDatabaseSingleSchema]);
+
+  const handlePanePickerSelect = useCallback(
+    (item: ITreeNodeItem) => {
+      if (item.icon === "database") {
+        return onSelectDatabase(Number(item.id));
+      }
+      if (item.icon === "folder") {
+        return onSelectSchema(String(item.id));
+      }
+    },
+    [onSelectDatabase, onSelectSchema],
+  );
+
+  const renderTable = useCallback(
+    (table: Table) => (
+      <TableSelectListItem
+        key={table.id}
+        table={table}
+        isSelected={selectedTableIds.includes(table.id)}
+        onSelect={onSelectedTable}
+      />
+    ),
+    [selectedTableIds, onSelectedTable],
+  );
+
+  return (
+    <PanePicker
+      data={treeData}
+      selectedId={selectedTreeItemId}
+      onSelect={handlePanePickerSelect}
+      onBack={onBack}
+    >
+      <StyledSelectList>{tables?.map?.(renderTable)}</StyledSelectList>
+    </PanePicker>
+  );
+}
+
+export default RawDataPickerView;
diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/index.ts b/frontend/src/metabase/containers/DataPicker/RawDataPicker/index.ts
new file mode 100644
index 00000000000..41003489171
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./RawDataPickerContainer";
diff --git a/frontend/src/metabase/containers/DataPicker/constants.ts b/frontend/src/metabase/containers/DataPicker/constants.ts
new file mode 100644
index 00000000000..adb548e3d6d
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/constants.ts
@@ -0,0 +1 @@
+export const MIN_SEARCH_LENGTH = 2;
diff --git a/frontend/src/metabase/containers/DataPicker/index.ts b/frontend/src/metabase/containers/DataPicker/index.ts
new file mode 100644
index 00000000000..cf5c29fe92b
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/index.ts
@@ -0,0 +1,11 @@
+export { default } from "./DataPickerContainer";
+export { useDataPicker } from "./DataPickerContext";
+
+export { default as useDataPickerValue } from "./useDataPickerValue";
+
+export type {
+  DataPickerDataType,
+  DataPickerValue,
+  DataPickerProps,
+  DataPickerFiltersProp,
+} from "./types";
diff --git a/frontend/src/metabase/containers/DataPicker/types.ts b/frontend/src/metabase/containers/DataPicker/types.ts
new file mode 100644
index 00000000000..fa00a86605c
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/types.ts
@@ -0,0 +1,43 @@
+import type { Collection } from "metabase-types/api";
+
+import type Database from "metabase-lib/metadata/Database";
+import type Table from "metabase-lib/metadata/Table";
+import type Schema from "metabase-lib/metadata/Schema";
+
+export type DataPickerDataType = "models" | "raw-data" | "questions";
+
+export type DataPickerValue = {
+  type?: DataPickerDataType;
+  databaseId?: Database["id"];
+  schemaId?: Schema["id"];
+  collectionId?: Collection["id"];
+  tableIds: Table["id"][];
+};
+
+export interface VirtualTable {
+  id: Table["id"];
+  display_name: string;
+  schema: {
+    id: Schema["id"];
+  };
+}
+
+export interface DataPickerFilters {
+  types: (type: DataPickerDataType) => boolean;
+  databases: (database: Database) => boolean;
+  schemas: (schema: Schema) => boolean;
+  tables: (table: Table | VirtualTable) => boolean;
+}
+
+export type DataPickerFiltersProp = Partial<DataPickerFilters>;
+
+export interface DataPickerProps {
+  value: DataPickerValue;
+  onChange: (value: DataPickerValue) => void;
+  filters?: DataPickerFiltersProp;
+}
+
+export type DataPickerSelectedItem = {
+  type: "database" | "schema" | "collection" | "table";
+  id: string | number;
+};
diff --git a/frontend/src/metabase/containers/DataPicker/useDataPickerValue.ts b/frontend/src/metabase/containers/DataPicker/useDataPickerValue.ts
new file mode 100644
index 00000000000..8a20406c0af
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/useDataPickerValue.ts
@@ -0,0 +1,68 @@
+import { useCallback, useState } from "react";
+
+import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase-lib/metadata/utils/saved-questions";
+
+import { DataPickerValue } from "./types";
+
+function cleanDatabaseValue({ type, databaseId }: Partial<DataPickerValue>) {
+  const isUsingVirtualTables = type === "models" || type === "questions";
+  if (isUsingVirtualTables) {
+    return SAVED_QUESTIONS_VIRTUAL_DB_ID;
+  }
+  return databaseId;
+}
+
+function cleanSchemaValue({ databaseId, schemaId }: Partial<DataPickerValue>) {
+  return databaseId ? schemaId : undefined;
+}
+
+function cleanTablesValue({
+  databaseId,
+  schemaId,
+  tableIds,
+}: Partial<DataPickerValue>) {
+  if (!tableIds) {
+    return [];
+  }
+  return databaseId && schemaId ? tableIds : [];
+}
+
+function cleanCollectionValue({
+  type,
+  databaseId,
+  collectionId,
+}: Partial<DataPickerValue>) {
+  const isUsingVirtualTables = type === "models" || type === "questions";
+  if (isUsingVirtualTables && databaseId === SAVED_QUESTIONS_VIRTUAL_DB_ID) {
+    return collectionId;
+  }
+  return undefined;
+}
+
+function cleanValue(value: Partial<DataPickerValue>): DataPickerValue {
+  return {
+    type: value.type,
+    databaseId: cleanDatabaseValue(value),
+    schemaId: cleanSchemaValue(value),
+    collectionId: cleanCollectionValue(value),
+    tableIds: cleanTablesValue(value),
+  };
+}
+
+type HookResult = [DataPickerValue, (value: DataPickerValue) => void];
+
+function useDataPickerValue(
+  initialValue: Partial<DataPickerValue> = {},
+): HookResult {
+  const [value, _setValue] = useState<DataPickerValue>(
+    cleanValue(initialValue),
+  );
+
+  const setValue = useCallback((nextValue: DataPickerValue) => {
+    _setValue(cleanValue(nextValue));
+  }, []);
+
+  return [value, setValue];
+}
+
+export default useDataPickerValue;
diff --git a/frontend/src/metabase/containers/DataPicker/useSelectedTables.ts b/frontend/src/metabase/containers/DataPicker/useSelectedTables.ts
new file mode 100644
index 00000000000..74bc6ea9b78
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/useSelectedTables.ts
@@ -0,0 +1,75 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+import type { TableId } from "metabase-types/api";
+
+interface SelectedTablesHookOpts {
+  initialValues?: TableId[];
+  mode?: "single" | "multiple";
+}
+
+function useSelectedTables({
+  initialValues = [],
+  mode = "single",
+}: SelectedTablesHookOpts = {}) {
+  const [selectedTableIds, setSelectedTableIds] = useState(
+    new Set(initialValues),
+  );
+
+  useEffect(() => {
+    setSelectedTableIds(new Set(initialValues));
+  }, [initialValues]);
+
+  const addSelectedTableId = useCallback(
+    (id: TableId) => {
+      const nextState =
+        mode === "multiple"
+          ? new Set([...selectedTableIds, id])
+          : new Set([id]);
+      setSelectedTableIds(nextState);
+      return Array.from(nextState);
+    },
+    [selectedTableIds, mode],
+  );
+
+  const removeSelectedTableId = useCallback(
+    (id: TableId) => {
+      if (selectedTableIds.has(id)) {
+        const nextState = new Set([...selectedTableIds].filter(i => i !== id));
+        setSelectedTableIds(nextState);
+        return Array.from(nextState);
+      }
+      return Array.from(selectedTableIds);
+    },
+    [selectedTableIds],
+  );
+
+  const toggleTableIdSelection = useCallback(
+    (id: TableId) => {
+      if (selectedTableIds.has(id)) {
+        return removeSelectedTableId(id);
+      } else {
+        return addSelectedTableId(id);
+      }
+    },
+    [selectedTableIds, addSelectedTableId, removeSelectedTableId],
+  );
+
+  const clearSelectedTables = useCallback(() => {
+    setSelectedTableIds(new Set());
+  }, []);
+
+  const selectedTableIdList = useMemo(
+    () => Array.from(selectedTableIds),
+    [selectedTableIds],
+  );
+
+  return {
+    selectedTableIds: selectedTableIdList,
+    addSelectedTableId,
+    removeSelectedTableId,
+    toggleTableIdSelection,
+    clearSelectedTables,
+  };
+}
+
+export default useSelectedTables;
diff --git a/frontend/src/metabase/containers/DataPicker/utils.ts b/frontend/src/metabase/containers/DataPicker/utils.ts
new file mode 100644
index 00000000000..553eac82f29
--- /dev/null
+++ b/frontend/src/metabase/containers/DataPicker/utils.ts
@@ -0,0 +1,54 @@
+import { t } from "ttag";
+
+import type { DataPickerDataType, DataPickerFilters } from "./types";
+
+export type DataTypeInfoItem = {
+  id: DataPickerDataType;
+  icon: string;
+  name: string;
+  description: string;
+};
+
+export function getDataTypes({
+  hasNestedQueriesEnabled,
+  hasModels,
+}: {
+  hasNestedQueriesEnabled: boolean;
+  hasModels: boolean;
+}): DataTypeInfoItem[] {
+  const dataTypes: DataTypeInfoItem[] = [
+    {
+      id: "raw-data",
+      icon: "database",
+      name: t`Raw Data`,
+      description: t`Unaltered tables in connected databases.`,
+    },
+  ];
+
+  if (hasNestedQueriesEnabled) {
+    if (hasModels) {
+      dataTypes.unshift({
+        id: "models",
+        icon: "model",
+        name: t`Models`,
+        description: t`The best starting place for new questions.`,
+      });
+    }
+
+    dataTypes.push({
+      id: "questions",
+      name: t`Saved Questions`,
+      icon: "folder",
+      description: t`Use any question’s results to start a new question.`,
+    });
+  }
+
+  return dataTypes;
+}
+
+export const DEFAULT_DATA_PICKER_FILTERS: DataPickerFilters = {
+  types: () => true,
+  databases: () => true,
+  schemas: () => true,
+  tables: () => true,
+};
diff --git a/frontend/src/metabase/entities/containers/EntityListLoader.jsx b/frontend/src/metabase/entities/containers/EntityListLoader.jsx
index d4c9eb7fa6e..92d11360b4c 100644
--- a/frontend/src/metabase/entities/containers/EntityListLoader.jsx
+++ b/frontend/src/metabase/entities/containers/EntityListLoader.jsx
@@ -26,6 +26,7 @@ const propTypes = {
   listName: PropTypes.string,
   selectorName: PropTypes.string,
   children: PropTypes.func,
+  onLoaded: PropTypes.func,
 
   // via entityType HOC
   entityDef: PropTypes.object,
@@ -118,6 +119,9 @@ class EntityListLoaderInner extends React.Component {
           !result.payload.result || result.payload.result.length === pageSize,
         );
       }
+
+      this.props?.onLoaded?.(result);
+
       return result;
     },
     250,
diff --git a/frontend/src/metabase/query_builder/components/DataSelector/data-search/SearchResults.jsx b/frontend/src/metabase/query_builder/components/DataSelector/data-search/SearchResults.jsx
index 1efa8ca8fa7..1c7e816791b 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector/data-search/SearchResults.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector/data-search/SearchResults.jsx
@@ -13,7 +13,7 @@ const propTypes = {
   searchQuery: PropTypes.string.required,
   onSelect: PropTypes.func.required,
   searchModels: PropTypes.arrayOf(
-    PropTypes.arrayOf(PropTypes.oneOf(["card", "table"])),
+    PropTypes.oneOf(["card", "dataset", "table"]),
   ),
 };
 
diff --git a/frontend/src/metabase/writeback/components/DataAppDataPicker/DataAppDataPicker.tsx b/frontend/src/metabase/writeback/components/DataAppDataPicker/DataAppDataPicker.tsx
deleted file mode 100644
index a5ee2a3c706..00000000000
--- a/frontend/src/metabase/writeback/components/DataAppDataPicker/DataAppDataPicker.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-
-import { DatabaseSchemaAndTableDataSelector } from "metabase/query_builder/components/DataSelector";
-
-import type { TableId } from "metabase-types/api";
-
-interface Props {
-  tableId: TableId | null;
-  onTableChange: (tableId: TableId | null) => void;
-}
-
-function DataAppDataPicker({ tableId, onTableChange }: Props) {
-  return (
-    <DatabaseSchemaAndTableDataSelector
-      selectedTableId={tableId}
-      setSourceTableFn={onTableChange}
-      requireWriteback
-      isPopover={false}
-    />
-  );
-}
-
-export default DataAppDataPicker;
diff --git a/frontend/src/metabase/writeback/components/DataAppDataPicker/index.ts b/frontend/src/metabase/writeback/components/DataAppDataPicker/index.ts
deleted file mode 100644
index a370f39fa3e..00000000000
--- a/frontend/src/metabase/writeback/components/DataAppDataPicker/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./DataAppDataPicker";
diff --git a/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/DataAppScaffoldingDataPicker.tsx b/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/DataAppScaffoldingDataPicker.tsx
new file mode 100644
index 00000000000..8ffcf28eb82
--- /dev/null
+++ b/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/DataAppScaffoldingDataPicker.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+import DataPicker, {
+  DataPickerProps,
+  DataPickerFiltersProp,
+} from "metabase/containers/DataPicker";
+
+const FILTERS: DataPickerFiltersProp = {
+  types: type => type !== "questions",
+};
+
+type DataAppScaffoldingDataPickerProps = Omit<DataPickerProps, "filters">;
+
+function DataAppScaffoldingDataPicker(
+  props: DataAppScaffoldingDataPickerProps,
+) {
+  return <DataPicker {...props} filters={FILTERS} />;
+}
+
+export default DataAppScaffoldingDataPicker;
diff --git a/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/index.ts b/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/index.ts
new file mode 100644
index 00000000000..abc22da165d
--- /dev/null
+++ b/frontend/src/metabase/writeback/components/DataAppScaffoldingDataPicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./DataAppScaffoldingDataPicker";
diff --git a/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.styled.tsx b/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.styled.tsx
index 9e164b52f52..ebc28e0e8c5 100644
--- a/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.styled.tsx
+++ b/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.styled.tsx
@@ -1,5 +1,9 @@
 import styled from "@emotion/styled";
+
+import Icon from "metabase/components/Icon";
+
 import { color } from "metabase/lib/colors";
+import { space } from "metabase/styled-components/theme";
 
 export const ModalRoot = styled.div`
   display: flex;
@@ -11,6 +15,7 @@ export const ModalRoot = styled.div`
 export const ModalHeader = styled.div`
   display: flex;
   align-items: center;
+  justify-content: space-between;
   padding: 1.5rem 2rem;
 `;
 
@@ -26,7 +31,7 @@ export const ModalBody = styled.div`
   display: flex;
   flex: 1;
 
-  padding: 0 1rem 1rem 1rem;
+  padding: 0 1rem 0 1rem;
 
   overflow-y: scroll;
 `;
@@ -40,3 +45,40 @@ export const ModalFooter = styled.div`
 
   border-top: 1px solid ${color("border")};
 `;
+
+export const SearchInputContainer = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  position: relative;
+
+  max-width: 14rem;
+  padding: 12px 14px;
+
+  border: 1px solid ${color("border")};
+  border-radius: 8px;
+`;
+
+export const SearchIcon = styled(Icon)`
+  color: ${color("text-light")};
+`;
+
+export const SearchInput = styled.input`
+  background-color: transparent;
+  border: none;
+
+  color: ${color("text-medium")};
+  font-weight: 700;
+
+  width: 100%;
+  margin-left: 8px;
+
+  &:focus {
+    outline: none;
+  }
+
+  &::placeholder {
+    color: ${color("text-light")};
+    font-weight: 700;
+  }
+`;
diff --git a/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.tsx b/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.tsx
index f2e452e9409..18bf48a2ce4 100644
--- a/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.tsx
+++ b/frontend/src/metabase/writeback/containers/CreateDataAppModal/CreateDataAppModal.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback } from "react";
 import { t } from "ttag";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
@@ -10,9 +10,14 @@ import * as Urls from "metabase/lib/urls";
 
 import DataApps, { ScaffoldNewAppParams } from "metabase/entities/data-apps";
 
-import DataAppDataPicker from "metabase/writeback/components/DataAppDataPicker";
+import DataPicker, {
+  useDataPicker,
+  useDataPickerValue,
+  DataPickerValue,
+} from "metabase/containers/DataPicker";
+import DataAppScaffoldingDataPicker from "metabase/writeback/components/DataAppScaffoldingDataPicker";
 
-import type { DataApp, TableId } from "metabase-types/api";
+import type { DataApp } from "metabase-types/api";
 import type { Dispatch, State } from "metabase-types/store";
 
 import {
@@ -21,6 +26,9 @@ import {
   ModalTitle,
   ModalBody,
   ModalFooter,
+  SearchInputContainer,
+  SearchInput,
+  SearchIcon,
 } from "./CreateDataAppModal.styled";
 
 interface OwnProps {
@@ -47,35 +55,67 @@ function mapDispatchToProps(dispatch: Dispatch) {
   };
 }
 
+function getSearchInputPlaceholder(value: DataPickerValue) {
+  if (value?.type === "models") {
+    return t`Search for a model…`;
+  }
+  if (value?.type === "raw-data") {
+    return t`Search for a table…`;
+  }
+  return t`Search for some data…`;
+}
+
+function DataPickerSearchInput({ value }: { value: DataPickerValue }) {
+  const { search } = useDataPicker();
+
+  return (
+    <SearchInputContainer>
+      <SearchIcon name="search" size={16} />
+      <SearchInput
+        value={search.query}
+        onChange={e => search.setQuery(e.target.value)}
+        placeholder={getSearchInputPlaceholder(value)}
+      />
+    </SearchInputContainer>
+  );
+}
+
 function CreateDataAppModal({ onCreate, onChangeLocation, onClose }: Props) {
-  const [tableId, setTableId] = useState<TableId | null>(null);
+  const [value, setValue] = useDataPickerValue();
+
+  const { tableIds } = value;
 
   const handleCreate = useCallback(async () => {
     const dataApp = await onCreate({
       name: t`New App`,
-      tables: [tableId] as number[],
+      tables: tableIds as number[],
     });
     onClose();
     onChangeLocation(Urls.dataApp(dataApp));
-  }, [tableId, onCreate, onChangeLocation, onClose]);
+  }, [tableIds, onCreate, onChangeLocation, onClose]);
+
+  const canSubmit = tableIds.length > 0;
 
   return (
-    <ModalRoot>
-      <ModalHeader>
-        <ModalTitle>{t`Pick your starting data`}</ModalTitle>
-      </ModalHeader>
-      <ModalBody>
-        <DataAppDataPicker tableId={tableId} onTableChange={setTableId} />
-      </ModalBody>
-      <ModalFooter>
-        <Button onClick={onClose}>{t`Cancel`}</Button>
-        <Button
-          primary
-          disabled={tableId == null}
-          onClick={handleCreate}
-        >{t`Create`}</Button>
-      </ModalFooter>
-    </ModalRoot>
+    <DataPicker.Provider>
+      <ModalRoot>
+        <ModalHeader>
+          <ModalTitle>{t`Pick your starting data`}</ModalTitle>
+          <DataPickerSearchInput value={value} />
+        </ModalHeader>
+        <ModalBody>
+          <DataAppScaffoldingDataPicker value={value} onChange={setValue} />
+        </ModalBody>
+        <ModalFooter>
+          <Button onClick={onClose}>{t`Cancel`}</Button>
+          <Button
+            primary
+            disabled={!canSubmit}
+            onClick={handleCreate}
+          >{t`Create`}</Button>
+        </ModalFooter>
+      </ModalRoot>
+    </DataPicker.Provider>
   );
 }
 
diff --git a/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.styled.tsx b/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.styled.tsx
index 9e164b52f52..8e510742d8e 100644
--- a/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.styled.tsx
+++ b/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.styled.tsx
@@ -26,7 +26,7 @@ export const ModalBody = styled.div`
   display: flex;
   flex: 1;
 
-  padding: 0 1rem 1rem 1rem;
+  padding: 0 1rem 0 1rem;
 
   overflow-y: scroll;
 `;
diff --git a/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.tsx b/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.tsx
index 8fe75ab0d5b..139b0dfbb33 100644
--- a/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.tsx
+++ b/frontend/src/metabase/writeback/containers/ScaffoldDataAppPagesModal/ScaffoldDataAppPagesModal.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback } from "react";
 import { t } from "ttag";
 import { connect } from "react-redux";
 
@@ -6,9 +6,10 @@ import Button from "metabase/core/components/Button";
 
 import DataApps, { ScaffoldNewPagesParams } from "metabase/entities/data-apps";
 
-import DataAppDataPicker from "metabase/writeback/components/DataAppDataPicker";
+import { useDataPickerValue } from "metabase/containers/DataPicker";
+import DataAppScaffoldingDataPicker from "metabase/writeback/components/DataAppScaffoldingDataPicker";
 
-import type { DataApp, TableId } from "metabase-types/api";
+import type { DataApp } from "metabase-types/api";
 import type { Dispatch, State } from "metabase-types/store";
 
 import {
@@ -48,16 +49,19 @@ function ScaffoldDataAppPagesModal({
   onScaffold,
   onClose,
 }: Props) {
-  const [tableId, setTableId] = useState<TableId | null>(null);
+  const [value, setValue] = useDataPickerValue();
+  const { tableIds } = value;
 
   const handleAdd = useCallback(async () => {
     const dataApp = await onScaffold({
       dataAppId,
-      tables: [tableId] as number[],
+      tables: tableIds as number[],
     });
     onClose();
     onAdd(dataApp);
-  }, [dataAppId, tableId, onAdd, onScaffold, onClose]);
+  }, [dataAppId, tableIds, onAdd, onScaffold, onClose]);
+
+  const canSubmit = tableIds.length > 0;
 
   return (
     <ModalRoot>
@@ -65,13 +69,13 @@ function ScaffoldDataAppPagesModal({
         <ModalTitle>{t`Pick your data`}</ModalTitle>
       </ModalHeader>
       <ModalBody>
-        <DataAppDataPicker tableId={tableId} onTableChange={setTableId} />
+        <DataAppScaffoldingDataPicker value={value} onChange={setValue} />
       </ModalBody>
       <ModalFooter>
         <Button onClick={onClose}>{t`Cancel`}</Button>
         <Button
           primary
-          disabled={tableId == null}
+          disabled={!canSubmit}
           onClick={handleAdd}
         >{t`Add`}</Button>
       </ModalFooter>
-- 
GitLab