diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.styled.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f032c8e52cef664ad95038213fd3f67eafbde04c
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.styled.tsx
@@ -0,0 +1,52 @@
+import styled from "@emotion/styled";
+import Button from "metabase/core/components/Button";
+import Input from "metabase/core/components/Input";
+
+import { color } from "metabase/lib/colors";
+
+export const QuestionCacheSectionRoot = styled.div`
+  display: flex;
+  flex-direction: column;
+
+  ${Button.Root} {
+    padding: 0;
+  }
+
+  ${Button.Content} {
+    justify-content: start;
+  }
+`;
+
+export const Text = styled.span`
+  font-weight: 700;
+  font-size: 0.875rem;
+  line-height: 1rem;
+  margin: 0.5rem 0rem;
+`;
+
+export const CachePopover = styled.div`
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+  align-items: end;
+
+  ${Text} {
+    margin-top: 0;
+    margin-bottom: 1.5rem;
+  }
+
+  ${Input.Field} {
+    width: 40px;
+    height: 32px;
+    font-size: 0.875rem;
+    line-height: 1rem;
+    padding: 0.625rem;
+    margin: 0 0.5rem;
+
+    border: 1px solid ${color("border")};
+  }
+
+  ${Button.Root} {
+    width: 120px;
+  }
+`;
diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e70d54ae60409f9232d3730a094c328947822a7
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/QuestionCacheSection.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback, useState } from "react";
+import { t } from "ttag";
+
+import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger";
+import Button from "metabase/core/components/Button";
+import ActionButton from "metabase/components/ActionButton";
+import NumericInput from "metabase/core/components/NumericInput";
+
+import Question from "metabase-lib/lib/Question";
+import { color } from "metabase/lib/colors";
+
+import { normalizeCacheTTL } from "../../utils";
+
+import {
+  Text,
+  QuestionCacheSectionRoot,
+  CachePopover,
+} from "./QuestionCacheSection.styled";
+
+interface QuestionCacheSectionProps {
+  question: Question;
+  onSave: (cache_ttl: number | null) => Promise<Question>;
+}
+
+export const QuestionCacheSection = ({
+  question,
+  onSave,
+}: QuestionCacheSectionProps) => {
+  const [cacheTTL, setCacheTTL] = useState<number | null>(question.cacheTTL());
+
+  const handleChange = useCallback(
+    number => {
+      setCacheTTL(normalizeCacheTTL(number));
+    },
+    [setCacheTTL],
+  );
+
+  const handleSave = useCallback(async () => {
+    return await onSave(cacheTTL);
+  }, [onSave, cacheTTL]);
+
+  return (
+    <QuestionCacheSectionRoot>
+      <TippyPopoverWithTrigger
+        key="extra-actions-menu"
+        placement="bottom-start"
+        renderTrigger={({ onClick, visible }) => (
+          <Button
+            borderless
+            color={color("brand")}
+            onClick={onClick}
+            iconRight={visible ? "chevronup" : "chevrondown"}
+          >
+            {t`Cache Configuration`}
+          </Button>
+        )}
+        popoverContent={
+          <CachePopover>
+            <Text>
+              {t`Cache results for`}
+              <NumericInput
+                placeholder="24"
+                value={cacheTTL || ""}
+                onChange={handleChange}
+              />
+              {t`hours`}
+            </Text>
+            <ActionButton
+              primary
+              actionFn={handleSave}
+            >{t`Save changes`}</ActionButton>
+          </CachePopover>
+        }
+      />
+    </QuestionCacheSectionRoot>
+  );
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/index.js b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a9bc9fc8c5ecfafa8c0b432e8dca6e4ff165b78
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/caching/components/QuestionCacheSection/index.js
@@ -0,0 +1 @@
+export * from "./QuestionCacheSection";
diff --git a/enterprise/frontend/src/metabase-enterprise/caching/index.js b/enterprise/frontend/src/metabase-enterprise/caching/index.js
index 10e3c9694be8d8b3ebd09448497d98ba949b7a6d..4ff9b9c3dec62fc1b34cc53a4e0166eb97e9b050 100644
--- a/enterprise/frontend/src/metabase-enterprise/caching/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/caching/index.js
@@ -6,6 +6,8 @@ import Link from "metabase/core/components/Link";
 import { CacheTTLField } from "./components/CacheTTLField";
 import { DatabaseCacheTTLField } from "./components/DatabaseCacheTTLField";
 import { QuestionCacheTTLField } from "./components/QuestionCacheTTLField";
+import { QuestionCacheSection } from "./components/QuestionCacheSection";
+
 import {
   getQuestionsImplicitCacheTTL,
   validateCacheTTL,
@@ -49,4 +51,6 @@ if (hasPremiumFeature("advanced_config")) {
   PLUGIN_FORM_WIDGETS.questionCacheTTL = QuestionCacheTTLField;
 
   PLUGIN_CACHING.getQuestionsImplicitCacheTTL = getQuestionsImplicitCacheTTL;
+  PLUGIN_CACHING.QuestionCacheSection = QuestionCacheSection;
+  PLUGIN_CACHING.isEnabled = () => true;
 }
diff --git a/enterprise/frontend/src/metabase-enterprise/caching/utils.js b/enterprise/frontend/src/metabase-enterprise/caching/utils.js
index 525ff74b75e1b5e744b354cf50a383047fdee0a9..0186b40ac14f300bd6173cd27440f95aabe185ba 100644
--- a/enterprise/frontend/src/metabase-enterprise/caching/utils.js
+++ b/enterprise/frontend/src/metabase-enterprise/caching/utils.js
@@ -52,5 +52,5 @@ export function validateCacheTTL(value) {
 }
 
 export function normalizeCacheTTL(value) {
-  return value === 0 ? null : value;
+  return value === 0 || value === undefined ? null : value;
 }
diff --git a/frontend/src/metabase-lib/lib/Question.ts b/frontend/src/metabase-lib/lib/Question.ts
index 32de0f573f9f48ff7b004dde055ac98aee8d6813..e5125faa1c33b47de273830f5924087af1a29175 100644
--- a/frontend/src/metabase-lib/lib/Question.ts
+++ b/frontend/src/metabase-lib/lib/Question.ts
@@ -284,6 +284,14 @@ class QuestionInner {
     return this.setCard(assoc(this.card(), "display", display));
   }
 
+  cacheTTL(): number | null {
+    return this._card?.cache_ttl;
+  }
+
+  setCacheTTL(cache) {
+    return this.setCard(assoc(this.card(), "cache_ttl", cache));
+  }
+
   /**
    * returns whether this question is a model
    * @returns boolean
@@ -861,10 +869,14 @@ class QuestionInner {
     return this.setCard(card);
   }
 
-  description(): string | null | undefined {
+  description(): string | null {
     return this._card && this._card.description;
   }
 
+  setDescription(description) {
+    return this.setCard(assoc(this.card(), "description", description));
+  }
+
   lastEditInfo() {
     return this._card && this._card["last-edit-info"];
   }
diff --git a/frontend/src/metabase-types/types/Card.ts b/frontend/src/metabase-types/types/Card.ts
index a49e8f7e60ac837effb6ec450645d5a8f4a1dce3..56a729116418858a7ae3be84ea8d30bdf359d583 100644
--- a/frontend/src/metabase-types/types/Card.ts
+++ b/frontend/src/metabase-types/types/Card.ts
@@ -22,11 +22,12 @@ export type UnsavedCard<Query = DatasetQuery> = {
 export type SavedCard<Query = DatasetQuery> = UnsavedCard<Query> & {
   id: CardId;
   name?: string;
-  description?: string;
+  description?: string | null;
   dataset?: boolean;
   can_write: boolean;
   public_uuid: string;
   archived?: boolean;
+  cache_ttl?: number | null;
 };
 
 export type Card<Query = DatasetQuery> = SavedCard<Query> | UnsavedCard<Query>;
diff --git a/frontend/src/metabase/lib/keyboard.js b/frontend/src/metabase/lib/keyboard.js
index 0cbc6a402aeec4d8aa2bbea9f2448490b75b505c..5a189d24c515a9edae34b5a866b4151afbe24014 100644
--- a/frontend/src/metabase/lib/keyboard.js
+++ b/frontend/src/metabase/lib/keyboard.js
@@ -13,3 +13,4 @@ export const KEY_COMMA = ",";
 export const KEYCODE_FORWARD_SLASH = 191;
 
 export const KEY_ESCAPE = "Escape";
+export const KEY_ENTER = "Enter";
diff --git a/frontend/src/metabase/lib/settings.ts b/frontend/src/metabase/lib/settings.ts
index 5e141951bc2aa20bddea44b33ba3983aa4697856..f15a27ccdd54e67640c910bb73afcbfa00bd78f2 100644
--- a/frontend/src/metabase/lib/settings.ts
+++ b/frontend/src/metabase/lib/settings.ts
@@ -96,7 +96,8 @@ export type SettingName =
   | "premium-embedding-token"
   | "metabase-store-managed"
   | "application-font"
-  | "available-fonts";
+  | "available-fonts"
+  | "enable-query-caching";
 
 type SettingsMap = Record<SettingName, any>; // provides access to Metabase application settings
 
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 3716aa15f9b735c48d84cf3b51b09e9f710b3ec1..2c6a1c851761b39d98ec8c9b019171371c676171 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -128,7 +128,9 @@ export const PLUGIN_CACHING = {
   dashboardCacheTTLFormField: null,
   databaseCacheTTLFormField: null,
   questionCacheTTLFormField: null,
-  getQuestionsImplicitCacheTTL: () => null,
+  getQuestionsImplicitCacheTTL: (question?: any) => null,
+  QuestionCacheSection: PluginPlaceholder,
+  isEnabled: () => false,
 };
 
 export const PLUGIN_REDUCERS: { applicationPermissionsPlugin: any } = {
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx
index cf96f5de8d0ce4f28a419cca805a7977c469c181..26bf85ca73161a7a059039060e2159da7e77cf10 100644
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx
+++ b/frontend/src/metabase/query_builder/components/EditableText/EditableText.stories.tsx
@@ -10,7 +10,8 @@ export default {
 
 const Template: ComponentStory<typeof EditableText> = args => {
   const [{ initialValue }, updateArgs] = useArgs();
-  const handleChange = (value: string) => updateArgs({ initialValue: value });
+  const handleChange = (value: string | null | undefined) =>
+    updateArgs({ initialValue: value });
 
   console.log(initialValue);
 
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx
index 499408f3e81e997979e4b7d536b56b599b679422..e5c86366b38dd8947b65f17e752b52996cb39076 100644
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/EditableText/EditableText.styled.tsx
@@ -2,7 +2,7 @@ import styled from "@emotion/styled";
 import { color } from "metabase/lib/colors";
 import { css } from "@emotion/react";
 
-const sharedStyle = css`
+export const SharedStyles = css`
   border: 1px solid transparent;
   border-radius: 0.25rem;
   padding: 0.5rem;
@@ -13,15 +13,19 @@ const sharedStyle = css`
   color: ${color("text-dark")};
 `;
 
-export const EditableTextRoot = styled.div`
+interface EditableTextRootProps {
+  value: string | null;
+}
+
+export const EditableTextRoot = styled.div<EditableTextRootProps>`
   display: grid;
-  max-width: 300px;
+  max-width: 500px;
 
-  &::after {
-    content: attr(data-replicated-value) " ";
+  &:after {
+    content: "${props => props.value?.replace(/\n/g, "\\00000a")} ";
     white-space: pre-wrap;
     visibility: hidden;
-    ${sharedStyle}
+    ${SharedStyles}
   }
 `;
 
@@ -39,5 +43,5 @@ export const EditableTextArea = styled.textarea`
     cursor: text;
   }
 
-  ${sharedStyle}
+  ${SharedStyles}
 `;
diff --git a/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx b/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx
index 32634d0093df767d47d4d9499a544019c14b0a8c..7a7c01d3dbb65e4750153e47eb70f11374b5a8be 100644
--- a/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx
+++ b/frontend/src/metabase/query_builder/components/EditableText/EditableText.tsx
@@ -1,26 +1,41 @@
-import React, { useState } from "react";
+import React, { useState, useRef } from "react";
+import { KEY_ESCAPE, KEY_ENTER } from "metabase/lib/keyboard";
 
-import { EditableTextRoot, EditableTextArea } from "./EditableText.styled";
-
-import { KEY_ESCAPE } from "metabase/lib/keyboard";
-
-type TEXT = string | null | undefined;
+import {
+  EditableTextRoot,
+  EditableTextArea,
+  SharedStyles,
+} from "./EditableText.styled";
 
 interface EditableTextProps {
-  initialValue: TEXT;
-  onChange?: (val: string) => void;
+  initialValue: string | null;
+  onChange?: (val: string | null) => void;
+  submitOnEnter?: boolean;
+  "data-testid"?: string;
+  placeholder?: string;
 }
 
-const EditableText = ({ initialValue, onChange }: EditableTextProps) => {
-  const [value, setValue] = useState<TEXT>(initialValue);
+const EditableText = ({
+  initialValue,
+  onChange,
+  submitOnEnter,
+  "data-testid": dataTestid,
+  placeholder,
+}: EditableTextProps) => {
+  const [value, setValue] = useState<string | null>(initialValue);
+  const textArea = useRef<HTMLTextAreaElement>(null);
 
   const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
     setValue(e.target.value);
   };
 
   const handleBlur = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    if (onChange) {
-      onChange(e.target.value);
+    const {
+      target: { value },
+    } = e;
+
+    if (onChange && value !== initialValue) {
+      onChange(value);
     }
   };
 
@@ -28,19 +43,31 @@ const EditableText = ({ initialValue, onChange }: EditableTextProps) => {
     if (e.key === KEY_ESCAPE) {
       setValue(initialValue);
     }
+    if (e.key === KEY_ENTER && submitOnEnter) {
+      textArea.current?.blur();
+      e.preventDefault();
+    }
   };
 
   return (
-    <EditableTextRoot data-replicated-value={value}>
+    <EditableTextRoot value={value}>
       <EditableTextArea
-        placeholder="Description"
-        value={value || undefined}
+        placeholder={placeholder}
+        value={value || ""}
         onChange={handleChange}
         onBlur={handleBlur}
         onKeyDown={handleKeyDown}
+        rows={1}
+        cols={1}
+        ref={textArea}
+        data-testid={dataTestid}
       />
     </EditableTextRoot>
   );
 };
 
-export default EditableText;
+export default Object.assign(EditableText, {
+  SharedStyles,
+  Root: EditableTextRoot,
+  TextArea: EditableTextArea,
+});
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
index 1e7ff1d39a46d28f4b57627f4a371d511c0487d5..5b9d16eb9d2598b051a311550813bd6f87987f60 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.jsx
@@ -1,35 +1,51 @@
 import React from "react";
+import { t } from "ttag";
 import PropTypes from "prop-types";
 
 import { PLUGIN_MODERATION } from "metabase/plugins";
-import { HeaderButton } from "./SavedQuestionHeaderButton.styled";
 
-export default SavedQuestionHeaderButton;
+import { color } from "metabase/lib/colors";
+
+import EditableText from "../EditableText";
+import {
+  HeaderRoot,
+  HeaderReviewIcon,
+} from "./SavedQuestionHeaderButton.styled";
 
 SavedQuestionHeaderButton.propTypes = {
   className: PropTypes.string,
   question: PropTypes.object.isRequired,
-  onClick: PropTypes.func.isRequired,
-  isActive: PropTypes.bool.isRequired,
+  onSave: PropTypes.func,
 };
 
-function SavedQuestionHeaderButton({ className, question, onClick, isActive }) {
+const ICON_SIZE = 16;
+
+function SavedQuestionHeaderButton({ className, question, onSave }) {
   const {
     name: reviewIconName,
     color: reviewIconColor,
   } = PLUGIN_MODERATION.getStatusIconForQuestion(question);
 
   return (
-    <HeaderButton
-      className={className}
-      onClick={onClick}
-      icon={reviewIconName}
-      leftIconColor={reviewIconColor}
-      isActive={isActive}
-      iconSize={20}
-      data-testid="saved-question-header-button"
-    >
-      {question.displayName()}
-    </HeaderButton>
+    <HeaderRoot>
+      <EditableText
+        initialValue={question.displayName()}
+        onChange={onSave}
+        submitOnEnter
+        placeholder={t`A nice title`}
+        data-testid="saved-question-header-title"
+      />
+      {reviewIconName && (
+        <HeaderReviewIcon
+          name={reviewIconName}
+          color={color(reviewIconColor)}
+          size={ICON_SIZE}
+        />
+      )}
+    </HeaderRoot>
   );
 }
+
+export default Object.assign(SavedQuestionHeaderButton, {
+  Root: HeaderRoot,
+});
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
index c60e65ee432e67548680bf2b68611c19c4e61163..471d21bf6e890c4d510d2dbbd130b16bfa3140da 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.styled.jsx
@@ -1,17 +1,20 @@
 import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
 
-import Button from "metabase/core/components/Button";
+import EditableText from "../EditableText/EditableText";
+import Icon from "metabase/components/Icon";
 
-export const HeaderButton = styled(Button)`
-  font-size: 1.25rem;
-  border: none;
-  padding: 0.25rem 0.25rem;
-  color: ${props => (props.isActive ? color("brand") : "unset")};
-  background-color: ${props => (props.isActive ? color("bg-light") : "unset")};
-  text-align: left;
-
-  .Icon:not(.Icon-chevrondown) {
-    color: ${props => color(props.leftIconColor)};
+export const HeaderRoot = styled.div`
+  ${EditableText.Root}:after, ${EditableText.TextArea} {
+    font-size: 1.25rem;
+    font-weight: 700;
+    line-height: 1.5rem;
+    padding: 0.25rem;
   }
+
+  display: flex;
+  align-items: center;
+`;
+
+export const HeaderReviewIcon = styled(Icon)`
+  padding-left: 0.25rem;
 `;
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.unit.spec.js b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.unit.spec.js
index 51902b40436b5928fef0c75d1b4de6b5ada1cb76..10f062b4b2178ba042b242631544e2c475cae341 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.unit.spec.js
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton.unit.spec.js
@@ -1,26 +1,23 @@
 import React from "react";
 import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
 
 import SavedQuestionHeaderButton from "./SavedQuestionHeaderButton";
 
 describe("SavedQuestionHeaderButton", () => {
-  let onClick;
+  let onSave;
   let question;
   let componentContainer;
 
   beforeEach(() => {
-    onClick = jest.fn();
+    onSave = jest.fn();
     question = {
       displayName: () => "foo",
       getModerationReviews: () => [],
     };
 
     const { container } = render(
-      <SavedQuestionHeaderButton
-        question={question}
-        onClick={onClick}
-        isActive={false}
-      />,
+      <SavedQuestionHeaderButton question={question} onSave={onSave} />,
     );
 
     componentContainer = container;
@@ -30,16 +27,17 @@ describe("SavedQuestionHeaderButton", () => {
     expect(screen.getByText("foo")).toBeInTheDocument();
   });
 
-  it("is clickable", () => {
-    screen.getByText("foo").click();
-    expect(onClick).toHaveBeenCalled();
+  it("is updateable", () => {
+    const title = screen.getByTestId("saved-question-header-title");
+    userEvent.type(title, "1");
+    title.blur();
+
+    expect(onSave).toHaveBeenCalled();
   });
 
   describe("when the question does not have a latest moderation review", () => {
     it("should contain no additional icons", () => {
-      expect(
-        componentContainer.querySelector(".Icon:not(.Icon-chevrondown)"),
-      ).toEqual(null);
+      expect(componentContainer.querySelector(".Icon")).toEqual(null);
     });
   });
 
@@ -54,20 +52,14 @@ describe("SavedQuestionHeaderButton", () => {
       };
 
       const { container } = render(
-        <SavedQuestionHeaderButton
-          question={question}
-          onClick={onClick}
-          isActive={false}
-        />,
+        <SavedQuestionHeaderButton question={question} onSave={onSave} />,
       );
 
       componentContainer = container;
     });
 
     it("should have an additional icon to signify the question's moderation status", () => {
-      expect(
-        componentContainer.querySelector(".Icon:not(.Icon-chevrondown)"),
-      ).toBeDefined();
+      expect(componentContainer.querySelector(".Icon")).toBeDefined();
     });
   });
 });
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
index b17d42d0b898235aa358e651c0ac12049e1122b8..c5d8142ff62d9b145e5c9a0a1e41884b675b8300 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useCallback } from "react";
+import React, { useEffect, useCallback, useState } from "react";
 import PropTypes from "prop-types";
 import { t } from "ttag";
 import cx from "classnames";
@@ -14,6 +14,7 @@ import ViewButton from "metabase/query_builder/components/view/ViewButton";
 
 import { usePrevious } from "metabase/hooks/use-previous";
 import { useToggle } from "metabase/hooks/use-toggle";
+import { useOnMount } from "metabase/hooks/use-on-mount";
 
 import { MODAL_TYPES } from "metabase/query_builder/constants";
 import SavedQuestionHeaderButton from "metabase/query_builder/components/SavedQuestionHeaderButton/SavedQuestionHeaderButton";
@@ -36,7 +37,6 @@ import QuestionActions from "../QuestionActions";
 import NativeQueryButton from "./NativeQueryButton";
 import {
   AdHocViewHeading,
-  DatasetHeaderButtonContainer,
   SaveButton,
   SavedQuestionHeaderButtonContainer,
   ViewHeaderMainLeftContentContainer,
@@ -45,6 +45,7 @@ import {
   ViewSubHeaderRoot,
   StyledLastEditInfoLabel,
   StyledQuestionDataSource,
+  SavedQuestionLeftSideRoot,
 } from "./ViewHeader.styled";
 
 const viewTitleHeaderPropTypes = {
@@ -129,9 +130,7 @@ export function ViewTitleHeader(props) {
         style={style}
         data-testid="qb-header"
       >
-        {isDataset ? (
-          <DatasetLeftSide {...props} />
-        ) : isSaved ? (
+        {isSaved ? (
           <SavedQuestionLeftSide {...props} />
         ) : (
           <AhHocQuestionLeftSide
@@ -168,7 +167,7 @@ SavedQuestionLeftSide.propTypes = {
   isAdditionalInfoVisible: PropTypes.bool,
   isShowingQuestionDetailsSidebar: PropTypes.bool,
   onOpenQuestionInfo: PropTypes.func.isRequired,
-  onOpenModal: PropTypes.func.isRequired,
+  onSave: PropTypes.func,
 };
 
 function SavedQuestionLeftSide(props) {
@@ -176,33 +175,45 @@ function SavedQuestionLeftSide(props) {
     question,
     isObjectDetail,
     isAdditionalInfoVisible,
-    isShowingQuestionDetailsSidebar,
     onOpenQuestionInfo,
-    onOpenModal,
+    onSave,
   } = props;
 
+  const [showSubHeader, setShowSubHeader] = useState(true);
+
+  useOnMount(() => {
+    const timerId = setTimeout(() => {
+      setShowSubHeader(false);
+    }, 4000);
+    return () => clearTimeout(timerId);
+  });
+
   const hasLastEditInfo = question.lastEditInfo() != null;
 
-  const onHeaderClick = useCallback(() => {
-    onOpenModal(MODAL_TYPES.EDIT);
-  }, [onOpenModal]);
+  const onHeaderChange = useCallback(
+    name => {
+      if (name !== question.displayName()) {
+        onSave({
+          ...question.card(),
+          name,
+        });
+      }
+    },
+    [question, onSave],
+  );
 
   return (
-    <div>
+    <SavedQuestionLeftSideRoot
+      data-testid="qb-header-left-side"
+      showSubHeader={showSubHeader}
+    >
       <ViewHeaderMainLeftContentContainer>
         <SavedQuestionHeaderButtonContainer>
           <SavedQuestionHeaderButton
             question={question}
-            isActive={isShowingQuestionDetailsSidebar}
-            onClick={onHeaderClick}
+            onSave={onHeaderChange}
           />
         </SavedQuestionHeaderButtonContainer>
-        {hasLastEditInfo && isAdditionalInfoVisible && (
-          <StyledLastEditInfoLabel
-            item={question.card()}
-            onClick={onOpenQuestionInfo}
-          />
-        )}
       </ViewHeaderMainLeftContentContainer>
       {isAdditionalInfoVisible && (
         <ViewHeaderLeftSubHeading>
@@ -213,9 +224,15 @@ function SavedQuestionLeftSide(props) {
               subHead
             />
           )}
+          {hasLastEditInfo && isAdditionalInfoVisible && (
+            <StyledLastEditInfoLabel
+              item={question.card()}
+              onClick={onOpenQuestionInfo}
+            />
+          )}
         </ViewHeaderLeftSubHeading>
       )}
-    </div>
+    </SavedQuestionLeftSideRoot>
   );
 }
 
@@ -271,55 +288,6 @@ function AhHocQuestionLeftSide(props) {
   );
 }
 
-DatasetLeftSide.propTypes = {
-  question: PropTypes.object.isRequired,
-  isAdditionalInfoVisible: PropTypes.bool,
-  isShowingQuestionDetailsSidebar: PropTypes.bool,
-  onOpenModal: PropTypes.func.isRequired,
-};
-
-function DatasetLeftSide(props) {
-  const {
-    question,
-    isAdditionalInfoVisible,
-    isShowingQuestionDetailsSidebar,
-    onOpenModal,
-  } = props;
-
-  const onHeaderClick = useCallback(() => {
-    onOpenModal(MODAL_TYPES.EDIT);
-  }, [onOpenModal]);
-
-  return (
-    <div>
-      <ViewHeaderMainLeftContentContainer>
-        <AdHocViewHeading>
-          <HeadBreadcrumbs
-            divider="/"
-            parts={[
-              ...(isAdditionalInfoVisible
-                ? [
-                    <DatasetCollectionBadge
-                      key="collection"
-                      dataset={question}
-                    />,
-                  ]
-                : []),
-              <DatasetHeaderButtonContainer key="dataset-header-button">
-                <SavedQuestionHeaderButton
-                  question={question}
-                  isActive={isShowingQuestionDetailsSidebar}
-                  onClick={onHeaderClick}
-                />
-              </DatasetHeaderButtonContainer>,
-            ]}
-          />
-        </AdHocViewHeading>
-      </ViewHeaderMainLeftContentContainer>
-    </div>
-  );
-}
-
 DatasetCollectionBadge.propTypes = {
   dataset: PropTypes.object.isRequired,
 };
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
index 6d561e0e646dfd32f0bd2b410c0a94c1e68ba847..6d66f899476dd9d8f867011484b4868fa713d141 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
@@ -8,6 +8,7 @@ import { color, alpha } from "metabase/lib/colors";
 import { breakpointMaxSmall, space } from "metabase/styled-components/theme";
 import ViewSection, { ViewSubHeading, ViewHeading } from "./ViewSection";
 import QuestionDataSource from "./QuestionDataSource";
+import SavedQuestionHeaderButton from "../SavedQuestionHeaderButton/SavedQuestionHeaderButton";
 
 export const ViewHeaderContainer = styled(ViewSection)`
   border-bottom: 1px solid ${color("border")};
@@ -54,11 +55,6 @@ export const SavedQuestionHeaderButtonContainer = styled.div`
   right: 0.38rem;
 `;
 
-export const DatasetHeaderButtonContainer = styled.div`
-  position: relative;
-  right: 0.3rem;
-`;
-
 export const HeaderButton = styled(Button)`
   font-size: 0.875rem;
   background-color: ${({ active, color = getDefaultColor() }) =>
@@ -123,7 +119,6 @@ export const StyledLastEditInfoLabel = styled(LastEditInfoLabel)`
 `;
 
 export const StyledQuestionDataSource = styled(QuestionDataSource)`
-  margin-bottom: 0.5rem;
   padding-right: 1rem;
 
   ${breakpointMaxSmall} {
@@ -131,3 +126,27 @@ export const StyledQuestionDataSource = styled(QuestionDataSource)`
     padding-right: 0;
   }
 `;
+
+export const SavedQuestionLeftSideRoot = styled.div`
+  ${SavedQuestionHeaderButton.Root} {
+    transition: all 400ms ease;
+    position: relative;
+    top: ${props => (props.showSubHeader ? "0" : "10px")};
+  }
+
+  ${ViewHeaderLeftSubHeading} {
+    opacity: ${props => (props.showSubHeader ? "1" : "0")};
+    transition: all 400ms ease;
+  }
+
+  &:hover,
+  &:focus-within {
+    ${SavedQuestionHeaderButton.Root} {
+      top: 0px;
+    }
+
+    ${ViewHeaderLeftSubHeading} {
+      opacity: 1;
+    }
+  }
+`;
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.unit.spec.js b/frontend/src/metabase/query_builder/components/view/ViewHeader.unit.spec.js
index b8b563556fa7bb2f1659e7bf6aab9b464ca020ad..67bdfb825c65c58edbed657d33683f42627c0116 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.unit.spec.js
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.unit.spec.js
@@ -1,5 +1,6 @@
 import React from "react";
 import xhrMock from "xhr-mock";
+import userEvent from "@testing-library/user-event";
 import { fireEvent, renderWithProviders, screen } from "__support__/ui";
 import {
   SAMPLE_DATABASE,
@@ -107,6 +108,7 @@ function setup({
     onCloseFilter: jest.fn(),
     onEditSummary: jest.fn(),
     onCloseSummary: jest.fn(),
+    onSave: jest.fn(),
   };
 
   renderWithProviders(
@@ -356,9 +358,11 @@ describe("ViewHeader", () => {
         });
 
         it("opens details sidebar on question name click", () => {
-          const { onOpenModal } = setup({ question });
-          fireEvent.click(screen.getByText(question.displayName()));
-          expect(onOpenModal).toHaveBeenCalled();
+          const { onSave } = setup({ question });
+          const title = screen.getByTestId("saved-question-header-title");
+          userEvent.type(title, "New Title");
+          fireEvent.blur(title);
+          expect(onSave).toHaveBeenCalled();
         });
 
         it("shows bookmark and action buttons", () => {
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
index b76bafc37e29e99f6f593dcb8e82564d9c1c42fa..daed291534026f153f6bd2f177ce3e55c19a5e0b 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar.tsx
@@ -1,37 +1,57 @@
 import React from "react";
 
-import { PLUGIN_MODERATION, PLUGIN_MODEL_PERSISTENCE } from "metabase/plugins";
+import {
+  PLUGIN_MODERATION,
+  PLUGIN_MODEL_PERSISTENCE,
+  PLUGIN_CACHING,
+} from "metabase/plugins";
+
+import MetabaseSettings from "metabase/lib/settings";
 
-import EditableText from "../../EditableText";
 import QuestionActivityTimeline from "metabase/query_builder/components/QuestionActivityTimeline";
+
 import Question from "metabase-lib/lib/Question";
 import { Card } from "metabase-types/types/Card";
 
+import EditableText from "../../EditableText";
 import { Root, ContentSection } from "./QuestionInfoSidebar.styled";
 
-interface Props {
+interface QuestionInfoSidebarProps {
   question: Question;
-  onSave: (card: Card) => void;
+  onSave: (card: Card) => Promise<Question>;
 }
 
-export const QuestionInfoSidebar = ({ question, onSave }: Props) => {
+export const QuestionInfoSidebar = ({
+  question,
+  onSave,
+}: QuestionInfoSidebarProps) => {
   const description = question.description();
   const isDataset = question.isDataset();
   const isPersisted = isDataset && question.isPersisted();
 
-  const handleSave = (description: string) => {
+  const showCaching =
+    PLUGIN_CACHING.isEnabled() && MetabaseSettings.get("enable-query-caching");
+
+  const handleSave = (description: string | null) => {
     if (question.description() !== description) {
-      onSave({
-        ...question.card(),
-        description,
-      });
+      onSave(question.setDescription(description).card());
+    }
+  };
+
+  const handleUpdateCacheTTL = (cache_ttl: number | undefined) => {
+    if (question.cacheTTL() !== cache_ttl) {
+      return onSave(question.setCacheTTL(cache_ttl).card());
     }
   };
 
   return (
     <Root>
       <ContentSection>
-        <EditableText initialValue={description} onChange={handleSave} />
+        <EditableText
+          initialValue={description}
+          onChange={handleSave}
+          placeholder="Description"
+        />
         <PLUGIN_MODERATION.QuestionModerationSection question={question} />
       </ContentSection>
 
@@ -42,6 +62,15 @@ export const QuestionInfoSidebar = ({ question, onSave }: Props) => {
           />
         </ContentSection>
       )}
+
+      {showCaching && (
+        <ContentSection extraPadding>
+          <PLUGIN_CACHING.QuestionCacheSection
+            question={question}
+            onSave={handleUpdateCacheTTL}
+          />
+        </ContentSection>
+      )}
       <ContentSection extraPadding>
         <QuestionActivityTimeline question={question} />
       </ContentSection>
diff --git a/frontend/test/metabase/scenarios/embedding/embedding-full-app.cy.spec.js b/frontend/test/metabase/scenarios/embedding/embedding-full-app.cy.spec.js
index 6d688d386439be87fd797acb3039ccde777f727e..e7608fc41619bef927e3b36f648f57e464e34217 100644
--- a/frontend/test/metabase/scenarios/embedding/embedding-full-app.cy.spec.js
+++ b/frontend/test/metabase/scenarios/embedding/embedding-full-app.cy.spec.js
@@ -55,6 +55,7 @@ describe("scenarios > embedding > full app", () => {
       visitQuestionUrl({ url: "/question/1" });
 
       cy.findByTestId("qb-header").should("be.visible");
+      cy.findByTestId("qb-header-left-side").realHover();
       cy.findByText(/Edited/).should("be.visible");
 
       cy.icon("refresh").should("be.visible");
diff --git a/frontend/test/metabase/scenarios/models/models.cy.spec.js b/frontend/test/metabase/scenarios/models/models.cy.spec.js
index cdc289025884eb6d999d98cc153b0341b1bdce92..e3222f1a0c0d03462d7f7584fae819470f1ffe70 100644
--- a/frontend/test/metabase/scenarios/models/models.cy.spec.js
+++ b/frontend/test/metabase/scenarios/models/models.cy.spec.js
@@ -28,7 +28,6 @@ import {
   selectDimensionOptionFromSidebar,
   saveQuestionBasedOnModel,
   assertIsQuestion,
-  openDetailsSidebar,
 } from "./helpers/e2e-models-helpers";
 
 const { PRODUCTS } = SAMPLE_DATABASE;
@@ -376,22 +375,21 @@ describe("scenarios > models", () => {
       cy.visit("/model/1");
       cy.wait("@dataset");
 
-      openDetailsSidebar();
-      modal().within(() => {
-        cy.findByLabelText("Name")
-          .clear()
-          .type("M1");
-        cy.findByLabelText("Description")
-          .clear()
-          .type("foo");
-        cy.button("Save").click();
-      });
+      cy.findByTestId("saved-question-header-title")
+        .clear()
+        .type("M1")
+        .blur();
       cy.wait("@updateCard");
 
       questionInfoButton().click();
 
-      cy.findByText("M1");
-      cy.findByText("foo");
+      cy.findByPlaceholderText("Description")
+        .type("foo")
+        .blur();
+      cy.wait("@updateCard");
+
+      cy.findByDisplayValue("M1");
+      cy.findByDisplayValue("foo");
     });
   });
 
diff --git a/frontend/test/metabase/scenarios/organization/moderation-question.cy.spec.js b/frontend/test/metabase/scenarios/organization/moderation-question.cy.spec.js
index b7aa67a2350308d529e944a239a41315c722aaa6..0046d3b326f6973ebfabc89cbb351a734044a512 100644
--- a/frontend/test/metabase/scenarios/organization/moderation-question.cy.spec.js
+++ b/frontend/test/metabase/scenarios/organization/moderation-question.cy.spec.js
@@ -42,7 +42,7 @@ describeEE("scenarios > saved question moderation", () => {
 
       cy.findByText("Verify this question").should("be.visible");
 
-      cy.findByTestId("saved-question-header-button").within(() => {
+      cy.findByTestId("qb-header-left-side").within(() => {
         cy.icon("verified").should("not.exist");
       });
 
diff --git a/frontend/test/metabase/scenarios/question/caching.cy.spec.js b/frontend/test/metabase/scenarios/question/caching.cy.spec.js
index e5490208726980ce275509ea73a8f1068942efb8..186b5257e351c43f81d6c9d1c34572fb47e64645 100644
--- a/frontend/test/metabase/scenarios/question/caching.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/caching.cy.spec.js
@@ -2,8 +2,10 @@ import {
   restore,
   describeEE,
   mockSessionProperty,
-  modal,
   visitQuestion,
+  questionInfoButton,
+  rightSidebar,
+  popover,
 } from "__support__/e2e/cypress";
 
 describeEE("scenarios > question > caching", () => {
@@ -17,40 +19,50 @@ describeEE("scenarios > question > caching", () => {
     cy.intercept("PUT", "/api/card/1").as("updateQuestion");
     visitQuestion(1);
 
-    openEditingModalForm();
-    modal().within(() => {
-      cy.findByText("More options").click();
+    questionInfoButton().click();
+
+    rightSidebar().within(() => {
+      cy.findByText("Cache Configuration").click();
+    });
+
+    popover().within(() => {
       cy.findByPlaceholderText("24")
         .clear()
         .type("48")
         .blur();
-      cy.button("Save").click();
+      cy.button("Save changes").click();
     });
 
     cy.wait("@updateQuestion");
+    cy.button(/Saved/);
     cy.reload();
 
-    openEditingModalForm();
-    modal().within(() => {
-      cy.findByText("More options").click();
+    questionInfoButton().click();
+
+    rightSidebar().within(() => {
+      cy.findByText("Cache Configuration").click();
+    });
+
+    popover().within(() => {
       cy.findByDisplayValue("48")
         .clear()
         .type("0")
         .blur();
-      cy.button("Save").click();
+      cy.button("Save changes").click();
     });
 
     cy.wait("@updateQuestion");
+    cy.button(/Saved/);
     cy.reload();
 
-    openEditingModalForm();
-    modal().within(() => {
-      cy.findByText("More options").click();
+    questionInfoButton().click();
+
+    rightSidebar().within(() => {
+      cy.findByText("Cache Configuration").click();
+    });
+
+    popover().within(() => {
       cy.findByPlaceholderText("24");
     });
   });
 });
-
-function openEditingModalForm() {
-  cy.findByTestId("saved-question-header-button").click();
-}
diff --git a/frontend/test/metabase/scenarios/question/question-management.cy.spec.js b/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
index a6d69cb62c163367e60fd5223d7c941226817c8f..336e1dacd6157335a46cecc8167598a287a7919f 100644
--- a/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
@@ -36,11 +36,10 @@ describe("managing question from the question's details sidebar", () => {
 
             it("should be able to edit question details (metabase#11719-1)", () => {
               // cy.skipOn(user === "nodata");
-              cy.findByTestId("saved-question-header-button").click();
-              cy.findByLabelText("Name")
+              cy.findByTestId("saved-question-header-title")
                 .click()
-                .type("1");
-              clickButton("Save");
+                .type("1")
+                .blur();
               assertOnRequest("updateQuestion");
               cy.findByText("Orders1");
             });
@@ -50,9 +49,10 @@ describe("managing question from the question's details sidebar", () => {
 
               questionInfoButton().click();
 
-              cy.findByPlaceholderText("Description").type("foo", { delay: 0 });
+              cy.findByPlaceholderText("Description")
+                .type("foo", { delay: 0 })
+                .blur();
 
-              cy.findByPlaceholderText("Description").blur();
               assertOnRequest("updateQuestion");
 
               cy.findByText("foo");
@@ -174,5 +174,4 @@ function assertOnRequest(xhr_alias) {
   cy.findByText("Sorry, you don’t have permission to see that.").should(
     "not.exist",
   );
-  cy.get(".Modal").should("not.exist");
 }
diff --git a/frontend/test/metabase/scenarios/smoketest/admin_setup.cy.spec.js b/frontend/test/metabase/scenarios/smoketest/admin_setup.cy.spec.js
index d4ef9b9b1d81ba599ec3b2c262d7b756e44b85ee..772e79101b2e130040fd0f95f23fb1cf783aa2a7 100644
--- a/frontend/test/metabase/scenarios/smoketest/admin_setup.cy.spec.js
+++ b/frontend/test/metabase/scenarios/smoketest/admin_setup.cy.spec.js
@@ -2,7 +2,6 @@ import {
   browse,
   popover,
   restore,
-  modal,
   openPeopleTable,
   visualize,
   navigationSidebar,
@@ -10,6 +9,7 @@ import {
   visitQuestion,
 } from "__support__/e2e/cypress";
 import { USERS } from "__support__/e2e/cypress_data";
+import { questionInfoButton } from "../../../__support__/e2e/helpers/e2e-ui-elements-helpers";
 
 const { admin, normal } = USERS;
 
@@ -44,6 +44,7 @@ describe("smoketest > admin_setup", () => {
     });
 
     it("should rename a question and description as admin", () => {
+      cy.intercept("PUT", "/api/card/3").as("updateCard");
       cy.visit("/");
 
       cy.findByText("Our analytics").click();
@@ -57,12 +58,18 @@ describe("smoketest > admin_setup", () => {
 
       cy.findByText("Settings");
 
-      cy.findByTestId("saved-question-header-button").click();
-      cy.findByLabelText("Name")
+      cy.findByTestId("saved-question-header-title")
         .clear()
-        .type("Test Question");
-      cy.findByLabelText("Description").type("Testing question description");
-      cy.findByText("Save").click();
+        .type("Test Question")
+        .blur();
+      cy.wait("@updateCard");
+
+      questionInfoButton().click();
+      cy.findByPlaceholderText("Description")
+        .clear()
+        .type("Testing question description")
+        .blur();
+      cy.wait("@updateCard");
     });
 
     it("should rename a table and add a description as admin", () => {
@@ -304,14 +311,11 @@ describe("smoketest > admin_setup", () => {
       );
 
       cy.findByText("Test Question").click();
-      cy.findByTestId("saved-question-header-button").click();
-
-      cy.findByText("Edit question");
-      modal().within(() => {
-        cy.findByText("Testing question description");
-      });
 
-      cy.findByText("Cancel").click();
+      questionInfoButton().click();
+      cy.findByDisplayValue("Test Question");
+      cy.findByDisplayValue("Testing question description");
+      questionInfoButton().click();
 
       // Check column names and visiblity