From 33dd211533f72c1a74f43e5481359fecede60597 Mon Sep 17 00:00:00 2001
From: Anton Kulyk <kuliks.anton@gmail.com>
Date: Tue, 15 Feb 2022 11:59:09 +0200
Subject: [PATCH] Clean up `QueryBuilder` top container and view components
 (#20416)

* Extract methods

* Reorder imports

* Extract method to render left QB sidebar

* Extract method to render right sidebar

* Extract `NewQuestionView` component

* Extract `renderHeader`

* Extract popovers rendering

* Extract styled components

* Fix styled components

* Extract `renderMain` method

* Fix some prop drilling and access

* Minor QB container cleanup

* Reorder QB container imports
---
 .../query_builder/components/view/View.jsx    | 548 ++++++++++--------
 .../components/view/View.styled.tsx           |  71 +++
 .../View/NewQuestionView/NewQuestionView.tsx  |  29 +
 .../view/View/NewQuestionView/index.ts        |   1 +
 .../query_builder/containers/QueryBuilder.jsx |  45 +-
 5 files changed, 417 insertions(+), 277 deletions(-)
 create mode 100644 frontend/src/metabase/query_builder/components/view/View.styled.tsx
 create mode 100644 frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
 create mode 100644 frontend/src/metabase/query_builder/components/view/View/NewQuestionView/index.ts

diff --git a/frontend/src/metabase/query_builder/components/view/View.jsx b/frontend/src/metabase/query_builder/components/view/View.jsx
index 8ba1f2eb8a9..16fa5b0d413 100644
--- a/frontend/src/metabase/query_builder/components/view/View.jsx
+++ b/frontend/src/metabase/query_builder/components/view/View.jsx
@@ -1,31 +1,24 @@
 /* eslint-disable react/prop-types */
 import React from "react";
-import { t } from "ttag";
-
-import cx from "classnames";
+import { Motion, spring } from "react-motion";
 
 import ExplicitSize from "metabase/components/ExplicitSize";
 import Popover from "metabase/components/Popover";
-import DebouncedFrame from "metabase/components/DebouncedFrame";
-import Subhead from "metabase/components/type/Subhead";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
+import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
+import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
+
+import AggregationPopover from "../AggregationPopover";
+import BreakoutPopover from "../BreakoutPopover";
+import DatasetEditor from "../DatasetEditor";
 import NativeQueryEditor from "../NativeQueryEditor";
 import QueryVisualization from "../QueryVisualization";
 import DataReference from "../dataref/DataReference";
 import TagEditorSidebar from "../template_tags/TagEditorSidebar";
 import SnippetSidebar from "../template_tags/SnippetSidebar";
 import SavedQuestionIntroModal from "../SavedQuestionIntroModal";
-
-import AggregationPopover from "../AggregationPopover";
-import BreakoutPopover from "../BreakoutPopover";
-
 import QueryModals from "../QueryModals";
-import { ViewTitleHeader, ViewSubHeader } from "./ViewHeader";
-import NewQuestionHeader from "./NewQuestionHeader";
-import ViewFooter from "./ViewFooter";
-import ViewSidebar from "./ViewSidebar";
-import QuestionDataSelector from "./QuestionDataSelector";
 
 import ChartSettingsSidebar from "./sidebars/ChartSettingsSidebar";
 import ChartTypeSidebar from "./sidebars/ChartTypeSidebar";
@@ -33,16 +26,24 @@ import SummarizeSidebar from "./sidebars/SummarizeSidebar/SummarizeSidebar";
 import FilterSidebar from "./sidebars/FilterSidebar";
 import QuestionDetailsSidebar from "./sidebars/QuestionDetailsSidebar";
 
-import { Motion, spring } from "react-motion";
-
-import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
-import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
-
-import DatasetEditor from "../DatasetEditor";
-
+import { ViewSubHeader } from "./ViewHeader";
+import NewQuestionHeader from "./NewQuestionHeader";
+import ViewFooter from "./ViewFooter";
+import ViewSidebar from "./ViewSidebar";
+import NewQuestionView from "./View/NewQuestionView";
 import QueryViewNotebook from "./View/QueryViewNotebook";
 
+import {
+  QueryBuilderViewRoot,
+  QueryBuilderContentContainer,
+  QueryBuilderMain,
+  QueryBuilderViewHeaderContainer,
+  BorderedViewTitleHeader,
+  NativeQueryEditorContainer,
+  StyledDebouncedFrame,
+  StyledSyncedParametersList,
+} from "./View.styled";
+
 const DEFAULT_POPOVER_STATE = {
   aggregationIndex: null,
   aggregationPopoverTarget: null,
@@ -62,6 +63,7 @@ export default class View extends React.Component {
       aggregationPopoverTarget: e.target,
     });
   };
+
   handleEditSeries = (e, index) => {
     this.setState({
       ...DEFAULT_POPOVER_STATE,
@@ -69,10 +71,12 @@ export default class View extends React.Component {
       aggregationIndex: index,
     });
   };
+
   handleRemoveSeries = (e, index) => {
     const { query } = this.props;
     query.removeAggregation(index).update(null, { run: true });
   };
+
   handleEditBreakout = (e, index) => {
     this.setState({
       ...DEFAULT_POPOVER_STATE,
@@ -80,77 +84,178 @@ export default class View extends React.Component {
       breakoutIndex: index,
     });
   };
+
   handleClosePopover = () => {
     this.setState({
       ...DEFAULT_POPOVER_STATE,
     });
   };
 
-  render() {
+  onChangeAggregation = aggregation => {
+    const { query } = this.props;
+    const { aggregationIndex } = this.state;
+    if (aggregationIndex != null) {
+      query
+        .updateAggregation(aggregationIndex, aggregation)
+        .update(null, { run: true });
+    } else {
+      query.aggregate(aggregation).update(null, { run: true });
+    }
+    this.handleClosePopover();
+  };
+
+  onChangeBreakout = breakout => {
+    const { query } = this.props;
+    const { breakoutIndex } = this.state;
+    if (breakoutIndex != null) {
+      query.updateBreakout(breakoutIndex, breakout).update(null, { run: true });
+    } else {
+      query.breakout(breakout).update(null, { run: true });
+    }
+    this.handleClosePopover();
+  };
+
+  getLeftSidebar = () => {
     const {
       question,
-      query,
-      card,
-      isDirty,
-      isResultDirty,
-      isLiveResizable,
-      runQuestionQuery,
-      databases,
-      isShowingTemplateTagsEditor,
-      isShowingDataReference,
-      isShowingNewbModal,
-      isShowingChartTypeSidebar,
       isShowingChartSettingsSidebar,
-      isShowingSummarySidebar,
-      isShowingFilterSidebar,
-      isShowingSnippetSidebar,
+      isShowingChartTypeSidebar,
       isShowingQuestionDetailsSidebar,
-      queryBuilderMode,
-      mode,
-      fitClassNames,
-      height,
       onOpenModal,
+      onCloseChartSettings,
+      onCloseChartType,
     } = this.props;
+
+    if (isShowingChartSettingsSidebar) {
+      return (
+        <ChartSettingsSidebar {...this.props} onClose={onCloseChartSettings} />
+      );
+    }
+
+    if (isShowingChartTypeSidebar) {
+      return <ChartTypeSidebar {...this.props} onClose={onCloseChartType} />;
+    }
+
+    if (isShowingQuestionDetailsSidebar) {
+      return (
+        <QuestionDetailsSidebar question={question} onOpenModal={onOpenModal} />
+      );
+    }
+
+    return null;
+  };
+
+  getRightSidebarForStructuredQuery = () => {
     const {
-      aggregationIndex,
-      aggregationPopoverTarget,
-      breakoutIndex,
-      breakoutPopoverTarget,
-    } = this.state;
+      question,
+      isResultDirty,
+      isShowingSummarySidebar,
+      isShowingFilterSidebar,
+      runQuestionQuery,
+      onCloseSummary,
+      onCloseFilter,
+    } = this.props;
 
-    // if we don't have a card at all or no databases then we are initializing, so keep it simple
-    if (!card || !databases) {
-      return <LoadingAndErrorWrapper className={fitClassNames} loading />;
+    if (isShowingSummarySidebar) {
+      return (
+        <SummarizeSidebar
+          question={question}
+          onClose={onCloseSummary}
+          isResultDirty={isResultDirty}
+          runQuestionQuery={runQuestionQuery}
+        />
+      );
     }
-    const queryMode = mode && mode.queryMode();
-    const ModeFooter = queryMode && queryMode.ModeFooter;
-    const isStructured = query instanceof StructuredQuery;
-    const isNative = query instanceof NativeQuery;
 
-    const isNewQuestion =
-      query instanceof StructuredQuery &&
-      !query.sourceTableId() &&
-      !query.sourceQuery();
+    if (isShowingFilterSidebar) {
+      return <FilterSidebar question={question} onClose={onCloseFilter} />;
+    }
 
-    if (isNewQuestion && queryBuilderMode === "view") {
+    return null;
+  };
+
+  getRightSidebarForNativeQuery = () => {
+    const {
+      isShowingTemplateTagsEditor,
+      isShowingDataReference,
+      isShowingSnippetSidebar,
+      toggleTemplateTagsEditor,
+      toggleDataReference,
+      toggleSnippetSidebar,
+    } = this.props;
+
+    if (isShowingTemplateTagsEditor) {
       return (
-        <div className={fitClassNames}>
-          <div className="p4 mx2">
-            <QuestionDataSelector
-              query={query}
-              triggerElement={
-                <Subhead className="mb2">{t`Pick your data`}</Subhead>
-              }
-            />
-          </div>
-        </div>
+        <TagEditorSidebar {...this.props} onClose={toggleTemplateTagsEditor} />
       );
     }
 
-    if (card.dataset && queryBuilderMode === "dataset") {
-      return <DatasetEditor {...this.props} />;
+    if (isShowingDataReference) {
+      return <DataReference {...this.props} onClose={toggleDataReference} />;
+    }
+
+    if (isShowingSnippetSidebar) {
+      return <SnippetSidebar {...this.props} onClose={toggleSnippetSidebar} />;
     }
 
+    return null;
+  };
+
+  getRightSidebar = () => {
+    const { question } = this.props;
+    const isStructured = question.isStructured();
+    return isStructured
+      ? this.getRightSidebarForStructuredQuery()
+      : this.getRightSidebarForNativeQuery();
+  };
+
+  renderHeader = () => {
+    const { query } = this.props;
+    const isStructured = query instanceof StructuredQuery;
+
+    const isNewQuestion =
+      isStructured && !query.sourceTableId() && !query.sourceQuery();
+
+    return (
+      <Motion
+        defaultStyle={isNewQuestion ? { opacity: 0 } : { opacity: 1 }}
+        style={isNewQuestion ? { opacity: spring(0) } : { opacity: spring(1) }}
+      >
+        {({ opacity }) => (
+          <QueryBuilderViewHeaderContainer>
+            <BorderedViewTitleHeader {...this.props} style={{ opacity }} />
+            {opacity < 1 && (
+              <NewQuestionHeader
+                className="spread"
+                style={{ opacity: 1 - opacity }}
+              />
+            )}
+          </QueryBuilderViewHeaderContainer>
+        )}
+      </Motion>
+    );
+  };
+
+  renderMain = ({ leftSidebar, rightSidebar }) => {
+    const {
+      query,
+      card,
+      mode,
+      parameters,
+      isDirty,
+      isLiveResizable,
+      isPreviewable,
+      isPreviewing,
+      height,
+      setParameterValue,
+      setIsPreviewing,
+    } = this.props;
+
+    const queryMode = mode && mode.queryMode();
+    const ModeFooter = queryMode && queryMode.ModeFooter;
+    const isStructured = query instanceof StructuredQuery;
+    const isNative = query instanceof NativeQuery;
+
     const topQuery = isStructured && query.topLevelQuery();
 
     // only allow editing of series for structured queries
@@ -161,203 +266,162 @@ export default class View extends React.Component {
     const onEditBreakout =
       topQuery && topQuery.hasBreakouts() ? this.handleEditBreakout : null;
 
-    const leftSideBar = isShowingChartSettingsSidebar ? (
-      <ChartSettingsSidebar
-        {...this.props}
-        onClose={this.props.onCloseChartSettings}
-      />
-    ) : isShowingChartTypeSidebar ? (
-      <ChartTypeSidebar {...this.props} onClose={this.props.onCloseChartType} />
-    ) : isShowingQuestionDetailsSidebar ? (
-      <QuestionDetailsSidebar question={question} onOpenModal={onOpenModal} />
-    ) : null;
-
-    const rightSideBar =
-      isStructured && isShowingSummarySidebar ? (
-        <SummarizeSidebar
-          question={question}
-          onClose={this.props.onCloseSummary}
-          isResultDirty={isResultDirty}
-          runQuestionQuery={runQuestionQuery}
-        />
-      ) : isStructured && isShowingFilterSidebar ? (
-        <FilterSidebar question={question} onClose={this.props.onCloseFilter} />
-      ) : isNative && isShowingTemplateTagsEditor ? (
-        <TagEditorSidebar
-          {...this.props}
-          onClose={this.props.toggleTemplateTagsEditor}
+    const isSidebarOpen = leftSidebar || rightSidebar;
+
+    return (
+      <QueryBuilderMain isSidebarOpen={isSidebarOpen}>
+        {isNative ? (
+          <NativeQueryEditorContainer className="hide sm-show">
+            <NativeQueryEditor
+              {...this.props}
+              viewHeight={height}
+              isOpen={!card.dataset_query.native.query || isDirty}
+              datasetQuery={card && card.dataset_query}
+            />
+          </NativeQueryEditorContainer>
+        ) : (
+          <StyledSyncedParametersList
+            parameters={parameters}
+            setParameterValue={setParameterValue}
+            commitImmediately
+          />
+        )}
+
+        <ViewSubHeader
+          isPreviewable={isPreviewable}
+          isPreviewing={isPreviewing}
+          setIsPreviewing={setIsPreviewing}
         />
-      ) : isNative && isShowingDataReference ? (
-        <DataReference
-          {...this.props}
-          onClose={this.props.toggleDataReference}
+
+        <StyledDebouncedFrame enabled={!isLiveResizable}>
+          <QueryVisualization
+            {...this.props}
+            noHeader
+            className="spread"
+            onAddSeries={onAddSeries}
+            onEditSeries={onEditSeries}
+            onRemoveSeries={onRemoveSeries}
+            onEditBreakout={onEditBreakout}
+          />
+        </StyledDebouncedFrame>
+
+        {ModeFooter && (
+          <ModeFooter {...this.props} className="flex-no-shrink" />
+        )}
+
+        <ViewFooter {...this.props} className="flex-no-shrink" />
+      </QueryBuilderMain>
+    );
+  };
+
+  renderAggregationPopover = () => {
+    const { query } = this.props;
+    const { aggregationPopoverTarget, aggregationIndex } = this.state;
+    return (
+      <Popover
+        isOpen={!!aggregationPopoverTarget}
+        target={aggregationPopoverTarget}
+        onClose={this.handleClosePopover}
+      >
+        <AggregationPopover
+          query={query}
+          aggregation={
+            aggregationIndex >= 0 ? query.aggregations()[aggregationIndex] : 0
+          }
+          onChangeAggregation={this.onChangeAggregation}
+          onClose={this.handleClosePopover}
         />
-      ) : isNative && isShowingSnippetSidebar ? (
-        <SnippetSidebar
-          {...this.props}
-          onClose={this.props.toggleSnippetSidebar}
+      </Popover>
+    );
+  };
+
+  renderBreakoutPopover = () => {
+    const { query } = this.props;
+    const { breakoutPopoverTarget, breakoutIndex } = this.state;
+    return (
+      <Popover
+        isOpen={!!breakoutPopoverTarget}
+        onClose={this.handleClosePopover}
+        target={breakoutPopoverTarget}
+      >
+        <BreakoutPopover
+          query={query}
+          breakout={breakoutIndex >= 0 ? query.breakouts()[breakoutIndex] : 0}
+          onChangeBreakout={this.onChangeBreakout}
+          onClose={this.handleClosePopover}
         />
-      ) : null;
+      </Popover>
+    );
+  };
+
+  render() {
+    const {
+      question,
+      query,
+      card,
+      databases,
+      isShowingNewbModal,
+      queryBuilderMode,
+      fitClassNames,
+      closeQbNewbModal,
+    } = this.props;
 
-    const isSidebarOpen = leftSideBar || rightSideBar;
+    // if we don't have a card at all or no databases then we are initializing, so keep it simple
+    if (!card || !databases) {
+      return <LoadingAndErrorWrapper className={fitClassNames} loading />;
+    }
+
+    const isStructured = query instanceof StructuredQuery;
+
+    const isNewQuestion =
+      isStructured && !query.sourceTableId() && !query.sourceQuery();
+
+    if (isNewQuestion && queryBuilderMode === "view") {
+      return <NewQuestionView query={query} fitClassNames={fitClassNames} />;
+    }
+
+    if (card.dataset && queryBuilderMode === "dataset") {
+      return <DatasetEditor {...this.props} />;
+    }
 
     const isNotebookContainerOpen =
       isNewQuestion || queryBuilderMode === "notebook";
 
+    const leftSidebar = this.getLeftSidebar();
+    const rightSidebar = this.getRightSidebar();
+
     return (
       <div className={fitClassNames}>
-        <div className={cx("QueryBuilder flex flex-column bg-white spread")}>
-          <Motion
-            defaultStyle={isNewQuestion ? { opacity: 0 } : { opacity: 1 }}
-            style={
-              isNewQuestion ? { opacity: spring(0) } : { opacity: spring(1) }
-            }
-          >
-            {({ opacity }) => (
-              <div className="flex-no-shrink z3 bg-white relative">
-                <ViewTitleHeader
-                  {...this.props}
-                  style={{ opacity }}
-                  py={1}
-                  className="border-bottom"
-                />
-                {opacity < 1 && (
-                  <NewQuestionHeader
-                    className="spread"
-                    style={{ opacity: 1 - opacity }}
-                  />
-                )}
-              </div>
-            )}
-          </Motion>
-
-          <div className="flex flex-full relative">
-            {query instanceof StructuredQuery && (
+        <QueryBuilderViewRoot className="QueryBuilder">
+          {this.renderHeader()}
+          <QueryBuilderContentContainer>
+            {isStructured && (
               <QueryViewNotebook
                 isNotebookContainerOpen={isNotebookContainerOpen}
                 {...this.props}
               />
             )}
-
-            <ViewSidebar side="left" isOpen={!!leftSideBar}>
-              {leftSideBar}
+            <ViewSidebar side="left" isOpen={!!leftSidebar}>
+              {leftSidebar}
             </ViewSidebar>
-
-            <div
-              className={cx("flex-full flex flex-column flex-basis-none", {
-                "hide sm-show": isSidebarOpen,
-              })}
-            >
-              {isNative ? (
-                <div className="z2 hide sm-show border-bottom mb2">
-                  <NativeQueryEditor
-                    {...this.props}
-                    viewHeight={height}
-                    isOpen={!card.dataset_query.native.query || isDirty}
-                    datasetQuery={card && card.dataset_query}
-                  />
-                </div>
-              ) : (
-                <SyncedParametersList
-                  className="mt2 ml3"
-                  parameters={this.props.parameters}
-                  setParameterValue={this.props.setParameterValue}
-                  commitImmediately
-                />
-              )}
-
-              <ViewSubHeader {...this.props} />
-
-              <DebouncedFrame
-                className="flex-full"
-                style={{ flexGrow: 1 }}
-                enabled={!isLiveResizable}
-              >
-                <QueryVisualization
-                  {...this.props}
-                  onAddSeries={onAddSeries}
-                  onEditSeries={onEditSeries}
-                  onRemoveSeries={onRemoveSeries}
-                  onEditBreakout={onEditBreakout}
-                  noHeader
-                  className="spread"
-                />
-              </DebouncedFrame>
-
-              {ModeFooter && (
-                <ModeFooter {...this.props} className="flex-no-shrink" />
-              )}
-
-              <ViewFooter {...this.props} className="flex-no-shrink" />
-            </div>
-
-            <ViewSidebar side="right" isOpen={!!rightSideBar}>
-              {rightSideBar}
+            {this.renderMain({ leftSidebar, rightSidebar })}
+            <ViewSidebar side="right" isOpen={!!rightSidebar}>
+              {rightSidebar}
             </ViewSidebar>
-          </div>
-        </div>
+          </QueryBuilderContentContainer>
+        </QueryBuilderViewRoot>
 
         {isShowingNewbModal && (
           <SavedQuestionIntroModal
-            question={this.props.question}
-            onClose={() => this.props.closeQbNewbModal()}
+            question={question}
+            onClose={() => closeQbNewbModal()}
           />
         )}
 
         <QueryModals {...this.props} />
 
-        {isStructured && (
-          <Popover
-            isOpen={!!aggregationPopoverTarget}
-            target={aggregationPopoverTarget}
-            onClose={this.handleClosePopover}
-          >
-            <AggregationPopover
-              query={query}
-              aggregation={
-                aggregationIndex >= 0
-                  ? query.aggregations()[aggregationIndex]
-                  : 0
-              }
-              onChangeAggregation={aggregation => {
-                if (aggregationIndex != null) {
-                  query
-                    .updateAggregation(aggregationIndex, aggregation)
-                    .update(null, { run: true });
-                } else {
-                  query.aggregate(aggregation).update(null, { run: true });
-                }
-                this.handleClosePopover();
-              }}
-              onClose={this.handleClosePopover}
-            />
-          </Popover>
-        )}
-        {isStructured && (
-          <Popover
-            isOpen={!!breakoutPopoverTarget}
-            onClose={this.handleClosePopover}
-            target={breakoutPopoverTarget}
-          >
-            <BreakoutPopover
-              query={query}
-              breakout={
-                breakoutIndex >= 0 ? query.breakouts()[breakoutIndex] : 0
-              }
-              onChangeBreakout={breakout => {
-                if (breakoutIndex != null) {
-                  query
-                    .updateBreakout(breakoutIndex, breakout)
-                    .update(null, { run: true });
-                } else {
-                  query.breakout(breakout).update(null, { run: true });
-                }
-                this.handleClosePopover();
-              }}
-              onClose={this.handleClosePopover}
-            />
-          </Popover>
-        )}
+        {isStructured && this.renderAggregationPopover()}
+        {isStructured && this.renderBreakoutPopover()}
       </div>
     );
   }
diff --git a/frontend/src/metabase/query_builder/components/view/View.styled.tsx b/frontend/src/metabase/query_builder/components/view/View.styled.tsx
new file mode 100644
index 00000000000..179b7bc39ff
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/View.styled.tsx
@@ -0,0 +1,71 @@
+import styled from "@emotion/styled";
+import { css } from "@emotion/react";
+
+import DebouncedFrame from "metabase/components/DebouncedFrame";
+import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
+
+import { color } from "metabase/lib/colors";
+import { breakpointMaxSmall } from "metabase/styled-components/theme/media-queries";
+
+import { ViewTitleHeader } from "./ViewHeader";
+
+export const QueryBuilderViewRoot = styled.div`
+  display: flex;
+  flex-direction: column;
+  background-color: ${color("bg-white")};
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+`;
+
+export const QueryBuilderContentContainer = styled.div`
+  display: flex;
+  flex: 1 0 auto;
+  position: relative;
+`;
+
+export const QueryBuilderMain = styled.main<{ isSidebarOpen: boolean }>`
+  display: flex;
+  flex-direction: column;
+  flex: 1 0 auto;
+  flex-basis: 0;
+
+  ${breakpointMaxSmall} {
+    ${props =>
+      props.isSidebarOpen &&
+      css`
+        display: none !important;
+      `};
+  }
+`;
+
+export const BorderedViewTitleHeader = styled(ViewTitleHeader)`
+  border-bottom: 1px solid ${color("border")};
+  padding-top: 8px;
+  padding-bottom: 8px;
+`;
+
+export const QueryBuilderViewHeaderContainer = styled.div`
+  flex-shrink: 0;
+  background-color: ${color("bg-white")};
+  position: relative;
+  z-index: 3;
+`;
+
+export const NativeQueryEditorContainer = styled.div`
+  margin-bottom: 1rem;
+  border-bottom: 1px solid ${color("border")};
+  z-index: 2;
+`;
+
+export const StyledDebouncedFrame = styled(DebouncedFrame)`
+  flex: 1 0 auto;
+  flex-grow: 1;
+`;
+
+export const StyledSyncedParametersList = styled(SyncedParametersList)`
+  margin-top: 1rem;
+  margin-left: 1.5rem;
+`;
diff --git a/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
new file mode 100644
index 00000000000..62bb064504a
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { t } from "ttag";
+
+import Subhead from "metabase/components/type/Subhead";
+import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
+
+import QuestionDataSelector from "../../QuestionDataSelector";
+
+type Props = {
+  query: StructuredQuery;
+  fitClassNames: string;
+};
+
+function NewQuestionView({ query, fitClassNames }: Props) {
+  return (
+    <div className={fitClassNames}>
+      <div className="p4 mx2">
+        <QuestionDataSelector
+          query={query}
+          triggerElement={
+            <Subhead className="mb2">{t`Pick your data`}</Subhead>
+          }
+        />
+      </div>
+    </div>
+  );
+}
+
+export default NewQuestionView;
diff --git a/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/index.ts b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/index.ts
new file mode 100644
index 00000000000..c8acd815501
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/index.ts
@@ -0,0 +1 @@
+export { default } from "./NewQuestionView";
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index a2024aeb865..837680b754e 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -1,17 +1,21 @@
 /* eslint-disable react/prop-types */
 import React, { Component } from "react";
 import { connect } from "react-redux";
+import { push } from "react-router-redux";
 import { t } from "ttag";
 import _ from "underscore";
 
-import fitViewport from "metabase/hoc/FitViewPort";
-
-import View from "../components/view/View";
-// import Notebook from "../components/notebook/Notebook";
+import Collections from "metabase/entities/collections";
+import { MetabaseApi } from "metabase/services";
+import { getMetadata } from "metabase/selectors/metadata";
+import { getUser, getUserIsAdmin } from "metabase/selectors/user";
 
+import fitViewport from "metabase/hoc/FitViewPort";
 import title from "metabase/hoc/Title";
 import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
 
+import View from "../components/view/View";
+
 import {
   getCard,
   getDatabasesList,
@@ -53,15 +57,7 @@ import {
   getNativeEditorCursorOffset,
   getNativeEditorSelectedText,
 } from "../selectors";
-
-import { getMetadata } from "metabase/selectors/metadata";
-import { getUser, getUserIsAdmin } from "metabase/selectors/user";
-
 import * as actions from "../actions";
-import { push } from "react-router-redux";
-
-import Collections from "metabase/entities/collections";
-import { MetabaseApi } from "metabase/services";
 
 function autocompleteResults(card, prefix) {
   const databaseId = card && card.dataset_query && card.dataset_query.database;
@@ -90,11 +86,9 @@ const mapStateToProps = (state, props) => {
 
     parameterValues: getParameterValues(state),
 
-    // TODO: data ref
     tableForeignKeys: getTableForeignKeys(state),
     tableForeignKeyReferences: getTableForeignKeyReferences(state),
 
-    // TODO: legacy
     card: getCard(state),
     originalCard: getOriginalCard(state),
     databases: getDatabasesList(state),
@@ -102,7 +96,6 @@ const mapStateToProps = (state, props) => {
     tables: getTables(state),
     tableMetadata: getTableMetadata(state),
 
-    // TODO: redundant, accessible through question
     query: getQuery(state),
     metadata: getMetadata(state),
 
@@ -161,8 +154,6 @@ const mapDispatchToProps = {
 export default class QueryBuilder extends Component {
   constructor(props, context) {
     super(props, context);
-
-    // TODO: React tells us that forceUpdate() is not the best thing to use, so ideally we can find a different way to trigger this
     this.forceUpdateDebounced = _.debounce(this.forceUpdate.bind(this), 400);
   }
 
@@ -204,14 +195,10 @@ export default class QueryBuilder extends Component {
   }
 
   componentWillUnmount() {
-    // cancel the query if one is running
     this.props.cancelQuery();
-
     window.removeEventListener("resize", this.handleResize);
-
     clearTimeout(this.timeout);
-
-    this.closeModal(); // close any modal that might be open
+    this.closeModal();
   }
 
   // When the window is resized we need to re-render, mainly so that our visualization pane updates
@@ -220,10 +207,10 @@ export default class QueryBuilder extends Component {
     this.forceUpdateDebounced();
   };
 
-  // NOTE: these were lifted from QueryHeader. Move to Redux?
   openModal = modal => {
     this.props.setUIControls({ modal });
   };
+
   closeModal = () => {
     this.props.setUIControls({ modal: null });
   };
@@ -259,15 +246,6 @@ export default class QueryBuilder extends Component {
     }
   };
 
-  resetStateOnTimeout = () => {
-    // clear any previously set timeouts then start a new one
-    clearTimeout(this.timeout);
-    this.timeout = setTimeout(() => {
-      this.props.onSetRecentlySaved(null);
-      this.timeout = null;
-    }, 5000);
-  };
-
   render() {
     const {
       uiControls: { modal, recentlySaved },
@@ -276,14 +254,11 @@ export default class QueryBuilder extends Component {
     return (
       <View
         {...this.props}
-        // NOTE: these were lifted from QueryHeader. Move to Redux?
         modal={modal}
         onOpenModal={this.openModal}
         onCloseModal={this.closeModal}
-        // recently saved indication
         recentlySaved={recentlySaved}
         onSetRecentlySaved={this.setRecentlySaved}
-        // save/create actions
         onSave={this.handleSave}
         onCreate={this.handleCreate}
         handleResize={this.handleResize}
-- 
GitLab