diff --git a/frontend/src/metabase-lib/metadata/Database.ts b/frontend/src/metabase-lib/metadata/Database.ts
index 1b9ce2113d67c6432ca61c16aa332fc1687c4863..d9f8a47c2b4156bf8c5c581d4434f121da835282 100644
--- a/frontend/src/metabase-lib/metadata/Database.ts
+++ b/frontend/src/metabase-lib/metadata/Database.ts
@@ -1,5 +1,6 @@
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-nocheck
+import { NativePermissions } from "metabase-types/api";
 import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
 import { createLookupByProperty, memoizeClass } from "metabase-lib/utils";
 import Question from "../Question";
@@ -25,6 +26,7 @@ class DatabaseInner extends Base {
   tables: Table[];
   schemas: Schema[];
   metadata: Metadata;
+  native_permissions: NativePermissions;
 
   // Only appears in  GET /api/database/:id
   "can-manage"?: boolean;
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index 658eaeb15923d3fd9664df31e564628d340156bd..288c3d3f1c0060cbceac271e1adb0109bd98449c 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -30,6 +30,7 @@ import NewEventModal from "metabase/timelines/questions/containers/NewEventModal
 import EditEventModal from "metabase/timelines/questions/containers/EditEventModal";
 import MoveEventModal from "metabase/timelines/questions/containers/MoveEventModal";
 import PreviewQueryModal from "metabase/query_builder/components/view/PreviewQueryModal";
+import ConvertQueryModal from "metabase/query_builder/components/view/ConvertQueryModal";
 import QuestionMoveToast from "./QuestionMoveToast";
 
 const mapDispatchToProps = {
@@ -68,6 +69,7 @@ class QueryModals extends React.Component {
       initialCollectionId,
       onCloseModal,
       onOpenModal,
+      updateQuestion,
       setQueryBuilderMode,
     } = this.props;
 
@@ -283,6 +285,14 @@ class QueryModals extends React.Component {
       <Modal fit onClose={onCloseModal}>
         <PreviewQueryModal question={question} onClose={onCloseModal} />
       </Modal>
+    ) : modal === MODAL_TYPES.CONVERT_QUERY ? (
+      <Modal fit onClose={onCloseModal}>
+        <ConvertQueryModal
+          question={question}
+          onUpdateQuestion={updateQuestion}
+          onClose={onCloseModal}
+        />
+      </Modal>
     ) : null;
   }
 }
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.styled.tsx b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ca84b51e840b21c65010baccf3c05fb8f4f79713
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.styled.tsx
@@ -0,0 +1,19 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+import Icon from "metabase/components/Icon";
+import IconButtonWrapper from "metabase/components/IconButtonWrapper";
+
+export const SqlButton = styled(IconButtonWrapper)`
+  color: ${color("text-dark")};
+  padding: 0.5rem;
+
+  &:hover {
+    color: ${color("brand")};
+    background-color: ${color("bg-medium")};
+  }
+`;
+
+export const SqlIcon = styled(Icon)`
+  width: 1rem;
+  height: 1rem;
+`;
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.tsx b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8ff0174e1dd997c20c6949e33ec685168d95bc4c
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/ConvertQueryButton.tsx
@@ -0,0 +1,57 @@
+import React, { useCallback } from "react";
+import { t } from "ttag";
+import { getEngineNativeType } from "metabase/lib/engine";
+import Tooltip from "metabase/components/Tooltip";
+import { MODAL_TYPES } from "metabase/query_builder/constants";
+import Question from "metabase-lib/Question";
+import { SqlButton, SqlIcon } from "./ConvertQueryButton.styled";
+
+const BUTTON_TOOLTIP = {
+  sql: t`View the SQL`,
+  json: t`View the native query`,
+};
+
+interface ConvertQueryButtonProps {
+  question: Question;
+  onOpenModal?: (modalType: string) => void;
+}
+
+const ConvertQueryButton = ({
+  question,
+  onOpenModal,
+}: ConvertQueryButtonProps): JSX.Element => {
+  const engineType = getEngineNativeType(question.database()?.engine);
+
+  const handleClick = useCallback(() => {
+    onOpenModal?.(MODAL_TYPES.CONVERT_QUERY);
+  }, [onOpenModal]);
+
+  return (
+    <Tooltip tooltip={BUTTON_TOOLTIP[engineType]} placement="bottom">
+      <SqlButton
+        onClick={handleClick}
+        data-metabase-event="Notebook Mode; Convert to SQL Click"
+      >
+        <SqlIcon name="sql" />
+      </SqlButton>
+    </Tooltip>
+  );
+};
+
+interface ConvertQueryButtonOpts {
+  question: Question;
+  queryBuilderMode: string;
+}
+
+ConvertQueryButton.shouldRender = ({
+  question,
+  queryBuilderMode,
+}: ConvertQueryButtonOpts) => {
+  return (
+    question.isStructured() &&
+    question.database()?.native_permissions === "write" &&
+    queryBuilderMode === "notebook"
+  );
+};
+
+export default ConvertQueryButton;
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/index.ts b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9722d7a4b043eb7bfa137bb80c3492f5b5fa531
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryButton/index.ts
@@ -0,0 +1 @@
+export { default } from "./ConvertQueryButton";
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.tsx b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9d4cb5f666de4206fe70fdab3e19066d9cbe2cac
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback } from "react";
+import { t } from "ttag";
+import { getEngineNativeType } from "metabase/lib/engine";
+import Button from "metabase/core/components/Button";
+import Question from "metabase-lib/Question";
+import NativeQueryModal, { useNativeQuery } from "../NativeQueryModal";
+
+const MODAL_TITLE = {
+  sql: t`SQL for this question`,
+  json: t`Native query for this question`,
+};
+
+const BUTTON_TITLE = {
+  sql: t`Convert this question to SQL`,
+  json: t`Convert this question to a native query`,
+};
+
+interface UpdateQuestionOpts {
+  shouldUpdateUrl?: boolean;
+}
+
+interface ConvertQueryModalProps {
+  question: Question;
+  onUpdateQuestion?: (question: Question, opts?: UpdateQuestionOpts) => void;
+  onClose?: () => void;
+}
+
+const ConvertQueryModal = ({
+  question,
+  onUpdateQuestion,
+  onClose,
+}: ConvertQueryModalProps): JSX.Element => {
+  const engineType = getEngineNativeType(question.database()?.engine);
+  const { query, error, isLoading } = useNativeQuery(question);
+
+  const handleConvertClick = useCallback(() => {
+    if (!query) {
+      return;
+    }
+
+    const newQuestion = question.setDatasetQuery({
+      type: "native",
+      native: { query, "template-tags": {} },
+      database: question.datasetQuery().database,
+    });
+
+    onUpdateQuestion?.(newQuestion, { shouldUpdateUrl: true });
+    onClose?.();
+  }, [question, query, onUpdateQuestion, onClose]);
+
+  return (
+    <NativeQueryModal
+      title={MODAL_TITLE[engineType]}
+      query={query}
+      error={error}
+      isLoading={isLoading}
+      onClose={onClose}
+    >
+      {query && (
+        <Button primary onClick={handleConvertClick}>
+          {BUTTON_TITLE[engineType]}
+        </Button>
+      )}
+    </NativeQueryModal>
+  );
+};
+
+export default ConvertQueryModal;
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.unit.spec.tsx b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..de0a1c4d6a18d915f873237391e569ff123f421a
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/ConvertQueryModal.unit.spec.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import xhrMock from "xhr-mock";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import Question from "metabase-lib/Question";
+import ConvertQueryModal from "./ConvertQueryModal";
+
+const SQL_QUERY = "SELECT 1";
+
+describe("ConvertQueryModal", () => {
+  beforeEach(() => {
+    xhrMock.setup();
+    mockNativeQuery();
+  });
+
+  afterEach(() => {
+    xhrMock.teardown();
+  });
+
+  it("should show a native query for a structured query", async () => {
+    const question = Question.create({ databaseId: 1 });
+
+    render(<ConvertQueryModal question={question} />);
+
+    expect(await screen.findByText(SQL_QUERY)).toBeInTheDocument();
+  });
+
+  it("should allow to convert a structured query to a native query", async () => {
+    const question = Question.create({ databaseId: 1 });
+    const onUpdateQuestion = jest.fn();
+
+    render(
+      <ConvertQueryModal
+        question={question}
+        onUpdateQuestion={onUpdateQuestion}
+      />,
+    );
+    userEvent.click(await screen.findByText("Convert this question to SQL"));
+
+    expect(onUpdateQuestion).toHaveBeenCalledWith(
+      question.setDatasetQuery({
+        type: "native",
+        database: 1,
+        native: {
+          query: SQL_QUERY,
+          "template-tags": {},
+        },
+      }),
+      { shouldUpdateUrl: true },
+    );
+  });
+});
+
+const mockNativeQuery = () => {
+  xhrMock.post("/api/dataset/native", {
+    status: 200,
+    body: JSON.stringify({
+      query: SQL_QUERY,
+    }),
+  });
+};
diff --git a/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/index.ts b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd5c4e141f6a6268d4a75f6f09c15ef0f741c368
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ConvertQueryModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./ConvertQueryModal";
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
deleted file mode 100644
index a6c867d898b05d85d2d1a6a9b3487cab24efa7f7..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t } from "ttag";
-import _ from "underscore";
-
-import Modal from "metabase/components/Modal";
-import Button from "metabase/core/components/Button";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import Tooltip from "metabase/components/Tooltip";
-import { formatNativeQuery, getEngineNativeType } from "metabase/lib/engine";
-import { MetabaseApi } from "metabase/services";
-
-import {
-  NativeCodeWrapper,
-  NativeCodeContainer,
-  SqlIconButton,
-} from "./NativeQueryButton.styled";
-
-const STRINGS = {
-  "": {
-    tooltip: t`View the native query`,
-    title: t`Native query for this question`,
-    button: t`Convert this question to a native query`,
-  },
-  sql: {
-    tooltip: t`View the SQL`,
-    title: t`SQL for this question`,
-    button: t`Convert this question to SQL`,
-  },
-};
-
-export default class NativeQueryButton extends React.Component {
-  state = {
-    open: false,
-    loading: false,
-    native: null,
-    datasetQuery: null,
-  };
-
-  handleOpen = async () => {
-    const { question } = this.props;
-    const datasetQuery = question.datasetQuery();
-    this.setState({ open: true });
-    if (!_.isEqual(datasetQuery, this.state.datasetQuery)) {
-      this.setState({ loading: true, error: null });
-      try {
-        const native = await MetabaseApi.native(datasetQuery);
-        this.setState({ loading: false, native, datasetQuery });
-      } catch (error) {
-        console.error(error);
-        this.setState({ loading: false, error });
-      }
-    }
-  };
-  handleClose = () => {
-    this.setState({ open: false });
-  };
-  handleConvert = () => {
-    const { question, updateQuestion } = this.props;
-
-    const newQuestion = question.setDatasetQuery({
-      type: "native",
-      native: { query: this.getFormattedQuery() },
-      database: this.state.datasetQuery.database,
-    });
-
-    updateQuestion(newQuestion, {
-      shouldUpdateUrl: true,
-    });
-  };
-
-  getFormattedQuery() {
-    const { question } = this.props;
-    const { native } = this.state;
-    return formatNativeQuery(
-      native && native.query,
-      question.database().engine,
-    );
-  }
-
-  render() {
-    const { question, size, ...props } = this.props;
-    const { loading, error } = this.state;
-
-    const engineType = getEngineNativeType(question.database().engine);
-    const { tooltip, title, button } =
-      STRINGS[engineType] || Object.values(STRINGS)[0];
-
-    return (
-      <span {...props}>
-        <Tooltip tooltip={tooltip} placement="bottom">
-          <SqlIconButton iconSize={size} onClick={this.handleOpen} />
-        </Tooltip>
-        <Modal
-          style={{ padding: "1em" }}
-          isOpen={this.state.open}
-          title={title}
-          footer={
-            loading || error ? null : (
-              <Button primary onClick={this.handleConvert}>
-                {button}
-              </Button>
-            )
-          }
-          onClose={this.handleClose}
-        >
-          <LoadingAndErrorWrapper loading={loading} error={error}>
-            <NativeCodeWrapper>
-              <NativeCodeContainer className="p2 sql-code">
-                {this.getFormattedQuery()}
-              </NativeCodeContainer>
-            </NativeCodeWrapper>
-          </LoadingAndErrorWrapper>
-        </Modal>
-      </span>
-    );
-  }
-}
-
-NativeQueryButton.shouldRender = ({ question, queryBuilderMode }) =>
-  queryBuilderMode === "notebook" &&
-  question.isStructured() &&
-  question.database() &&
-  question.database().native_permissions === "write";
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx
deleted file mode 100644
index 8fe7e3f47f30f0fdacbc313649cd9c357f67d361..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import styled from "@emotion/styled";
-
-import { color } from "metabase/lib/colors";
-import {
-  breakpointMinHeightExtraSmall,
-  breakpointMinHeightMedium,
-  breakpointMinHeightSmall,
-  space,
-} from "metabase/styled-components/theme";
-import Button from "metabase/core/components/Button";
-
-export const SqlIconButton = styled(Button)`
-  padding: ${space(1)};
-  border: none;
-  background-color: transparent;
-  color: ${color("text-dark")};
-  cursor: pointer;
-
-  :hover {
-    background-color: transparent;
-    color: ${color("brand")};
-  }
-`;
-
-SqlIconButton.defaultProps = {
-  icon: "sql",
-};
-
-export const NativeCodeWrapper = styled.pre`
-  box-sizing: border-box;
-  display: inline-block;
-  margin: 0;
-  padding: 0;
-  max-width: 100%;
-  overflow: hidden;
-  vertical-align: bottom;
-  width: 100%;
-`;
-
-export const NativeCodeContainer = styled.code`
-  box-sizing: border-box;
-  display: inline-block;
-  margin: 0;
-  max-height: 20vh;
-  max-width: 100%;
-  overflow: auto;
-  vertical-align: bottom;
-  white-space: pre;
-  width: 100%;
-  word-break: break-all;
-
-  ${breakpointMinHeightExtraSmall} {
-    max-height: 25vh;
-  }
-
-  ${breakpointMinHeightSmall} {
-    max-height: 45vh;
-  }
-
-  ${breakpointMinHeightMedium} {
-    max-height: 60vh;
-  }
-`;
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.styled.tsx b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..21a57fa2411fe7d294cf141225eba6292ff866b3
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.styled.tsx
@@ -0,0 +1,68 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+import Icon from "metabase/components/Icon";
+import LoadingSpinner from "metabase/components/LoadingSpinner";
+import IconButtonWrapper from "metabase/components/IconButtonWrapper";
+
+export const ModalRoot = styled.div`
+  display: flex;
+  flex-direction: column;
+  padding: 2rem;
+  min-width: 40rem;
+  max-width: 85vw;
+  min-height: 20rem;
+  max-height: 90vh;
+`;
+
+export const ModalHeader = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 1.5rem;
+`;
+
+export const ModalBody = styled.div`
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: column;
+  min-height: 0;
+`;
+
+export const ModalFooter = styled.div`
+  display: flex;
+  justify-content: end;
+  margin-top: 1.5rem;
+`;
+
+export const ModalTitle = styled.div`
+  flex: 1 1 auto;
+  color: ${color("text-dark")};
+  font-size: 1.25rem;
+  line-height: 1.5rem;
+  font-weight: bold;
+`;
+
+export const ModalWarningIcon = styled(Icon)`
+  flex: 0 0 auto;
+  color: ${color("error")};
+  width: 1rem;
+  height: 1rem;
+  margin-right: 0.75rem;
+`;
+
+export const ModalCloseButton = styled(IconButtonWrapper)`
+  flex: 0 0 auto;
+  margin-left: 1rem;
+`;
+
+export const ModalCloseIcon = styled(Icon)`
+  color: ${color("text-light")};
+`;
+
+export const ModalLoadingSpinner = styled(LoadingSpinner)`
+  color: ${color("brand")};
+`;
+
+export const ModalDivider = styled.div`
+  border-top: 1px solid ${color("border")};
+  margin-bottom: 1.5rem;
+`;
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.tsx b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7805f64368a093833b67a896a7e334b7a09f311d
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/NativeQueryModal.tsx
@@ -0,0 +1,60 @@
+import React, { ReactNode } from "react";
+import { t } from "ttag";
+import NativeCodePanel from "../NativeCodePanel";
+import {
+  ModalBody,
+  ModalCloseButton,
+  ModalCloseIcon,
+  ModalDivider,
+  ModalFooter,
+  ModalHeader,
+  ModalLoadingSpinner,
+  ModalRoot,
+  ModalTitle,
+  ModalWarningIcon,
+} from "./NativeQueryModal.styled";
+
+interface NativeQueryModalProps {
+  title: string;
+  query?: string;
+  error?: string;
+  isLoading?: boolean;
+  children?: ReactNode;
+  onClose?: () => void;
+}
+
+const NativeQueryModal = ({
+  title,
+  query,
+  error,
+  isLoading,
+  children,
+  onClose,
+}: NativeQueryModalProps): JSX.Element => {
+  return (
+    <ModalRoot>
+      <ModalHeader>
+        {error && <ModalWarningIcon name="warning" />}
+        <ModalTitle>
+          {error ? t`An error occurred in your query` : title}
+        </ModalTitle>
+        <ModalCloseButton>
+          <ModalCloseIcon name="close" onClick={onClose} />
+        </ModalCloseButton>
+      </ModalHeader>
+      {error && <ModalDivider />}
+      <ModalBody>
+        {isLoading ? (
+          <ModalLoadingSpinner />
+        ) : error ? (
+          <NativeCodePanel value={error} isHighlighted />
+        ) : query ? (
+          <NativeCodePanel value={query} isCopyEnabled />
+        ) : null}
+      </ModalBody>
+      {children && <ModalFooter>{children}</ModalFooter>}
+    </ModalRoot>
+  );
+};
+
+export default NativeQueryModal;
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryModal/index.ts b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96bb309650dcb4a58e44705d94df72615dde884c
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./NativeQueryModal";
+export { useNativeQuery } from "./use-native-query";
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryModal/use-native-query.ts b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/use-native-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47e6a9a4d26a2f0593f9d88829703f164e457c5c
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryModal/use-native-query.ts
@@ -0,0 +1,37 @@
+import { useEffect, useState } from "react";
+import { getIn } from "icepick";
+import { formatNativeQuery } from "metabase/lib/engine";
+import { NativeQueryForm } from "metabase-types/api";
+import Question from "metabase-lib/Question";
+
+interface UseNativeQuery {
+  query?: string;
+  error?: string;
+  isLoading: boolean;
+}
+
+export const useNativeQuery = (question: Question) => {
+  const [state, setState] = useState<UseNativeQuery>({ isLoading: true });
+
+  useEffect(() => {
+    question
+      .apiGetNativeQueryForm()
+      .then(data =>
+        setState({ query: getQuery(question, data), isLoading: false }),
+      )
+      .catch(error =>
+        setState({ error: getErrorMessage(error), isLoading: false }),
+      );
+  }, [question]);
+
+  return state;
+};
+
+const getQuery = (question: Question, data: NativeQueryForm) => {
+  const engine = question.database()?.engine;
+  return formatNativeQuery(data.query, engine);
+};
+
+const getErrorMessage = (error: unknown): string | undefined => {
+  return getIn(error, ["data", "message"]);
+};
diff --git a/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.styled.tsx b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.styled.tsx
index 21a57fa2411fe7d294cf141225eba6292ff866b3..c144c5858be7ef95cd27f57c8759799970d75680 100644
--- a/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.styled.tsx
@@ -1,68 +1,15 @@
 import styled from "@emotion/styled";
 import { color } from "metabase/lib/colors";
-import Icon from "metabase/components/Icon";
-import LoadingSpinner from "metabase/components/LoadingSpinner";
-import IconButtonWrapper from "metabase/components/IconButtonWrapper";
+import ExternalLink from "metabase/core/components/ExternalLink";
 
-export const ModalRoot = styled.div`
-  display: flex;
-  flex-direction: column;
-  padding: 2rem;
-  min-width: 40rem;
-  max-width: 85vw;
-  min-height: 20rem;
-  max-height: 90vh;
-`;
-
-export const ModalHeader = styled.div`
-  display: flex;
-  align-items: center;
-  margin-bottom: 1.5rem;
-`;
-
-export const ModalBody = styled.div`
-  display: flex;
-  flex: 1 1 auto;
-  flex-direction: column;
-  min-height: 0;
-`;
-
-export const ModalFooter = styled.div`
-  display: flex;
-  justify-content: end;
-  margin-top: 1.5rem;
-`;
-
-export const ModalTitle = styled.div`
-  flex: 1 1 auto;
-  color: ${color("text-dark")};
-  font-size: 1.25rem;
-  line-height: 1.5rem;
-  font-weight: bold;
-`;
-
-export const ModalWarningIcon = styled(Icon)`
-  flex: 0 0 auto;
-  color: ${color("error")};
-  width: 1rem;
-  height: 1rem;
-  margin-right: 0.75rem;
-`;
-
-export const ModalCloseButton = styled(IconButtonWrapper)`
-  flex: 0 0 auto;
-  margin-left: 1rem;
-`;
-
-export const ModalCloseIcon = styled(Icon)`
-  color: ${color("text-light")};
-`;
-
-export const ModalLoadingSpinner = styled(LoadingSpinner)`
+export const ModalExternalLink = styled(ExternalLink)`
   color: ${color("brand")};
-`;
+  font-size: 0.75rem;
+  line-height: 1rem;
+  font-weight: bold;
+  text-decoration: none;
 
-export const ModalDivider = styled.div`
-  border-top: 1px solid ${color("border")};
-  margin-bottom: 1.5rem;
+  &:hover {
+    text-decoration: underline;
+  }
 `;
diff --git a/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.tsx b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.tsx
index 79504202bb3037528cefa4abb0fa54fcbb613e9a..d0cc8d000efbbadfdc8800a3366889240153936d 100644
--- a/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.tsx
+++ b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.tsx
@@ -1,24 +1,9 @@
-import React, { useEffect, useMemo, useState } from "react";
-import { getIn } from "icepick";
+import React from "react";
 import { t } from "ttag";
-import { formatNativeQuery } from "metabase/lib/engine";
 import MetabaseSettings from "metabase/lib/settings";
-import ExternalLink from "metabase/core/components/ExternalLink";
-import { NativeQueryForm } from "metabase-types/api";
 import Question from "metabase-lib/Question";
-import NativeCodePanel from "../NativeCodePanel";
-import {
-  ModalBody,
-  ModalCloseButton,
-  ModalCloseIcon,
-  ModalDivider,
-  ModalFooter,
-  ModalHeader,
-  ModalLoadingSpinner,
-  ModalRoot,
-  ModalTitle,
-  ModalWarningIcon,
-} from "./PreviewQueryModal.styled";
+import NativeQueryModal, { useNativeQuery } from "../NativeQueryModal";
+import { ModalExternalLink } from "./PreviewQueryModal.styled";
 
 interface PreviewQueryModalProps {
   question: Question;
@@ -29,72 +14,24 @@ const PreviewQueryModal = ({
   question,
   onClose,
 }: PreviewQueryModalProps): JSX.Element => {
-  const { data, error, isLoading } = useNativeQuery(question);
-
-  const queryText = useMemo(() => {
-    const engine = question.database()?.engine;
-    return data ? formatNativeQuery(data.query, engine) : undefined;
-  }, [question, data]);
-
-  const errorText = useMemo(() => {
-    return error ? getErrorMessage(error) : undefined;
-  }, [error]);
+  const { query, error, isLoading } = useNativeQuery(question);
+  const learnUrl = MetabaseSettings.learnUrl("debugging-sql/sql-syntax");
 
   return (
-    <ModalRoot>
-      <ModalHeader>
-        {error && <ModalWarningIcon name="warning" />}
-        <ModalTitle>
-          {error ? t`An error occurred in your query` : t`Query preview`}
-        </ModalTitle>
-        <ModalCloseButton>
-          <ModalCloseIcon name="close" onClick={onClose} />
-        </ModalCloseButton>
-      </ModalHeader>
-      {error && <ModalDivider />}
-      <ModalBody>
-        {isLoading ? (
-          <ModalLoadingSpinner />
-        ) : errorText ? (
-          <NativeCodePanel value={errorText} isHighlighted />
-        ) : queryText ? (
-          <NativeCodePanel value={queryText} isCopyEnabled />
-        ) : undefined}
-      </ModalBody>
+    <NativeQueryModal
+      title={t`Query preview`}
+      query={query}
+      error={error}
+      isLoading={isLoading}
+      onClose={onClose}
+    >
       {error && (
-        <ModalFooter>
-          <ExternalLink
-            href={MetabaseSettings.learnUrl("debugging-sql/sql-syntax")}
-          >
-            {t`Learn how to debug SQL errors`}
-          </ExternalLink>
-        </ModalFooter>
+        <ModalExternalLink href={learnUrl}>
+          {t`Learn how to debug SQL errors`}
+        </ModalExternalLink>
       )}
-    </ModalRoot>
+    </NativeQueryModal>
   );
 };
 
-interface UseNativeQuery {
-  data?: NativeQueryForm;
-  error?: unknown;
-  isLoading: boolean;
-}
-
-const useNativeQuery = (question: Question) => {
-  const [state, setState] = useState<UseNativeQuery>({ isLoading: true });
-
-  useEffect(() => {
-    question
-      .apiGetNativeQueryForm()
-      .then(data => setState({ data, isLoading: false }))
-      .catch(error => setState({ isLoading: false, error }));
-  }, [question]);
-
-  return state;
-};
-
-const getErrorMessage = (error: unknown): string | undefined => {
-  return getIn(error, ["data", "message"]);
-};
-
 export default PreviewQueryModal;
diff --git a/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.unit.spec.tsx b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..954c0556eb18ba5d6291cfac53b0b6e6628c37c2
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/PreviewQueryModal/PreviewQueryModal.unit.spec.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import xhrMock from "xhr-mock";
+import { render, screen } from "@testing-library/react";
+import Question from "metabase-lib/Question";
+import PreviewQueryModal from "./PreviewQueryModal";
+
+const SQL_QUERY = "SELECT 1";
+const SQL_QUERY_ERROR = "Cannot run the query";
+
+describe("PreviewQueryModal", () => {
+  beforeEach(() => {
+    xhrMock.setup();
+  });
+
+  afterEach(() => {
+    xhrMock.teardown();
+  });
+
+  it("should show a fully parameterized native query", async () => {
+    const question = Question.create({ databaseId: 1 });
+    mockNativeQuery();
+
+    render(<PreviewQueryModal question={question} />);
+
+    expect(await screen.findByText(SQL_QUERY)).toBeInTheDocument();
+  });
+
+  it("should show a native query error", async () => {
+    const question = Question.create({ databaseId: 1 });
+    mockNativeQueryError();
+
+    render(<PreviewQueryModal question={question} />);
+
+    expect(await screen.findByText(SQL_QUERY_ERROR)).toBeInTheDocument();
+  });
+});
+
+const mockNativeQuery = () => {
+  xhrMock.post("/api/dataset/native", {
+    status: 200,
+    body: JSON.stringify({
+      query: "SELECT 1",
+    }),
+  });
+};
+
+const mockNativeQueryError = () => {
+  xhrMock.post("/api/dataset/native", {
+    status: 500,
+    body: JSON.stringify({
+      message: SQL_QUERY_ERROR,
+    }),
+  });
+};
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
index 84ae8f94b1a1828c76a22f8a60ce729e54ffa6d8..9844efb9bbf22bee12ccb95dbf40afcb384af0af 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
@@ -24,13 +24,13 @@ import { HeadBreadcrumbs } from "./HeaderBreadcrumbs";
 import QuestionDataSource from "./QuestionDataSource";
 import QuestionDescription from "./QuestionDescription";
 import QuestionNotebookButton from "./QuestionNotebookButton";
+import ConvertQueryButton from "./ConvertQueryButton";
 import QuestionFilters, {
   FilterHeaderToggle,
   FilterHeader,
   QuestionFilterWidget,
 } from "./QuestionFilters";
 import { QuestionSummarizeWidget } from "./QuestionSummaries";
-import NativeQueryButton from "./NativeQueryButton";
 import {
   AdHocViewHeading,
   SaveButton,
@@ -371,7 +371,6 @@ function ViewTitleHeaderRightSide(props) {
     isResultDirty,
     isActionListVisible,
     runQuestionQuery,
-    updateQuestion,
     cancelQuery,
     onOpenModal,
     onEditSummary,
@@ -463,15 +462,8 @@ function ViewTitleHeaderRightSide(props) {
           />
         </ViewHeaderIconButtonContainer>
       )}
-      {NativeQueryButton.shouldRender(props) && (
-        <ViewHeaderIconButtonContainer>
-          <NativeQueryButton
-            size={16}
-            question={question}
-            updateQuestion={updateQuestion}
-            data-metabase-event="Notebook Mode; Convert to SQL Click"
-          />
-        </ViewHeaderIconButtonContainer>
+      {ConvertQueryButton.shouldRender(props) && (
+        <ConvertQueryButton question={question} onOpenModal={onOpenModal} />
       )}
       {hasExploreResultsLink && <ExploreResultsLink question={question} />}
       {hasRunButton && !isShowingNotebook && (
diff --git a/frontend/src/metabase/query_builder/constants.js b/frontend/src/metabase/query_builder/constants.js
index 8bc11fe7dad0b2c805f1c3f163767251db362b73..f0f45732e5cac366c06bd453b789d4134c17b85d 100644
--- a/frontend/src/metabase/query_builder/constants.js
+++ b/frontend/src/metabase/query_builder/constants.js
@@ -18,6 +18,7 @@ export const MODAL_TYPES = {
   EDIT_EVENT: "edit-event",
   MOVE_EVENT: "move-event",
   PREVIEW_QUERY: "preview-query",
+  CONVERT_QUERY: "convert-query",
 };
 
 export const SIDEBAR_SIZES = {