diff --git a/e2e/test/scenarios/question/saved.cy.spec.js b/e2e/test/scenarios/question/saved.cy.spec.js
index 6354e0d54479c462d369c190b20ba5e013b7890b..304cbac69b13e49e13c78a9042463f5c110d3cbd 100644
--- a/e2e/test/scenarios/question/saved.cy.spec.js
+++ b/e2e/test/scenarios/question/saved.cy.spec.js
@@ -1,3 +1,4 @@
+import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
 import {
   ORDERS_COUNT_QUESTION_ID,
   ORDERS_QUESTION_ID,
@@ -11,9 +12,11 @@ import {
   addSummaryGroupingField,
   appBar,
   collectionOnTheGoModal,
+  createQuestion,
   entityPickerModal,
   entityPickerModalTab,
   getAlertChannel,
+  main,
   modal,
   openNotebook,
   openOrdersTable,
@@ -24,12 +27,15 @@ import {
   restore,
   rightSidebar,
   selectFilterOperator,
+  sidebar,
   sidesheet,
   summarize,
   tableHeaderClick,
   visitQuestion,
 } from "e2e/support/helpers";
 
+const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
+
 describe("scenarios > question > saved", () => {
   beforeEach(() => {
     restore();
@@ -317,6 +323,127 @@ describe("scenarios > question > saved", () => {
       );
     });
   });
+
+  describe("with hidden tables", () => {
+    beforeEach(() => {
+      cy.signInAsAdmin();
+    });
+
+    const HIDDEN_TYPES = ["hidden", "technical", "cruft"];
+
+    function hideTable(name, visibilityType) {
+      cy.visit("/admin/datamodel");
+      sidebar().findByText(name).click();
+      main().findByText("Hidden").click();
+
+      if (visibilityType === "technical") {
+        main().findByText("Technical Data").click();
+      }
+      if (visibilityType === "cruft") {
+        main().findByText("Irrelevant/Cruft").click();
+      }
+    }
+
+    HIDDEN_TYPES.forEach(visibilityType => {
+      it(`should show a View-only tag when the source table is marked as ${visibilityType}`, () => {
+        hideTable("Orders", visibilityType);
+
+        visitQuestion(ORDERS_QUESTION_ID);
+
+        queryBuilderHeader()
+          .findByText("View-only")
+          .should("be.visible")
+          .realHover();
+        popover()
+          .findByText(
+            "One of the administrators hid the source table “Orders”, making this question view-only.",
+          )
+          .should("be.visible");
+      });
+
+      it(`should show a View-only tag when a joined table is marked as ${visibilityType}`, () => {
+        cy.signInAsAdmin();
+        hideTable("Products", visibilityType);
+        createQuestion(
+          {
+            name: "Joined question",
+            query: {
+              "source-table": ORDERS_ID,
+              joins: [
+                {
+                  "source-table": PRODUCTS_ID,
+                  alias: "Orders",
+                  condition: [
+                    "=",
+                    ["field", ORDERS.PRODUCT_ID, null],
+                    ["field", PRODUCTS.ID, { "join-alias": "Products" }],
+                  ],
+                  fields: "all",
+                },
+              ],
+            },
+          },
+          {
+            visitQuestion: true,
+          },
+        );
+        queryBuilderHeader()
+          .findByText("View-only")
+          .should("be.visible")
+          .realHover();
+        popover()
+          .findByText(
+            "One of the administrators hid the source table “Products”, making this question view-only.",
+          )
+          .should("be.visible");
+      });
+    });
+
+    function moveQuestionTo(newCollectionName, clickTab = false) {
+      openQuestionActions();
+      cy.findByTestId("move-button").click();
+      entityPickerModal().within(() => {
+        clickTab && cy.findByRole("tab", { name: /Collections/ }).click();
+        cy.findByText(newCollectionName).click();
+        cy.button("Move").click();
+      });
+    }
+
+    it("should show a View-only tag when one of the source cards is unavailable", () => {
+      createQuestion(
+        {
+          name: "Products Question + Orders",
+          query: {
+            "source-table": `card__${ORDERS_QUESTION_ID}`,
+            joins: [
+              {
+                "source-table": PRODUCTS_ID,
+                alias: "Orders Question",
+                fields: "all",
+                condition: [
+                  "=",
+                  ["field", PRODUCTS.PRODUCT_ID, null],
+                  ["field", ORDERS.ID, { "join-alias": "Orders" }],
+                ],
+              },
+            ],
+          },
+        },
+        {
+          wrapId: true,
+          idAlias: "questionId",
+        },
+      );
+
+      visitQuestion(ORDERS_QUESTION_ID);
+      moveQuestionTo(/Personal Collection/, true);
+
+      cy.signInAsNormalUser();
+      cy.get("@questionId").then(visitQuestion);
+
+      queryBuilderHeader().findByText("View-only").should("be.visible");
+    });
+  });
 });
 
 //http://127.0.0.1:9080/api/session/00000000-0000-0000-0000-000000000000/requests
diff --git a/frontend/src/metabase-lib/order_by.unit.spec.ts b/frontend/src/metabase-lib/order_by.unit.spec.ts
index 5a49e657accbb526b76900bdeb61b19f26140db5..fb445e329a802d73a4285aef2a21dc4bcc1d0808 100644
--- a/frontend/src/metabase-lib/order_by.unit.spec.ts
+++ b/frontend/src/metabase-lib/order_by.unit.spec.ts
@@ -37,6 +37,7 @@ describe("order by", () => {
             longDisplayName: "Orders",
             isSourceTable: true,
             schema: "1:PUBLIC",
+            visibilityType: null,
           },
         }),
       );
@@ -62,6 +63,7 @@ describe("order by", () => {
             longDisplayName: "Products",
             isSourceTable: false,
             schema: "1:PUBLIC",
+            visibilityType: null,
           },
         }),
       );
diff --git a/frontend/src/metabase-lib/types.ts b/frontend/src/metabase-lib/types.ts
index 55fc8c738295b95a1a710110b4df82e251e2f1c7..9afb6cf42eba530a21b3f26d396d142a6aad1e9c 100644
--- a/frontend/src/metabase-lib/types.ts
+++ b/frontend/src/metabase-lib/types.ts
@@ -7,6 +7,7 @@ import type {
   RowValue,
   SchemaId,
   TableId,
+  TableVisibilityType,
   TemporalUnit,
 } from "metabase-types/api";
 
@@ -135,6 +136,7 @@ export type TableDisplayInfo = {
   isQuestion?: boolean;
   isModel?: boolean;
   isMetric?: boolean;
+  visibilityType?: TableVisibilityType;
 };
 
 export type CardDisplayInfo = TableDisplayInfo;
diff --git a/frontend/src/metabase-lib/v1/metadata/Metadata.ts b/frontend/src/metabase-lib/v1/metadata/Metadata.ts
index 4633a3e5597e57a675bb90a04b99385b9cc42fde..1968e941793fe401ab75feb84af4d7a34340d883 100644
--- a/frontend/src/metabase-lib/v1/metadata/Metadata.ts
+++ b/frontend/src/metabase-lib/v1/metadata/Metadata.ts
@@ -112,7 +112,7 @@ class Metadata {
   }
 
   /**
-   * @deprecated load data via RTK Query - useGetTableQuery or useGetTableMetadataQuery
+   * @deprecated load data via RTK Query - useGetTableQuery or useGetTableQueryMetadataQuery
    */
   table(tableId: TableId | undefined | null): Table | null {
     return (tableId != null && this.tables[tableId]) || null;
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/ViewTitleHeader.unit.spec.js b/frontend/src/metabase/query_builder/components/view/ViewHeader/ViewTitleHeader.unit.spec.js
index e6ca3445d3f9b7e08ee71cd6f0d23ff392e38be4..b71668d8c8893bd5e7f6ef97b5eb4707b6ec3d89 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader/ViewTitleHeader.unit.spec.js
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/ViewTitleHeader.unit.spec.js
@@ -12,7 +12,11 @@ import { COMMON_DATABASE_FEATURES } from "metabase-types/api/mocks";
 import {
   ORDERS,
   ORDERS_ID,
+  PRODUCTS,
+  PRODUCTS_ID,
   SAMPLE_DB_ID,
+  createOrdersTable,
+  createProductsTable,
   createSampleDatabase,
 } from "metabase-types/api/mocks/presets";
 import { createMockState } from "metabase-types/store/mocks";
@@ -20,6 +24,12 @@ import { createMockState } from "metabase-types/store/mocks";
 import { ViewTitleHeader } from "./ViewTitleHeader";
 
 console.warn = jest.fn();
+console.error = jest.fn();
+
+const PRODUCTS_TABLE = createProductsTable();
+const HIDDEN_ORDERS_TABLE = createOrdersTable({
+  visibility_type: "hidden",
+});
 
 const BASE_GUI_QUESTION = {
   display: "table",
@@ -105,6 +115,7 @@ function setup({
   isAdditionalInfoVisible = true,
   isDirty = false,
   isRunnable = true,
+  hideOrdersTable = false,
   ...props
 } = {}) {
   mockSettings(settings);
@@ -126,6 +137,9 @@ function setup({
     entities: createMockEntitiesState({
       databases: [database],
       questions: [card],
+      tables: hideOrdersTable
+        ? [PRODUCTS_TABLE, HIDDEN_ORDERS_TABLE]
+        : undefined,
     }),
   });
 
@@ -185,6 +199,10 @@ function setupSavedNative(props = {}) {
 }
 
 describe("ViewTitleHeader", () => {
+  beforeEach(() => {
+    fetchMock.reset();
+  });
+
   const TEST_CASE = {
     SAVED_GUI_QUESTION: {
       card: getSavedGUIQuestionCard(),
@@ -573,3 +591,72 @@ describe("View Header | Read only permissions", () => {
     expect(screen.queryByTestId("saved-question-header-title")).toBeDisabled();
   });
 });
+
+describe("View Header | Hidden tables", () => {
+  it("should show the View-only badge when the source table is hidden", async () => {
+    setup({
+      hideOrdersTable: true,
+      card: getSavedGUIQuestionCard({ can_write: false }),
+    });
+    expect(await screen.findByText("View-only")).toBeInTheDocument();
+  });
+
+  it("should show the View-only badge when a joined table is hidden", async () => {
+    setup({
+      hideOrdersTable: true,
+      card: getSavedGUIQuestionCard({
+        can_write: false,
+        dataset_query: {
+          type: "query",
+          database: SAMPLE_DB_ID,
+          query: {
+            "source-table": PRODUCTS_ID,
+            joins: [
+              {
+                alias: "Orders",
+                fields: "all",
+                "source-table": ORDERS_ID,
+                condition: [
+                  "=",
+                  ["field", PRODUCTS.ID, null],
+                  ["field", ORDERS.PRODUCT_ID, null],
+                ],
+              },
+            ],
+          },
+        },
+      }),
+    });
+    expect(await screen.findByText("View-only")).toBeInTheDocument();
+  });
+});
+
+describe("View Header | Inaccessible Cards", () => {
+  it("should show the View-only badge when the source question is inaccessible", async () => {
+    setup({
+      card: getSavedGUIQuestionCard({
+        can_write: false,
+        dataset_query: {
+          type: "query",
+          database: SAMPLE_DB_ID,
+          query: {
+            "source-table": "card_123",
+            joins: [
+              {
+                alias: "Orders",
+                fields: "all",
+                "source-table": ORDERS_ID,
+                condition: [
+                  "=",
+                  ["field", PRODUCTS.ID, null],
+                  ["field", ORDERS.PRODUCT_ID, null],
+                ],
+              },
+            ],
+          },
+        },
+      }),
+    });
+    expect(await screen.findByText("View-only")).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.module.css b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..92956617bdd84a4300ddf1693ec8ba8e1c727d5e
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.module.css
@@ -0,0 +1,5 @@
+.badge {
+  background: var(--mb-color-bg-medium);
+  border-radius: 0.25rem;
+  user-select: none;
+}
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.tsx b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.tsx
index eab493ce947d142c6be7d45c20c5282b207df5f0..00e69f244a179188290cf3dce8f112d0315d5c82 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.tsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/SavedQuestionLeftSide.tsx
@@ -11,12 +11,15 @@ import {
   ViewHeaderLeftSubHeading,
   ViewHeaderMainLeftContentContainer,
 } from "metabase/query_builder/components/view/ViewHeader/ViewTitleHeader.styled";
+import { Flex } from "metabase/ui";
 import type Question from "metabase-lib/v1/Question";
 
 import { HeadBreadcrumbs } from "../HeaderBreadcrumbs";
 import { HeaderCollectionBadge } from "../HeaderCollectionBadge";
 import { QuestionDataSource } from "../QuestionDataSource";
 
+import { ViewOnlyTag } from "./ViewOnly";
+
 interface SavedQuestionLeftSideProps {
   question: Question;
   isObjectDetail?: boolean;
@@ -68,25 +71,29 @@ export function SavedQuestionLeftSide({
     >
       <ViewHeaderMainLeftContentContainer>
         <SavedQuestionHeaderButtonContainer isModelOrMetric={isModelOrMetric}>
-          <HeadBreadcrumbs
-            divider={<HeaderDivider>/</HeaderDivider>}
-            parts={[
-              ...(isAdditionalInfoVisible && isModelOrMetric
-                ? [
-                    <HeaderCollectionBadge
-                      key="collection"
-                      question={question}
-                    />,
-                  ]
-                : []),
+          <Flex align="center" gap="sm">
+            <HeadBreadcrumbs
+              divider={<HeaderDivider>/</HeaderDivider>}
+              parts={[
+                ...(isAdditionalInfoVisible && isModelOrMetric
+                  ? [
+                      <HeaderCollectionBadge
+                        key="collection"
+                        question={question}
+                      />,
+                    ]
+                  : []),
 
-              <SavedQuestionHeaderButton
-                key={question.displayName()}
-                question={question}
-                onSave={onHeaderChange}
-              />,
-            ]}
-          />
+                <SavedQuestionHeaderButton
+                  key={question.displayName()}
+                  question={question}
+                  onSave={onHeaderChange}
+                />,
+              ]}
+            />
+
+            <ViewOnlyTag question={question} />
+          </Flex>
         </SavedQuestionHeaderButtonContainer>
       </ViewHeaderMainLeftContentContainer>
       {isAdditionalInfoVisible && (
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.tsx b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c4de8c0cbbd665bb257cc61da40da835cc1fbfc0
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.tsx
@@ -0,0 +1,38 @@
+import { t } from "ttag";
+
+import { Flex, HoverCard, Icon, Text, rem } from "metabase/ui";
+import * as Lib from "metabase-lib";
+import type Question from "metabase-lib/v1/Question";
+
+import CS from "./SavedQuestionLeftSide.module.css";
+import { useHiddenSourceTables } from "./hooks";
+
+export function ViewOnlyTag({ question }: { question: Question }) {
+  const { isEditable } = Lib.queryDisplayInfo(question.query());
+  const hiddenSourceTables = useHiddenSourceTables(question);
+
+  if (isEditable) {
+    return null;
+  }
+
+  const tableName = hiddenSourceTables[0]?.displayName;
+
+  return (
+    <HoverCard position="bottom-start" disabled={!tableName}>
+      <HoverCard.Target>
+        <Flex align="center" gap="xs" px={4} py={2} mt={4} className={CS.badge}>
+          <Icon name="lock_filled" size={12} />
+          <Text size="xs" fw="bold">
+            {t`View-only`}
+          </Text>
+        </Flex>
+      </HoverCard.Target>
+      <HoverCard.Dropdown>
+        <Text
+          maw={rem(360)}
+          p="md"
+        >{t`One of the administrators hid the source table “${tableName}”, making this question view-only.`}</Text>
+      </HoverCard.Dropdown>
+    </HoverCard>
+  );
+}
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.unit.spec.tsx b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..585a3469d8383f3d182c4ebd6ea939f3c2de8a9e
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/ViewOnly.unit.spec.tsx
@@ -0,0 +1,297 @@
+import userEvent from "@testing-library/user-event";
+
+import { createMockEntitiesState } from "__support__/store";
+import { renderWithProviders, screen } from "__support__/ui";
+import { getMetadata } from "metabase/selectors/metadata";
+import * as Lib from "metabase-lib";
+import { createQuery } from "metabase-lib/test-helpers";
+import Question from "metabase-lib/v1/Question";
+import type { Card, Database, Table } from "metabase-types/api";
+import { createMockCard } from "metabase-types/api/mocks";
+import {
+  ORDERS,
+  ORDERS_ID,
+  PRODUCTS,
+  PRODUCTS_ID,
+  SAMPLE_DB_ID,
+  createOrdersTable,
+  createProductsTable,
+  createSampleDatabase,
+} from "metabase-types/api/mocks/presets";
+import { createMockState } from "metabase-types/store/mocks";
+
+import { ViewOnlyTag } from "./ViewOnly";
+
+type SetupOpts = {
+  card: Card;
+  database?: Database;
+  tables?: Table[];
+  questions?: Card[];
+};
+
+function setup({
+  card,
+  tables,
+  database = createSampleDatabase(),
+  questions = [],
+}: SetupOpts) {
+  console.warn = jest.fn();
+
+  const storeInitialState = createMockState({
+    entities: createMockEntitiesState({
+      databases: [database],
+      questions: [...questions, card],
+      tables,
+    }),
+  });
+
+  const metadata = getMetadata(storeInitialState);
+  const isSaved = card.id != null;
+  const question = isSaved
+    ? metadata.question(card.id)
+    : new Question(card, metadata);
+
+  if (!question) {
+    throw new Error("question is null");
+  }
+
+  renderWithProviders(
+    <div>
+      <ViewOnlyTag question={question} />
+    </div>,
+    {
+      storeInitialState,
+    },
+  );
+}
+
+async function expectNoPopover() {
+  userEvent.hover(screen.getByText("View-only"));
+  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+}
+
+async function expectPopoverToHaveText(text: string) {
+  userEvent.hover(screen.getByText("View-only"));
+  const dialog = await screen.findByRole("dialog");
+  expect(dialog).toBeInTheDocument();
+  expect(dialog).toHaveTextContent(text);
+}
+
+const HIDDEN_VISIBILITY_TYPES = ["hidden", "technical", "cruft"] as const;
+
+const ORDERS_QUERY = (function () {
+  const query = createQuery({ databaseId: SAMPLE_DB_ID });
+
+  const availableColumns = Lib.fieldableColumns(query, -1);
+  const columns = availableColumns.filter(column => {
+    const info = Lib.displayInfo(query, -1, column);
+    return info.table?.name === "ORDERS" || info.table?.name === "PRODUCTS";
+  });
+
+  return Lib.withFields(query, -1, columns);
+})();
+
+const ORDERS_JOIN_PRODUCTS_QUERY = (function () {
+  let query = createQuery({ databaseId: SAMPLE_DB_ID });
+  const joinTable = Lib.tableOrCardMetadata(query, PRODUCTS_ID);
+
+  query = Lib.join(
+    query,
+    -1,
+    Lib.joinClause(
+      joinTable,
+      [
+        Lib.joinConditionClause(
+          query,
+          -1,
+          Lib.joinConditionOperators(query, -1)[0],
+          Lib.joinConditionLHSColumns(query, -1)[0],
+          Lib.joinConditionRHSColumns(query, -1, joinTable)[0],
+        ),
+      ],
+      Lib.availableJoinStrategies(query, -1)[0],
+    ),
+  );
+
+  return query;
+})();
+
+function createCardFromQuery({
+  query,
+  ...rest
+}: Partial<Card> & { query: Lib.Query }): Card {
+  return createMockCard({
+    ...rest,
+    dataset_query: Lib.toLegacyQuery(query),
+  });
+}
+
+describe("ViewOnlyTag", () => {
+  describe("cards", () => {
+    it("should show the View-only badge when the source card is inaccessible", () => {
+      setup({
+        card: createCardFromQuery({
+          query: createQuery({
+            databaseId: SAMPLE_DB_ID,
+            query: {
+              database: SAMPLE_DB_ID,
+              type: "query",
+              query: {
+                "source-table": "card__123",
+              },
+            },
+          }),
+        }),
+      });
+
+      expect(screen.getByText("View-only")).toBeInTheDocument();
+      expectNoPopover();
+    });
+
+    it("should show the View-only badge when a joined card is inaccessible", () => {
+      setup({
+        card: createCardFromQuery({
+          query: createQuery({
+            query: {
+              type: "query",
+              database: SAMPLE_DB_ID,
+              query: {
+                "source-table": ORDERS_ID,
+                joins: [
+                  {
+                    alias: "Orders Question",
+                    fields: "all",
+                    // This card does not exist
+                    "source-table": "card__123",
+                    condition: [
+                      "=",
+                      ["field", PRODUCTS.ID, null],
+                      ["field", ORDERS.PRODUCT_ID, null],
+                    ],
+                  },
+                ],
+              },
+            },
+          }),
+        }),
+      });
+
+      expect(screen.getByText("View-only")).toBeInTheDocument();
+      expectNoPopover();
+    });
+
+    it("should not show the View-only badge when the source card is accessible", () => {
+      const sourceCard = createMockCard({
+        dataset_query: {
+          type: "query",
+          database: SAMPLE_DB_ID,
+          query: {
+            "source-table": ORDERS_ID,
+          },
+        },
+      });
+      setup({
+        questions: [sourceCard],
+        card: createMockCard({
+          dataset_query: {
+            type: "query",
+            database: SAMPLE_DB_ID,
+            query: {
+              // This card does not exist
+              "source-table": `card__${sourceCard.id}`,
+            },
+          },
+        }),
+      });
+
+      expect(screen.queryByText("View-only")).not.toBeInTheDocument();
+    });
+
+    it("should not show the View-only badge when the joined card is accessible", () => {
+      const sourceCard = createMockCard({
+        dataset_query: {
+          type: "query",
+          database: SAMPLE_DB_ID,
+          query: {
+            "source-table": PRODUCTS_ID,
+          },
+        },
+      });
+      setup({
+        card: createMockCard({
+          dataset_query: {
+            type: "query",
+            database: SAMPLE_DB_ID,
+            query: {
+              "source-table": ORDERS_ID,
+              joins: [
+                {
+                  alias: "Orders Question",
+                  fields: "all",
+                  "source-table": `card__${sourceCard.id}`,
+                  condition: [
+                    "=",
+                    ["field", PRODUCTS.ID, null],
+                    ["field", ORDERS.PRODUCT_ID, null],
+                  ],
+                },
+              ],
+            },
+          },
+        }),
+      });
+
+      expect(screen.queryByText("View-only")).not.toBeInTheDocument();
+    });
+  });
+
+  describe("tables", () => {
+    for (const visibility_type of HIDDEN_VISIBILITY_TYPES) {
+      it(`should show the View-only badge when the source table is ${visibility_type}`, async () => {
+        setup({
+          card: createCardFromQuery({ query: ORDERS_JOIN_PRODUCTS_QUERY }),
+          tables: [
+            createOrdersTable({ visibility_type }),
+            createProductsTable({ visibility_type: null }),
+          ],
+        });
+
+        expect(screen.getByText("View-only")).toBeInTheDocument();
+        await expectPopoverToHaveText(
+          "One of the administrators hid the source table “Orders”, making this question view-only.",
+        );
+      });
+
+      it(`should show the View-only badge when a joined table is ${visibility_type}`, async () => {
+        setup({
+          card: createCardFromQuery({ query: ORDERS_JOIN_PRODUCTS_QUERY }),
+          tables: [
+            createOrdersTable({ visibility_type: null }),
+            createProductsTable({ visibility_type }),
+          ],
+        });
+
+        expect(screen.getByText("View-only")).toBeInTheDocument();
+        await expectPopoverToHaveText(
+          "One of the administrators hid the source table “Products”, making this question view-only.",
+        );
+      });
+    }
+  });
+
+  describe("implicit joins", () => {
+    for (const visibility_type of HIDDEN_VISIBILITY_TYPES) {
+      it(`should not show the View-only badge when an implictly joined table is ${visibility_type}`, async () => {
+        setup({
+          card: createCardFromQuery({ query: ORDERS_QUERY }),
+          tables: [
+            createOrdersTable({ visibility_type: null }),
+            createProductsTable({ visibility_type }),
+          ],
+        });
+
+        expect(screen.queryByText("View-only")).not.toBeInTheDocument();
+      });
+    }
+  });
+});
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/hooks.ts b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/hooks.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02022ac279430d457b2656a98e6babc8d31f33ed
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/SavedQuestionLeftSide/hooks.ts
@@ -0,0 +1,47 @@
+import { useSelector } from "metabase/lib/redux";
+import { getMetadataUnfiltered } from "metabase/selectors/metadata";
+import * as Lib from "metabase-lib";
+import type Question from "metabase-lib/v1/Question";
+
+export function useHiddenSourceTables(
+  question: Question,
+): Lib.TableDisplayInfo[] {
+  const datasetQuery = question.datasetQuery();
+  const metadata = useSelector(getMetadataUnfiltered);
+  const metadataProvider = Lib.metadataProvider(
+    datasetQuery.database,
+    metadata,
+  );
+  const query = Lib.fromLegacyQuery(
+    datasetQuery.database,
+    metadataProvider,
+    datasetQuery,
+  );
+
+  const sourceTableId = Lib.sourceTableOrCardId(query);
+
+  const joinTablesInfo = Lib.stageIndexes(query).flatMap(stageIndex =>
+    Lib.joins(query, stageIndex)
+      .map(join => Lib.joinedThing(query, join))
+      .filter(joinTable => joinTable != null)
+      .map(joinTable => Lib.displayInfo(query, stageIndex, joinTable)),
+  );
+
+  if (sourceTableId) {
+    const sourceTableMetadata = Lib.tableOrCardMetadata(
+      metadataProvider,
+      sourceTableId,
+    );
+    if (sourceTableMetadata) {
+      const sourceTableInfo = Lib.displayInfo(query, -1, sourceTableMetadata);
+      joinTablesInfo.unshift(sourceTableInfo);
+    }
+  }
+
+  return joinTablesInfo.filter(
+    tableInfo =>
+      !tableInfo.isSourceTable ||
+      (tableInfo.visibilityType !== null &&
+        tableInfo.visibilityType !== "normal"),
+  );
+}
diff --git a/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts b/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts
index 8ca44a9d5bd9039a9affeabde2ee3be3bf57ab7e..85371df4459d08bbeb9d7e8ec4bd52d9731f9d03 100644
--- a/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts
+++ b/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts
@@ -231,6 +231,8 @@ import location_component from "./location.svg?component";
 import location_source from "./location.svg?source";
 import lock_component from "./lock.svg?component";
 import lock_source from "./lock.svg?source";
+import lock_filled_component from "./lock_filled.svg?component";
+import lock_filled_source from "./lock_filled.svg?source";
 import mail_component from "./mail.svg?component";
 import mail_source from "./mail.svg?source";
 import mail_filled_component from "./mail_filled.svg?component";
@@ -859,6 +861,10 @@ export const Icons = {
     component: lock_component,
     source: lock_source,
   },
+  lock_filled: {
+    component: lock_filled_component,
+    source: lock_filled_source,
+  },
   mail: {
     component: mail_component,
     source: mail_source,
diff --git a/frontend/src/metabase/ui/components/icons/Icon/icons/lock_filled.svg b/frontend/src/metabase/ui/components/icons/Icon/icons/lock_filled.svg
new file mode 100644
index 0000000000000000000000000000000000000000..121de17eb1a8a4143e6823f76e8c50a513527920
--- /dev/null
+++ b/frontend/src/metabase/ui/components/icons/Icon/icons/lock_filled.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 12 12" fill="currentcolor" stroke="currentColor" clip-rule="evenodd" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <path d="M3.1875 5.9375H3.6875V5.4375V3.75C3.6875 2.47284 4.72284 1.4375 6 1.4375C7.27718 1.4375 8.3125 2.47284 8.3125 3.75V5.4375V5.9375H8.8125H9C9.44873 5.9375 9.8125 6.30127 9.8125 6.75V9.75C9.8125 10.1987 9.44873 10.5625 9 10.5625H3C2.55127 10.5625 2.1875 10.1987 2.1875 9.75V6.75C2.1875 6.30127 2.55127 5.9375 3 5.9375H3.1875ZM7.6875 5.9375H8.1875V5.4375V3.75C8.1875 2.54188 7.20812 1.5625 6 1.5625C4.79188 1.5625 3.8125 2.54188 3.8125 3.75V5.4375V5.9375H4.3125H7.6875Z" />
+</svg>
diff --git a/src/metabase/lib/metadata/calculation.cljc b/src/metabase/lib/metadata/calculation.cljc
index 536160f7993e4d9ac273c3cd00de940b56ff5848..e43e40799d21e44fb69c70ca6858061bf5ae0b0a 100644
--- a/src/metabase/lib/metadata/calculation.cljc
+++ b/src/metabase/lib/metadata/calculation.cljc
@@ -399,7 +399,8 @@
   [query stage-number table]
   (merge (default-display-info query stage-number table)
          {:is-source-table (= (lib.util/source-table-id query) (:id table))
-          :schema (:schema table)}))
+          :schema (:schema table)
+          :visibility-type (:visibility-type table)}))
 
 (def ColumnMetadataWithSource
   "Schema for the column metadata that should be returned by [[metadata]]."