diff --git a/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.tsx b/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.tsx
index 20da3839e6e2c49914361a3b99c37269bfc31ed8..224d0498696c41e6079dc99ea5a86048e6f92271 100644
--- a/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.tsx
@@ -4,6 +4,7 @@ import moment from "moment";
 import { connect } from "react-redux";
 
 import PersistedModels from "metabase/entities/persisted-models";
+import { checkCanRefreshModelCache } from "metabase/lib/data-modeling/utils";
 
 import Question from "metabase-lib/lib/Question";
 import { ModelCacheRefreshStatus } from "metabase-types/api";
@@ -28,9 +29,15 @@ type LoaderRenderProps = {
 };
 
 function getStatusMessage(job: ModelCacheRefreshStatus) {
+  if (job.state === "off") {
+    return `Caching is turned off`;
+  }
   if (job.state === "error") {
     return t`Failed to update model cache`;
   }
+  if (job.state === "creating") {
+    return t`Queued`;
+  }
   if (job.state === "refreshing") {
     return t`Refreshing model cache`;
   }
@@ -52,7 +59,11 @@ function ModelCacheManagementSection({ model, onRefresh }: Props) {
       loadingAndErrorWrapper={false}
     >
       {({ persistedModel }: LoaderRenderProps) => {
-        if (!persistedModel) {
+        if (
+          !persistedModel ||
+          persistedModel.state === "off" ||
+          persistedModel.state === "deletable"
+        ) {
           return null;
         }
 
@@ -60,7 +71,7 @@ function ModelCacheManagementSection({ model, onRefresh }: Props) {
         const lastRefreshTime = moment(persistedModel.refresh_end).fromNow();
 
         return (
-          <Row>
+          <Row data-testid="model-cache-section">
             <div>
               <StatusContainer>
                 <StatusLabel>{getStatusMessage(persistedModel)}</StatusLabel>
@@ -72,9 +83,15 @@ function ModelCacheManagementSection({ model, onRefresh }: Props) {
                 </LastRefreshTimeLabel>
               )}
             </div>
-            <IconButton onClick={() => onRefresh(persistedModel)}>
-              <RefreshIcon name="refresh" tooltip={t`Refresh now`} size={14} />
-            </IconButton>
+            {checkCanRefreshModelCache(persistedModel) && (
+              <IconButton onClick={() => onRefresh(persistedModel)}>
+                <RefreshIcon
+                  name="refresh"
+                  tooltip={t`Refresh now`}
+                  size={14}
+                />
+              </IconButton>
+            )}
           </Row>
         );
       }}
diff --git a/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..65835613ecae39ba399f9d957041fc0199932305
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/model_persistence/components/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx
@@ -0,0 +1,138 @@
+import React from "react";
+import moment from "moment";
+import xhrMock from "xhr-mock";
+
+import PersistedModels from "metabase/entities/persisted-models";
+import { ModelCacheRefreshStatus } from "metabase-types/api";
+import { getMockModelCacheInfo } from "metabase-types/api/mocks/models";
+
+import {
+  fireEvent,
+  renderWithProviders,
+  waitFor,
+  screen,
+} from "__support__/ui";
+import { ORDERS } from "__support__/sample_database_fixture";
+
+import ModelCacheManagementSection from "./ModelCacheManagementSection";
+
+type SetupOpts = Partial<ModelCacheRefreshStatus> & {
+  waitForSectionAppearance?: boolean;
+};
+
+async function setup({
+  waitForSectionAppearance = true,
+  ...cacheInfo
+}: SetupOpts = {}) {
+  const question = ORDERS.question();
+  const model = question.setCard({
+    ...question.card(),
+    id: 1,
+    name: "Order model",
+    dataset: true,
+  });
+
+  const modelCacheInfo = getMockModelCacheInfo({
+    ...cacheInfo,
+    card_id: model.id(),
+    card_name: model.displayName(),
+  });
+
+  const onRefreshMock = jest
+    .spyOn(PersistedModels.objectActions, "refreshCache")
+    .mockReturnValue({ type: "__MOCK__" });
+
+  xhrMock.get(`/api/persist/card/${model.id()}`, {
+    body: JSON.stringify(modelCacheInfo),
+  });
+
+  if (!waitForSectionAppearance) {
+    jest.spyOn(PersistedModels, "Loader").mockImplementation(props => {
+      const { children } = props as any;
+      return children({ persistedModel: cacheInfo });
+    });
+  }
+
+  const utils = renderWithProviders(
+    <ModelCacheManagementSection model={model} />,
+  );
+
+  if (waitForSectionAppearance) {
+    await waitFor(() => utils.queryByTestId("model-cache-section"));
+  }
+
+  return {
+    ...utils,
+    modelCacheInfo,
+    onRefreshMock,
+  };
+}
+
+describe("ModelCacheManagementSection", () => {
+  beforeEach(() => {
+    xhrMock.setup();
+  });
+
+  afterEach(() => {
+    xhrMock.teardown();
+    jest.resetAllMocks();
+  });
+
+  it("doesn't show up in 'off' state", async () => {
+    await setup({ state: "off" });
+    expect(screen.queryByTestId("model-cache-section")).not.toBeInTheDocument();
+  });
+
+  it("doesn't show up in 'deletable' state", async () => {
+    await setup({ state: "deletable" });
+    expect(screen.queryByTestId("model-cache-section")).not.toBeInTheDocument();
+  });
+
+  it("displays 'creating' state correctly", async () => {
+    await setup({ state: "creating" });
+    expect(screen.getByText("Queued")).toBeInTheDocument();
+    expect(screen.queryByLabelText("refresh icon")).not.toBeInTheDocument();
+  });
+
+  it("displays 'refreshing' state correctly", async () => {
+    await setup({ state: "refreshing" });
+    expect(screen.getByText("Refreshing model cache")).toBeInTheDocument();
+    expect(screen.queryByLabelText("refresh icon")).not.toBeInTheDocument();
+  });
+
+  it("displays 'persisted' state correctly", async () => {
+    const { modelCacheInfo } = await setup({ state: "persisted" });
+    const expectedTimestamp = moment(modelCacheInfo.refresh_end).fromNow();
+    expect(
+      screen.getByText(`Model last cached ${expectedTimestamp}`),
+    ).toBeInTheDocument();
+    expect(screen.getByLabelText("refresh icon")).toBeInTheDocument();
+  });
+
+  it("triggers refresh from 'persisted' state", async () => {
+    const { modelCacheInfo, onRefreshMock } = await setup({
+      state: "persisted",
+    });
+    fireEvent.click(screen.getByLabelText("refresh icon"));
+    expect(onRefreshMock).toHaveBeenCalledWith(modelCacheInfo);
+  });
+
+  it("displays 'error' state correctly", async () => {
+    const { modelCacheInfo } = await setup({ state: "error" });
+    const expectedTimestamp = moment(modelCacheInfo.refresh_end).fromNow();
+
+    expect(
+      screen.getByText("Failed to update model cache"),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(`Last attempt ${expectedTimestamp}`),
+    ).toBeInTheDocument();
+    expect(screen.getByLabelText("refresh icon")).toBeInTheDocument();
+  });
+
+  it("triggers refresh from 'error' state", async () => {
+    const { modelCacheInfo, onRefreshMock } = await setup({ state: "error" });
+    fireEvent.click(screen.getByLabelText("refresh icon"));
+    expect(onRefreshMock).toHaveBeenCalledWith(modelCacheInfo);
+  });
+});
diff --git a/frontend/src/metabase-types/api/mocks/index.ts b/frontend/src/metabase-types/api/mocks/index.ts
index e26e0e465d8751da547af1e13406b95ad72c6906..deac792384d31edcf8e7d37b8af47a8004257db9 100644
--- a/frontend/src/metabase-types/api/mocks/index.ts
+++ b/frontend/src/metabase-types/api/mocks/index.ts
@@ -4,6 +4,7 @@ export * from "./collection";
 export * from "./dashboard";
 export * from "./database";
 export * from "./dataset";
+export * from "./models";
 export * from "./timeline";
 export * from "./settings";
 export * from "./user";
diff --git a/frontend/src/metabase-types/api/mocks/models.ts b/frontend/src/metabase-types/api/mocks/models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c7702114c635880022e44c2e153caf967f8ce4d7
--- /dev/null
+++ b/frontend/src/metabase-types/api/mocks/models.ts
@@ -0,0 +1,39 @@
+import { ModelCacheRefreshStatus } from "metabase-types/api";
+
+export const getMockModelCacheInfo = (
+  opts?: Partial<ModelCacheRefreshStatus>,
+): ModelCacheRefreshStatus => {
+  const now = new Date();
+
+  const past = new Date();
+  past.setMinutes(now.getMinutes() - 30);
+
+  const future = new Date();
+  future.setHours(now.getHours() + 1);
+
+  return {
+    id: 1,
+    state: "persisted",
+    error: null,
+    active: true,
+
+    card_id: 1,
+    card_name: "Test Model",
+
+    collection_id: "root",
+    collection_name: "Our analytics",
+    collection_authority_level: null,
+
+    columns: [],
+    database_id: 1,
+    database_name: "Sample Database",
+    schema_name: "PUBLIC",
+    table_name: "Orders",
+
+    refresh_begin: past.toISOString(),
+    refresh_end: now.toISOString(),
+    "next-fire-time": future.toISOString(),
+
+    ...opts,
+  };
+};
diff --git a/frontend/src/metabase-types/api/models.ts b/frontend/src/metabase-types/api/models.ts
index c75a2256a5d401c802295eeb54eebf029cf7aedb..24bc6970dd55012e820b49240b61a6f3275e963d 100644
--- a/frontend/src/metabase-types/api/models.ts
+++ b/frontend/src/metabase-types/api/models.ts
@@ -7,9 +7,17 @@ import {
   UserId,
 } from "metabase-types/api";
 
+export type ModelCacheState =
+  | "creating"
+  | "refreshing"
+  | "persisted"
+  | "error"
+  | "deletable"
+  | "off";
+
 export interface ModelCacheRefreshStatus {
   id: number;
-  state: "refreshing" | "persisted" | "error";
+  state: ModelCacheState;
   error: string | null;
   active: boolean;
 
diff --git a/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.tsx b/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.tsx
index e8cbd4bdf950886f1a8630f907963b019ff4f3cc..f9315c26a84f78cf5fb81b8809b61baccd058ca2 100644
--- a/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.tsx
+++ b/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.tsx
@@ -5,11 +5,13 @@ import { connect } from "react-redux";
 
 import Link from "metabase/core/components/Link";
 import DateTime from "metabase/components/DateTime";
+import EmptyState from "metabase/components/EmptyState";
 import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
 import PaginationControls from "metabase/components/PaginationControls";
 
 import PersistedModels from "metabase/entities/persisted-models";
+import { checkCanRefreshModelCache } from "metabase/lib/data-modeling/utils";
 import { capitalize } from "metabase/lib/formatting";
 import * as Urls from "metabase/lib/urls";
 
@@ -17,6 +19,8 @@ import { usePagination } from "metabase/hooks/use-pagination";
 
 import { ModelCacheRefreshStatus } from "metabase-types/api";
 
+import NoResults from "assets/img/no_results.svg";
+
 import {
   ErrorBox,
   IconButtonContainer,
@@ -39,6 +43,12 @@ function JobTableItem({ job, onRefresh }: JobTableItemProps) {
   const lastRunAtLabel = capitalize(moment(job.refresh_begin).fromNow());
 
   const renderStatus = useCallback(() => {
+    if (job.state === "off") {
+      return t`Off`;
+    }
+    if (job.state === "creating") {
+      return t`Queued`;
+    }
     if (job.state === "refreshing") {
       return t`Refreshing`;
     }
@@ -73,11 +83,13 @@ function JobTableItem({ job, onRefresh }: JobTableItemProps) {
       </th>
       <th>{job.creator?.common_name || t`Automatic`}</th>
       <th>
-        <Tooltip tooltip={t`Refresh`}>
-          <IconButtonContainer onClick={onRefresh}>
-            <Icon name="refresh" />
-          </IconButtonContainer>
-        </Tooltip>
+        {checkCanRefreshModelCache(job) && (
+          <Tooltip tooltip={t`Refresh`}>
+            <IconButtonContainer onClick={onRefresh}>
+              <Icon name="refresh" />
+            </IconButtonContainer>
+          </Tooltip>
+        )}
       </th>
     </tr>
   );
@@ -118,8 +130,23 @@ function ModelCacheRefreshJobs({ children, onRefresh }: Props) {
         {({ persistedModels, metadata }: PersistedModelsListLoaderProps) => {
           const hasPagination = metadata.total > PAGE_SIZE;
 
+          const modelCacheInfo = persistedModels.filter(
+            cacheInfo => cacheInfo.state !== "deletable",
+          );
+
+          if (modelCacheInfo.length === 0) {
+            return (
+              <div data-testid="model-cache-logs">
+                <EmptyState
+                  title={t`No results`}
+                  illustrationElement={<img src={NoResults} />}
+                />
+              </div>
+            );
+          }
+
           return (
-            <>
+            <div data-testid="model-cache-logs">
               <table className="ContentTable border-bottom">
                 <colgroup>
                   <col style={{ width: "30%" }} />
@@ -138,7 +165,7 @@ function ModelCacheRefreshJobs({ children, onRefresh }: Props) {
                   </tr>
                 </thead>
                 <tbody>
-                  {persistedModels.map(job => (
+                  {modelCacheInfo.map(job => (
                     <JobTableItem
                       key={job.id}
                       job={job}
@@ -160,7 +187,7 @@ function ModelCacheRefreshJobs({ children, onRefresh }: Props) {
                   />
                 </PaginationControlsContainer>
               )}
-            </>
+            </div>
           );
         }}
       </PersistedModels.ListLoader>
diff --git a/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.unit.spec.tsx b/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f35820627cad84dfaa24afd5e57280ed6f3816f2
--- /dev/null
+++ b/frontend/src/metabase/admin/tasks/containers/ModelCacheRefreshJobs/ModelCacheRefreshJobs.unit.spec.tsx
@@ -0,0 +1,131 @@
+import React from "react";
+
+import PersistedModels from "metabase/entities/persisted-models";
+import { ModelCacheRefreshStatus } from "metabase-types/api";
+import { getMockModelCacheInfo } from "metabase-types/api/mocks/models";
+
+import { renderWithProviders, waitFor, screen } from "__support__/ui";
+
+import ModelCacheRefreshJobs from "./ModelCacheRefreshJobs";
+
+async function setup({ logs = [] }: { logs?: ModelCacheRefreshStatus[] } = {}) {
+  const onRefreshMock = jest
+    .spyOn(PersistedModels.objectActions, "refreshCache")
+    .mockReturnValue({ type: "__MOCK__" });
+
+  jest.spyOn(PersistedModels, "ListLoader").mockImplementation(props => {
+    const { children } = props as any;
+    return children({
+      persistedModels: logs,
+      metadata: {
+        limit: 20,
+        offset: 0,
+        total: logs.length,
+      },
+    });
+  });
+
+  const utils = renderWithProviders(
+    <ModelCacheRefreshJobs>
+      <></>
+    </ModelCacheRefreshJobs>,
+  );
+
+  await waitFor(() => utils.queryByTestId("model-cache-logs"));
+
+  return {
+    ...utils,
+    onRefreshMock,
+  };
+}
+
+describe("ModelCacheRefreshJobs", () => {
+  afterEach(() => {
+    jest.resetAllMocks();
+  });
+
+  it("shows empty state when there are no cache logs", async () => {
+    await setup({ logs: [] });
+    expect(screen.getByText("No results")).toBeInTheDocument();
+    expect(document.querySelector("table")).not.toBeInTheDocument();
+  });
+
+  it("shows empty state when all logs are in 'deletable' state", async () => {
+    await setup({
+      logs: [
+        getMockModelCacheInfo({ id: 1, card_id: 1, state: "deletable" }),
+        getMockModelCacheInfo({ id: 2, card_id: 2, state: "deletable" }),
+      ],
+    });
+    expect(screen.getByText("No results")).toBeInTheDocument();
+    expect(document.querySelector("table")).not.toBeInTheDocument();
+  });
+
+  it("shows model and collection names", async () => {
+    const info = getMockModelCacheInfo({
+      collection_name: "Growth",
+      card_name: "Customer",
+    });
+
+    await setup({ logs: [info] });
+
+    expect(screen.getByText("Customer")).toBeInTheDocument();
+    expect(screen.getByText("Growth")).toBeInTheDocument();
+  });
+
+  it("handles models in root collections", async () => {
+    const info = getMockModelCacheInfo({
+      collection_id: "root",
+      collection_name: undefined,
+    });
+    await setup({ logs: [info] });
+    expect(screen.getByText("Our analytics")).toBeInTheDocument();
+  });
+
+  it("doesn't show records in 'deletable' state", async () => {
+    await setup({
+      logs: [
+        getMockModelCacheInfo({
+          id: 1,
+          card_id: 1,
+          card_name: "DELETABLE",
+          state: "deletable",
+        }),
+        getMockModelCacheInfo({ id: 2, card_id: 2, state: "persisted" }),
+      ],
+    });
+    expect(screen.queryByText("DELETABLE")).not.toBeInTheDocument();
+  });
+
+  it("displays 'off' state correctly", async () => {
+    await setup({ logs: [getMockModelCacheInfo({ state: "off" })] });
+    expect(screen.getByText("Off")).toBeInTheDocument();
+    expect(screen.queryByLabelText("refresh icon")).not.toBeInTheDocument();
+  });
+
+  it("displays 'creating' state correctly", async () => {
+    await setup({ logs: [getMockModelCacheInfo({ state: "creating" })] });
+    expect(screen.getByText("Queued")).toBeInTheDocument();
+    expect(screen.queryByLabelText("refresh icon")).not.toBeInTheDocument();
+  });
+
+  it("displays 'refreshing' state correctly", async () => {
+    await setup({ logs: [getMockModelCacheInfo({ state: "refreshing" })] });
+    expect(screen.getByText("Refreshing")).toBeInTheDocument();
+    expect(screen.queryByLabelText("refresh icon")).not.toBeInTheDocument();
+  });
+
+  it("displays 'persisted' state correctly", async () => {
+    await setup({ logs: [getMockModelCacheInfo({ state: "persisted" })] });
+    expect(screen.getByText("Completed")).toBeInTheDocument();
+    expect(screen.getByLabelText("refresh icon")).toBeInTheDocument();
+  });
+
+  it("displays 'error' state correctly", async () => {
+    await setup({
+      logs: [getMockModelCacheInfo({ state: "error", error: "FOO BAR ERROR" })],
+    });
+    expect(screen.getByText("FOO BAR ERROR")).toBeInTheDocument();
+    expect(screen.getByLabelText("refresh icon")).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/lib/data-modeling/utils.ts b/frontend/src/metabase/lib/data-modeling/utils.ts
index 286f1639c5843d2ac6342c46409bae88c0a8f50f..5e07b7301d138741e40c83003dbce03e23266297 100644
--- a/frontend/src/metabase/lib/data-modeling/utils.ts
+++ b/frontend/src/metabase/lib/data-modeling/utils.ts
@@ -1,8 +1,11 @@
 import Question from "metabase-lib/lib/Question";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import Database from "metabase-lib/lib/metadata/Database";
+
 import { isStructured } from "metabase/lib/query";
 import { getQuestionVirtualTableId } from "metabase/lib/saved-questions";
+
+import { ModelCacheRefreshStatus } from "metabase-types/api";
 import { TemplateTag } from "metabase-types/types/Query";
 import {
   Card as CardObject,
@@ -66,3 +69,9 @@ export function isAdHocModelQuestion(
   }
   return isAdHocModelQuestionCard(question.card(), originalQuestion.card());
 }
+
+export function checkCanRefreshModelCache(
+  refreshInfo: ModelCacheRefreshStatus,
+) {
+  return refreshInfo.state === "persisted" || refreshInfo.state === "error";
+}
diff --git a/frontend/src/metabase/lib/data-modeling/utils.unit.spec.ts b/frontend/src/metabase/lib/data-modeling/utils.unit.spec.ts
index 71ebcc06ffbfede14b281ec9bf947c30d4b0f5c2..bc7229dfdb1d68a5a6147be827b8d7849b63c53d 100644
--- a/frontend/src/metabase/lib/data-modeling/utils.unit.spec.ts
+++ b/frontend/src/metabase/lib/data-modeling/utils.unit.spec.ts
@@ -1,5 +1,7 @@
 import Question from "metabase-lib/lib/Question";
 import Database from "metabase-lib/lib/metadata/Database";
+
+import { ModelCacheState } from "metabase-types/api";
 import {
   TemplateTag,
   TemplateTagType,
@@ -7,12 +9,16 @@ import {
   SourceTableId,
 } from "metabase-types/types/Query";
 import { CardId } from "metabase-types/types/Card";
+
 import { createMockDatabase } from "metabase-types/api/mocks/database";
+import { getMockModelCacheInfo } from "metabase-types/api/mocks/models";
 import { ORDERS, metadata } from "__support__/sample_database_fixture";
+
 import {
   checkCanBeModel,
   isAdHocModelQuestion,
   isAdHocModelQuestionCard,
+  checkCanRefreshModelCache,
 } from "./utils";
 
 type NativeQuestionFactoryOpts = {
@@ -249,4 +255,24 @@ describe("data model utils", () => {
       ).toBe(false);
     });
   });
+
+  describe("checkCanRefreshModelCache", () => {
+    const testCases: Record<ModelCacheState, boolean> = {
+      creating: false,
+      refreshing: false,
+      persisted: true,
+      error: true,
+      deletable: false,
+      off: false,
+    };
+    const states = Object.keys(testCases) as ModelCacheState[];
+
+    states.forEach(state => {
+      const canRefresh = testCases[state];
+      it(`returns '${canRefresh}' for '${state}' caching state`, () => {
+        const info = getMockModelCacheInfo({ state });
+        expect(checkCanRefreshModelCache(info)).toBe(canRefresh);
+      });
+    });
+  });
 });
diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts
index 8f9aee72e1f79504980f60c5106b0028bda17788..e92012c8de7c9c8cefee78e221deb0c2c4682352 100644
--- a/frontend/src/types/global.d.ts
+++ b/frontend/src/types/global.d.ts
@@ -1,3 +1,9 @@
 interface Window {
   MetabaseBootstrap: any;
 }
+
+// This allows importing static SVGs from TypeScript files
+declare module "*.svg" {
+  const content: any;
+  export default content;
+}