diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 423fbdb66a1b63fc120bdb8c57fe53c603a26683..6377b2454268543c6cf46b2b0c13af9979a0e1c3 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -1009,7 +1009,10 @@ const TablePicker = ({
       },
     ];
     return (
-      <div style={{ width: 300, overflowY: "auto" }}>
+      <div
+        style={{ width: 300, overflowY: "auto" }}
+        data-testid="data-selector"
+      >
         <AccordionList
           id="TablePicker"
           key="tablePicker"
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookCell.jsx b/frontend/src/metabase/query_builder/components/notebook/NotebookCell.jsx
index c006ae0f534ab55bbf4d094375541b9dd6489224..8b1e20f8e99be0da13de30a2927e17ea7655f519 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookCell.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookCell.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 
 import { Flex } from "grid-styled";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
 
 import Icon from "metabase/components/Icon";
 
@@ -14,45 +14,101 @@ export const NotebookCell = styled(Flex).attrs({
 })`
   border-radius: 8px;
   background-color: ${props => alpha(props.color, 0.1)};
+  padding: 14px;
 `;
 
-NotebookCell.defaultProps = {
-  px: 2,
-  pt: 2,
-  pb: 1,
-};
-
 NotebookCell.displayName = "NotebookCell";
 
-export const NotebookCellItem = styled(Flex).attrs({
-  align: "center",
-  children: ({ icon, children }) => [
-    icon && <Icon className="mr1" name={icon} size={10} />,
-    children,
-  ],
-})`
+const NotebookCellItemContainer = styled(Flex).attrs({ align: "center" })`
   font-weight: bold;
-  border: 2px solid transparent;
-  border-radius: 6px;
   color: ${props => (props.inactive ? props.color : "white")};
-  background-color: ${props => (props.inactive ? "transparent" : props.color)};
+  border-radius: 6px;
+  margin-right: 4px;
+
+  border: 2px solid transparent;
   border-color: ${props =>
     props.inactive ? alpha(props.color, 0.25) : "transparent"};
+
   &:hover {
-    background-color: ${props => !props.inactive && alpha(props.color, 0.8)};
     border-color: ${props => props.inactive && alpha(props.color, 0.8)};
   }
-  transition: background 300ms linear, border 300ms linear;
-  > .Icon {
+
+  transition: border 300ms linear;
+
+  .Icon-close {
     opacity: 0.6;
   }
 `;
 
-NotebookCellItem.defaultProps = {
-  p: 1,
-  mr: 1,
-  mb: 1,
-};
+const NotebookCellItemContentContainer = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  background-color: ${props => (props.inactive ? "transparent" : props.color)};
+
+  &:hover {
+    background-color: ${props => !props.inactive && alpha(props.color, 0.8)};
+  }
+
+  ${props =>
+    !!props.border &&
+    css`
+    border-${props.border}: 1px solid ${alpha("white", 0.25)};
+  `}
+
+  ${props =>
+    props.roundedCorners.includes("left") &&
+    css`
+      border-top-left-radius: 6px;
+      border-bottom-left-radius: 6px;
+    `}
+
+  ${props =>
+    props.roundedCorners.includes("right") &&
+    css`
+      border-top-right-radius: 6px;
+      border-bottom-right-radius: 6px;
+    `}
+
+  transition: background 300ms linear;
+`;
+
+export function NotebookCellItem({
+  inactive,
+  color,
+  right,
+  rightContainerStyle,
+  children,
+  ...props
+}) {
+  const hasRightSide = React.isValidElement(right);
+  const mainContentRoundedCorners = ["left"];
+  if (!hasRightSide) {
+    mainContentRoundedCorners.push("right");
+  }
+  return (
+    <NotebookCellItemContainer inactive={inactive} color={color} {...props}>
+      <NotebookCellItemContentContainer
+        inactive={inactive}
+        color={color}
+        roundedCorners={mainContentRoundedCorners}
+      >
+        {children}
+      </NotebookCellItemContentContainer>
+      {hasRightSide && (
+        <NotebookCellItemContentContainer
+          inactive={inactive}
+          color={color}
+          border="left"
+          roundedCorners={["right"]}
+          style={rightContainerStyle}
+        >
+          {right}
+        </NotebookCellItemContentContainer>
+      )}
+    </NotebookCellItemContainer>
+  );
+}
 
 NotebookCellItem.displayName = "NotebookCellItem";
 
@@ -61,14 +117,6 @@ export const NotebookCellAdd = styled(NotebookCellItem).attrs({
   // eslint-disable-next-line react/display-name
   children: ({ initialAddText }) =>
     initialAddText || <Icon name="add" className="text-white" />,
-})`
-  > .Icon {
-    opacity: 1;
-  }
-`;
-
-NotebookCellAdd.defaultProps = {
-  mb: 1,
-};
+})``;
 
 NotebookCellAdd.displayName = "NotebookCellAdd";
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
index 1138332df0228e199fcc1ea3905a6e729d496777..4364d7672da71330a37b384c9e6c093b32260380 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
@@ -30,11 +30,7 @@ function DataStep({ color, query, databases, updateQuery }) {
               {t`Pick your starting data`}
             </NotebookCellItem>
           ) : (
-            <NotebookCellItem
-              color={color}
-              icon="table2"
-              data-testid="data-step-cell"
-            >
+            <NotebookCellItem color={color} data-testid="data-step-cell">
               {table && table.displayName()}
             </NotebookCellItem>
           )
@@ -42,7 +38,7 @@ function DataStep({ color, query, databases, updateQuery }) {
       />
       {table && query.isRaw() && (
         <DataFieldsPicker
-          className="ml-auto mb1 text-bold"
+          className="ml-auto text-bold"
           query={query}
           updateQuery={updateQuery}
         />
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
index 820f5a0d72f0cdd2240becb30bedecc6ead0e142..333336199c818e45a20343bb7df1b3429624bc01 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
@@ -168,7 +168,7 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
 
   return (
     <JoinClauseRoot>
-      <NotebookCellItem color={color} icon="table2">
+      <NotebookCellItem color={color}>
         {(lhsTable && lhsTable.displayName()) || `Previous results`}
       </NotebookCellItem>
 
@@ -194,6 +194,7 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
             options={join.parentDimensionOptions()}
             onChange={onParentDimensionChange}
             ref={parentDimensionPickerRef}
+            data-testid="parent-dimension"
           />
 
           <JoinOnConditionLabel />
@@ -205,13 +206,14 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
             options={join.joinDimensionOptions()}
             onChange={onJoinDimensionChange}
             ref={joinDimensionPickerRef}
+            data-testid="join-dimension"
           />
         </JoinedTableControlRoot>
       )}
 
       {join.isValid() && (
         <JoinFieldsPicker
-          className="mb1 ml-auto text-bold"
+          className="ml-auto text-bold"
           join={join}
           updateQuery={updateQuery}
         />
@@ -256,7 +258,7 @@ function JoinTablePicker({
   }
 
   return (
-    <NotebookCellItem color={color} icon="table2" inactive={!joinedTable}>
+    <NotebookCellItem color={color} inactive={!joinedTable}>
       <DatabaseSchemaAndTableDataSelector
         hasTableSearch
         canChangeDatabase={false}
@@ -380,6 +382,7 @@ const joinDimensionPickerPropTypes = {
   }).isRequired,
   query: PropTypes.object.isRequired,
   color: PropTypes.string,
+  "data-testid": PropTypes.string,
 };
 
 class JoinDimensionPicker extends React.Component {
@@ -388,14 +391,15 @@ class JoinDimensionPicker extends React.Component {
   }
   render() {
     const { dimension, onChange, options, query, color } = this.props;
+    const testID = this.props["data-testid"] || "join-dimension";
     return (
       <PopoverWithTrigger
         ref={ref => (this._popover = ref)}
         triggerElement={
           <NotebookCellItem
             color={color}
-            icon={dimension && dimension.icon()}
             inactive={!dimension}
+            data-testid={testID}
           >
             {dimension ? dimension.displayName() : `Pick a column...`}
           </NotebookCellItem>
@@ -412,6 +416,7 @@ class JoinDimensionPicker extends React.Component {
               onChange(field);
               onClose();
             }}
+            data-testid={`${testID}-picker`}
           />
         )}
       </PopoverWithTrigger>
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.js b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.js
index b6d757e04a288a078fea64bc5a3a93fb8ae261f9..4d89c801226faba829e0533d00e452d98e24dab0 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.js
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.js
@@ -21,7 +21,9 @@ export const JoinClauseRoot = styled.div`
 
 export const JoinStrategyIcon = styled(Icon).attrs({ size: 32 })`
   color: ${color("brand")};
-  margin-right: ${space(1)};
+  margin-right: 6px;
+  margin-left: 2px;
+  margin-top: 6px;
 `;
 
 export const JoinTypeSelectRoot = styled.div`
@@ -62,14 +64,17 @@ export const JoinedTableControlRoot = styled.div`
 export const JoinWhereConditionLabel = styled.span.attrs({ children: "where" })`
   color: ${color("text-medium")};
   font-weight: bold;
-  margin-left: ${space(1)};
-  margin-right: ${space(2)};
+  margin-top: 6px;
+  margin-left: 10px;
+  margin-right: 14px;
 `;
 
 export const JoinOnConditionLabel = styled.span.attrs({ children: "=" })`
   font-weight: bold;
   color: ${color("text-medium")};
-  margin-right: 8px;
+  margin-left: 2px;
+  margin-right: 6px;
+  margin-top: 6px;
 `;
 
 export const RemoveJoinIcon = styled(Icon).attrs({ name: "close", size: 18 })`
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.unit.spec.js b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..746f46a5e805abd998f6e2f23813e1022aa49f69
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.unit.spec.js
@@ -0,0 +1,287 @@
+/* eslint-disable react/prop-types */
+import React, { useState } from "react";
+import { Provider } from "react-redux";
+import {
+  render,
+  screen,
+  fireEvent,
+  within,
+  waitForElementToBeRemoved,
+} from "@testing-library/react";
+import xhrMock from "xhr-mock";
+import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
+import { getStore } from "__support__/entities-store";
+import {
+  state,
+  ORDERS,
+  PRODUCTS,
+  SAMPLE_DATASET,
+} from "__support__/sample_dataset_fixture";
+import JoinStep from "./JoinStep";
+
+jest.setTimeout(10000);
+
+describe("Notebook Editor > Join Step", () => {
+  const TEST_QUERY = {
+    type: "query",
+    database: SAMPLE_DATASET.id,
+    query: {
+      "source-table": ORDERS.id,
+    },
+  };
+
+  function JoinStepWrapped({ initialQuery, onChange, ...props }) {
+    const [query, setQuery] = useState(initialQuery);
+    return (
+      <JoinStep
+        {...props}
+        query={query}
+        updateQuery={datasetQuery => {
+          const newQuery = query.setDatasetQuery(datasetQuery);
+          setQuery(newQuery);
+          onChange(datasetQuery);
+        }}
+      />
+    );
+  }
+
+  function setup() {
+    const query = new StructuredQuery(ORDERS.question(), TEST_QUERY);
+    const onQueryChange = jest.fn();
+
+    const TEST_STEP = {
+      id: "0:join",
+      type: "join",
+      itemIndex: 0,
+      stageIndex: 0,
+      query,
+      valid: true,
+      visible: true,
+      active: true,
+      actions: [],
+      update: jest.fn(),
+      clean: jest.fn(),
+      revert: jest.fn(),
+    };
+
+    render(
+      <Provider store={getStore({}, state)}>
+        <JoinStepWrapped
+          initialQuery={query}
+          step={TEST_STEP}
+          onChange={onQueryChange}
+        />
+      </Provider>,
+    );
+
+    return { onQueryChange };
+  }
+
+  function toFieldRef(field, joinedTable) {
+    return [
+      "field",
+      field.id,
+      joinedTable ? { "join-alias": joinedTable.display_name } : null,
+    ];
+  }
+
+  function expectedJoin({
+    fields = "all",
+    joinedTable,
+    leftField,
+    rightField,
+  }) {
+    const joinFields = Array.isArray(fields)
+      ? fields.map(field => toFieldRef(field, joinedTable))
+      : fields;
+
+    return {
+      ...TEST_QUERY,
+      query: {
+        ...TEST_QUERY.query,
+        joins: [
+          expect.objectContaining({
+            fields: joinFields,
+            "source-table": joinedTable.id,
+            condition: [
+              "=",
+              toFieldRef(leftField),
+              toFieldRef(rightField, joinedTable),
+            ],
+          }),
+        ],
+      },
+    };
+  }
+
+  async function selectTable(tableName) {
+    fireEvent.click(screen.queryByText(/Sample Dataset/i));
+    const dataSelector = await screen.findByTestId("data-selector");
+    fireEvent.click(within(dataSelector).queryByText(tableName));
+
+    await waitForElementToBeRemoved(() =>
+      screen.queryByTestId("data-selector"),
+    );
+  }
+
+  function openDimensionPicker(type) {
+    const openPickerButton = screen.getByTestId(`${type}-dimension`);
+    fireEvent.click(openPickerButton);
+    return screen.findByRole("rowgroup");
+  }
+
+  beforeEach(() => {
+    xhrMock.setup();
+    xhrMock.get("/api/database", {
+      body: JSON.stringify({
+        total: 1,
+        data: [SAMPLE_DATASET.getPlainObject()],
+      }),
+    });
+    xhrMock.get("/api/database/1/schemas", {
+      body: JSON.stringify(["PUBLIC"]),
+    });
+    xhrMock.get("/api/database/1/schema/PUBLIC", {
+      body: JSON.stringify(
+        SAMPLE_DATASET.tables.filter(table => table.schema === "PUBLIC"),
+      ),
+    });
+  });
+
+  afterEach(() => {
+    xhrMock.teardown();
+  });
+
+  it("displays a source table and suggests to pick a join table", () => {
+    setup();
+    expect(screen.queryByText("Orders")).toBeInTheDocument();
+    expect(screen.queryByText("Pick a table...")).toBeInTheDocument();
+  });
+
+  it("opens a schema browser by default", async () => {
+    setup();
+
+    fireEvent.click(screen.queryByText(/Sample Dataset/i));
+    const dataSelector = await screen.findByTestId("data-selector");
+
+    SAMPLE_DATASET.tables.forEach(table => {
+      const tableName = new RegExp(table.display_name, "i");
+      expect(within(dataSelector).queryByText(tableName)).toBeInTheDocument();
+    });
+  });
+
+  it("automatically sets join fields if possible", async () => {
+    const { onQueryChange } = setup();
+
+    await selectTable(/Products/i);
+
+    expect(screen.getByTestId("parent-dimension")).toHaveTextContent(
+      /Product ID/i,
+    );
+    expect(screen.getByTestId("join-dimension")).toHaveTextContent(/ID/i);
+    expect(onQueryChange).toHaveBeenLastCalledWith(
+      expectedJoin({
+        joinedTable: PRODUCTS,
+        leftField: ORDERS.PRODUCT_ID,
+        rightField: PRODUCTS.ID,
+      }),
+    );
+  });
+
+  it("shows a parent dimension's join field picker", async () => {
+    const ordersFields = Object.values(ORDERS.fieldsLookup());
+    setup();
+    await selectTable(/Products/i);
+
+    fireEvent.click(screen.getByTestId("parent-dimension"));
+
+    const picker = await screen.findByRole("rowgroup");
+    expect(picker).toBeInTheDocument();
+    expect(picker).toBeVisible();
+    expect(within(picker).queryByText("Order")).toBeInTheDocument();
+    ordersFields.forEach(field => {
+      expect(
+        within(picker).queryByText(field.display_name),
+      ).toBeInTheDocument();
+    });
+  });
+
+  it("can change parent dimension's join field", async () => {
+    const { onQueryChange } = setup();
+    await selectTable(/Products/i);
+    const picker = await openDimensionPicker("parent");
+
+    fireEvent.click(within(picker).getByText("Tax"));
+
+    expect(onQueryChange).toHaveBeenLastCalledWith(
+      expectedJoin({
+        joinedTable: PRODUCTS,
+        leftField: ORDERS.TAX,
+        rightField: PRODUCTS.ID,
+      }),
+    );
+  });
+
+  it("shows a join dimension's field picker", async () => {
+    const productsFields = Object.values(PRODUCTS.fieldsLookup());
+    setup();
+    await selectTable(/Products/i);
+
+    fireEvent.click(screen.getByTestId("join-dimension"));
+
+    const picker = await screen.findByRole("rowgroup");
+    expect(picker).toBeInTheDocument();
+    expect(picker).toBeVisible();
+    expect(within(picker).queryByText("Product")).toBeInTheDocument();
+    productsFields.forEach(field => {
+      expect(
+        within(picker).queryByText(field.display_name),
+      ).toBeInTheDocument();
+    });
+  });
+
+  it("can change join dimension's field", async () => {
+    const { onQueryChange } = setup();
+    await selectTable(/Products/i);
+    const picker = await openDimensionPicker("join");
+
+    fireEvent.click(within(picker).getByText("Category"));
+
+    expect(onQueryChange).toHaveBeenLastCalledWith(
+      expectedJoin({
+        joinedTable: PRODUCTS,
+        leftField: ORDERS.PRODUCT_ID,
+        rightField: PRODUCTS.CATEGORY,
+      }),
+    );
+  });
+
+  it("automatically opens dimensions picker if can't automatically set join fields", async () => {
+    setup();
+
+    await selectTable(/Reviews/i);
+
+    const picker = await screen.findByRole("rowgroup");
+    expect(picker).toBeInTheDocument();
+    expect(picker).toBeVisible();
+    expect(within(picker).queryByText("Order")).toBeInTheDocument();
+  });
+
+  it("can select fields to select from a joined table", async () => {
+    const { onQueryChange } = setup();
+    await selectTable(/Products/i);
+
+    fireEvent.click(screen.getByText("Columns"));
+    fireEvent.click(screen.getByText("Select None"));
+    fireEvent.click(screen.getByText("Category"));
+
+    expect(onQueryChange).toHaveBeenLastCalledWith(
+      expectedJoin({
+        joinedTable: PRODUCTS,
+        leftField: ORDERS.PRODUCT_ID,
+        rightField: PRODUCTS.ID,
+        fields: [PRODUCTS.CATEGORY],
+      }),
+    );
+  });
+});
diff --git a/frontend/src/metabase/reference/databases/FieldList.jsx b/frontend/src/metabase/reference/databases/FieldList.jsx
index df50df3fa58711bf27d2f5c65aa0b137498ed0f6..fd42cd47674bc735005b5db3c8d9aaf790bc91c4 100644
--- a/frontend/src/metabase/reference/databases/FieldList.jsx
+++ b/frontend/src/metabase/reference/databases/FieldList.jsx
@@ -91,6 +91,7 @@ export default class FieldList extends Component {
     loadingError: PropTypes.object,
     submitting: PropTypes.bool,
     resetForm: PropTypes.func,
+    "data-testid": PropTypes.string,
   };
 
   render() {
@@ -123,6 +124,7 @@ export default class FieldList extends Component {
               this.props,
             ),
         )}
+        testID={this.props["data-testid"]}
       >
         {isEditing && (
           <EditHeader
diff --git a/frontend/test/__support__/entities-store.js b/frontend/test/__support__/entities-store.js
index a452512da84edde892db2d13d182d7c70049c39b..b57d50c3887569d9c034f057b50dcaf0517cf63a 100644
--- a/frontend/test/__support__/entities-store.js
+++ b/frontend/test/__support__/entities-store.js
@@ -5,7 +5,7 @@ import promise from "redux-promise";
 import { thunkWithDispatchAction } from "metabase/store";
 import * as entities from "metabase/redux/entities";
 
-export function getStore(reducers = {}) {
+export function getStore(reducers = {}, initialState = {}) {
   const reducer = combineReducers({
     entities: entities.reducer,
     requests: (state, action) =>
@@ -13,8 +13,6 @@ export function getStore(reducers = {}) {
     ...reducers,
   });
 
-  const initialState = {};
-
   return createStore(
     reducer,
     initialState,
diff --git a/frontend/test/metabase/scenarios/question/new.cy.spec.js b/frontend/test/metabase/scenarios/question/new.cy.spec.js
index 462655059164883cdc874b7743720846a407217c..1a8b844296a63b9c4ed80f55577c24a534ef1820 100644
--- a/frontend/test/metabase/scenarios/question/new.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/new.cy.spec.js
@@ -208,7 +208,7 @@ describe("scenarios > question > new", () => {
         cy.findByText("Orders, Count").click();
 
         // Try to choose a different saved question
-        cy.get("[icon=table2]").click();
+        cy.findByTestId("data-step-cell").click();
 
         cy.findByText("Our analytics");
         cy.findByText("Orders");