Repository: metabase/metabase
Pull request #57465: Create new PublicSharingSettingsPage component
Author: login: fraserdrops
Labels: name: no-backport
description: Do not backport this PR to any branch
name: .Team/AdminWebapp
description: DEPRECATED  Please use .Team/UXWest instead
Created: 2025-04-30T23:59:47Z | Merged: 2025-05-06T04:00:56Z

Converts the Public Sharing settings tab of Admin settings to use new hooks and be a React page.

<img width="1509" alt="Screenshot 2025-05-06 at 7 05 46 AM" src="https://github.com/user-attachments/assets/8ac5d5b9-8b44-4dcd-934f-b84ae01ca715" />



Diff:
diff --git a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
index a34dee547227..8d0f2367f13b 100644
--- a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
+++ b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
@@ -61,10 +61,11 @@ describe("scenarios > admin > settings > public sharing", () => {
 
   it("should be able to toggle public sharing", () => {
     cy.visit("/admin/settings/public-sharing");
-    cy.findByLabelText("Enable Public Sharing")
-      .should("be.checked")
-      .click()
-      .should("not.be.checked");
+    cy.findByTestId("enable-public-sharing-setting").within(() => {
+      cy.findByText("Enabled").should("be.visible");
+      cy.findByText("Enabled").click();
+      cy.findByText("Disabled").should("be.visible");
+    });
   });
 
   it("should see public dashboards", () => {
diff --git a/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx b/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
new file mode 100644
index 000000000000..6c500226f6c1
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
@@ -0,0 +1,48 @@
+import { t } from "ttag";
+
+import { useSetting } from "metabase/common/hooks";
+import { Stack, Text } from "metabase/ui";
+
+import { AdminSettingInput } from "../widgets/AdminSettingInput";
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "../widgets/PublicLinksListing";
+
+export function PublicSharingSettingsPage() {
+  const publicSharingEnabled = useSetting("enable-public-sharing");
+  return (
+    <Stack gap="xl" p="2rem 2rem 2rem 1rem">
+      <AdminSettingInput
+        name="enable-public-sharing"
+        title={t`Enable Public Sharing`}
+        inputType="boolean"
+      />
+      {publicSharingEnabled && (
+        <Stack gap="xl">
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Dashboards`}
+            </Text>
+
+            <PublicLinksDashboardListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Questions`}
+            </Text>
+
+            <PublicLinksQuestionListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Action Forms`}
+            </Text>
+            <PublicLinksActionListing />
+          </Stack>
+        </Stack>
+      )}
+    </Stack>
+  );
+}
diff --git a/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.unit.spec.tsx b/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.unit.spec.tsx
new file mode 100644
index 000000000000..250431d845b6
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.unit.spec.tsx
@@ -0,0 +1,133 @@
+import userEvent from "@testing-library/user-event";
+import { act } from "react-dom/test-utils";
+
+import {
+  findRequests,
+  setupListPublicActionsEndpoint,
+  setupListPublicCardsEndpoint,
+  setupListPublicDashboardsEndpoint,
+  setupPropertiesEndpoints,
+  setupSettingsEndpoints,
+  setupUpdateSettingEndpoint,
+} from "__support__/server-mocks";
+import { renderWithProviders, screen, waitFor } from "__support__/ui";
+import { UndoListing } from "metabase/containers/UndoListing";
+import type { SettingKey } from "metabase-types/api";
+import {
+  createMockSettingDefinition,
+  createMockSettings,
+} from "metabase-types/api/mocks";
+import { createMockSettingsState } from "metabase-types/store/mocks";
+
+import { PublicSharingSettingsPage } from "./PublicSharingSettingsPage";
+
+const setup = async (enablePublicSharing = false) => {
+  const publicSharingSettings = {
+    "enable-public-sharing": enablePublicSharing,
+  } as const;
+
+  const settings = createMockSettings(publicSharingSettings);
+  setupPropertiesEndpoints(settings);
+  setupUpdateSettingEndpoint();
+  setupSettingsEndpoints(
+    Object.entries(settings).map(([key, value]) =>
+      createMockSettingDefinition({ key: key as SettingKey, value }),
+    ),
+  );
+  setupListPublicActionsEndpoint([
+    {
+      id: 1,
+      name: "Test Action",
+      model_id: 1,
+      public_uuid: "e4f2be29-78df-4c35-9cc4-98d04091ff13",
+    },
+  ]);
+  setupListPublicCardsEndpoint([
+    {
+      name: "Test Question",
+      id: 2,
+      public_uuid: "11bf0e18-34d2-4630-865a-c0bebb75c8b3",
+    },
+  ]);
+  setupListPublicDashboardsEndpoint([
+    {
+      name: "Test Dashboard",
+      id: 3,
+      public_uuid: "16a4568d-c328-4306-9c4b-ec8fbd6e4c8e",
+    },
+  ]);
+
+  renderWithProviders(
+    <div>
+      <PublicSharingSettingsPage />
+      <UndoListing />
+    </div>,
+    {
+      storeInitialState: {
+        settings: createMockSettingsState(settings),
+      },
+    },
+  );
+};
+
+describe("PublicSharingSettingsPage", () => {
+  it("should render the PublicSharingSettingsPage with public sharing disabled", async () => {
+    await act(() => setup(false));
+
+    expect(screen.getByText("Enable Public Sharing")).toBeInTheDocument();
+
+    [
+      "Shared Dashboards",
+      "Shared Questions",
+      "Shared Action Forms",
+      "Test Action",
+      "Test Dashboard",
+      "Test Question",
+    ].forEach((text) => {
+      expect(screen.queryByText(text)).not.toBeInTheDocument();
+    });
+  });
+
+  it("should render the PublicSharingSettingsPage with public sharing enabled", async () => {
+    await act(() => setup(true));
+    [
+      "Enable Public Sharing",
+      "Shared Dashboards",
+      "Shared Questions",
+      "Shared Action Forms",
+      "Test Action",
+      "Test Dashboard",
+      "Test Question",
+    ].forEach((text) => {
+      expect(screen.getByText(text)).toBeInTheDocument();
+    });
+  });
+
+  it("should toggle public sharing", async () => {
+    await setup(false);
+    expect(screen.queryByText("Shared Dashboards")).not.toBeInTheDocument();
+    expect(screen.queryByText("Shared Questions")).not.toBeInTheDocument();
+    expect(screen.queryByText("Shared Action Forms")).not.toBeInTheDocument();
+
+    // Toggle public sharing on
+    const toggle = await screen.findByRole("switch");
+    await userEvent.click(toggle);
+
+    await waitFor(async () => {
+      const puts = await findRequests("PUT");
+      expect(puts).toHaveLength(1);
+    });
+
+    const puts = await findRequests("PUT");
+    const { url: putUrl, body: putBody } = puts[0];
+
+    expect(putUrl).toContain("/api/setting/enable-public-sharing");
+    expect(putBody).toEqual({ value: true });
+
+    // Should show success toast
+    await waitFor(() => {
+      const toast = screen.getByLabelText("check icon");
+      expect(toast).toBeInTheDocument();
+    });
+  });
+});
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.tsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.tsx
index cead5e89942b..ed4223866401 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.tsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.tsx
@@ -22,7 +22,7 @@ export const PublicLinksListing = <
   "data-testid": dataTestId,
 }: {
   data?: T[];
-  isLoading: boolean;
+  isLoading?: boolean;
   revoke?: (item: T) => Promise<unknown>;
   getUrl: (item: T) => string;
   getPublicUrl?: (item: T) => string | null;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.unit.spec.tsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.unit.spec.tsx
new file mode 100644
index 000000000000..3909e6994943
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.unit.spec.tsx
@@ -0,0 +1,122 @@
+import userEvent from "@testing-library/user-event";
+import { act } from "react-dom/test-utils";
+
+import { renderWithProviders, screen, within } from "__support__/ui";
+import { UndoListing } from "metabase/containers/UndoListing";
+import * as Urls from "metabase/lib/urls";
+import type { GetPublicCard } from "metabase-types/api";
+
+import { PublicLinksListing } from "./PublicLinksListing";
+
+const setup = async (props: {
+  publicCardData?: GetPublicCard[];
+  revoke?: () => Promise<unknown>;
+}) => {
+  renderWithProviders(
+    <div>
+      <PublicLinksListing<GetPublicCard>
+        data={props.publicCardData}
+        noLinksMessage={`No questions have been publicly shared yet.`}
+        getUrl={(question) => Urls.question(question)}
+        getPublicUrl={({ name }) => {
+          return `test-public-url-${name}`;
+        }}
+        revoke={props.revoke || Promise.resolve}
+      />
+      <UndoListing />
+    </div>,
+  );
+};
+
+describe("PublicSharingSettingsPage", () => {
+  it("should render an empty table when no data present", async () => {
+    await act(() => setup({}));
+    expect(
+      screen.getByText("No questions have been publicly shared yet."),
+    ).toBeInTheDocument();
+  });
+
+  it("should render table data", async () => {
+    const publicCardData: GetPublicCard[] = [
+      {
+        public_uuid: "5eb3c485-f7d4-40d7-b625-6cc338cf9f4c",
+        name: "Test Question 1",
+        id: 1,
+      },
+      {
+        public_uuid: "316ff9b2-d86e-483f-9156-c766081a1c05",
+        name: "Test Question 2",
+        id: 2,
+      },
+      {
+        public_uuid: "c5c992f9-e761-4a26-a836-de5ce89d1fe5",
+        name: "Test Question 3",
+        id: 3,
+      },
+    ];
+    await act(() =>
+      setup({
+        publicCardData,
+      }),
+    );
+
+    const tableRows = screen.getAllByRole("row");
+
+    // first row contains the table headers
+    tableRows.slice(1).forEach((row, index) => {
+      const cells = within(row).getAllByRole("cell");
+      const cardName = publicCardData[index].name;
+      expect(cells[0]).toHaveTextContent(cardName);
+      expect(cells[1]).toHaveTextContent(`test-public-url-${cardName}`);
+      const anchor = within(cells[1]).getByRole("link");
+      expect(anchor).toBeInTheDocument();
+      expect(anchor).toHaveAttribute(
+        "href",
+        expect.stringContaining(`test-public-url-${cardName}`),
+      );
+      const button = within(cells[2]).getByRole("button");
+      expect(button).toBeInTheDocument();
+      expect(button).toHaveAttribute("aria-label", "Revoke link");
+    });
+  });
+
+  it("should only call revoke function when user confirms in modal", async () => {
+    const mockRevoke = jest.fn().mockResolvedValue(undefined);
+    const publicCardData: GetPublicCard[] = [
+      {
+        public_uuid: "5eb3c485-f7d4-40d7-b625-6cc338cf9f4c",
+        name: "Test Question 1",
+        id: 1,
+      },
+    ];
+
+    await act(() =>
+      setup({
+        publicCardData,
+        revoke: mockRevoke,
+      }),
+    );
+
+    const revokeButton = screen.getByRole("button", { name: /revoke link/i });
+    await userEvent.click(revokeButton);
+
+    expect(screen.getByText("Disable this link?")).toBeInTheDocument();
+    expect(
+      screen.getByText(
+        "They won't work anymore, and can't be restored, but you can create new links.",
+      ),
+    ).toBeInTheDocument();
+
+    const cancelButton = screen.getByRole("button", { name: /cancel/i });
+    await userEvent.click(cancelButton);
+    expect(screen.queryByText("Disable this link?")).not.toBeInTheDocument();
+    expect(mockRevoke).not.toHaveBeenCalled();
+
+    await userEvent.click(revokeButton);
+    const confirmButton = screen.getByRole("button", { name: /yes/i });
+    await userEvent.click(confirmButton);
+    expect(mockRevoke).toHaveBeenCalledTimes(1);
+    expect(mockRevoke).toHaveBeenCalledWith(publicCardData[0]);
+    expect(screen.queryByText("Disable this link?")).not.toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.tsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.tsx
index 2732fca184a5..dd5400675009 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.tsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.tsx
@@ -44,6 +44,7 @@ export const PublicLinksDashboardListing = () => {
 export const PublicLinksQuestionListing = () => {
   const query = useListPublicCardsQuery();
   const [revoke] = useDeleteCardPublicLinkMutation();
+
   return (
     <div className={cx(CS.bordered, CS.rounded, CS.full)}>
       <PublicLinksListing<GetPublicCard>
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.unit.spec.tsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.unit.spec.tsx
new file mode 100644
index 000000000000..1c04c995daf7
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicResources.unit.spec.tsx
@@ -0,0 +1,157 @@
+import {
+  setupListPublicActionsEndpoint,
+  setupListPublicCardsEndpoint,
+  setupListPublicDashboardsEndpoint,
+} from "__support__/server-mocks";
+import { renderWithProviders, screen, waitFor } from "__support__/ui";
+import type {
+  GetPublicAction,
+  GetPublicCard,
+  GetPublicDashboard,
+} from "metabase-types/api";
+
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "./PublicResources";
+
+describe("PublicResources", () => {
+  describe("PublicLinksActionListing", () => {
+    const setupActionListing = async (actions: GetPublicAction[]) => {
+      setupListPublicActionsEndpoint(actions);
+      renderWithProviders(<PublicLinksActionListing />);
+    };
+
+    it("should show empty state when no actions", async () => {
+      setupActionListing([]);
+      await waitFor(() => {
+        expect(
+          screen.getByText("No actions have been publicly shared yet."),
+        ).toBeInTheDocument();
+      });
+    });
+
+    it("should render action listings when loaded", async () => {
+      setupActionListing([
+        {
+          id: 1,
+          name: "Test Action 1",
+          model_id: 1,
+          public_uuid: "e4f2be29-78df-4c35-9cc4-98d04091ff13",
+        },
+        {
+          id: 2,
+          name: "Test Action 2",
+          model_id: 1,
+          public_uuid: "e4f2be29-78df-4c35-9cc4-98d04091ff13",
+        },
+        {
+          id: 3,
+          name: "Test Action 3",
+          model_id: 1,
+          public_uuid: "e4f2be29-78df-4c35-9cc4-98d04091ff13",
+        },
+      ]);
+
+      await waitFor(() => {
+        ["Test Action 1", "Test Action 2", "Test Action 3"].forEach((name) =>
+          expect(screen.getByText(name)).toBeInTheDocument(),
+        );
+      });
+
+      const rows = screen.getAllByRole("row");
+      expect(rows).toHaveLength(4); // Header + 3 rows
+    });
+  });
+
+  describe("PublicLinksQuestionListing", () => {
+    const setupQuestion = async (cards: GetPublicCard[]) => {
+      setupListPublicCardsEndpoint(cards);
+      renderWithProviders(<PublicLinksQuestionListing />);
+    };
+
+    it("should show empty state when no questions", async () => {
+      setupQuestion([]);
+      await waitFor(() => {
+        expect(
+          screen.getByText("No questions have been publicly shared yet."),
+        ).toBeInTheDocument();
+      });
+    });
+
+    it("should render question listings when loaded", async () => {
+      setupQuestion([
+        {
+          name: "Test Question 1",
+          id: 1,
+          public_uuid: "11bf0e18-34d2-4630-865a-c0bebb75c8b3",
+        },
+        {
+          name: "Test Question 2",
+          id: 2,
+          public_uuid: "502876cd-da65-4e72-a60f-253584548940",
+        },
+        {
+          name: "Test Question 3",
+          id: 2,
+          public_uuid: "5bec9624-3102-428a-8b9f-47c56bd96c6c",
+        },
+      ]);
+
+      await waitFor(() => {
+        ["Test Question 1", "Test Question 2", "Test Question 3"].forEach(
+          (name) => expect(screen.getByText(name)).toBeInTheDocument(),
+        );
+      });
+
+      const rows = screen.getAllByRole("row");
+      expect(rows).toHaveLength(4); // Header + 3 rows
+    });
+  });
+
+  describe("PublicLinksDashboardListing", () => {
+    const setupDashboardListing = async (dashboards: GetPublicDashboard[]) => {
+      setupListPublicDashboardsEndpoint(dashboards);
+      renderWithProviders(<PublicLinksDashboardListing />);
+    };
+
+    it("should show empty state when no dashboards", async () => {
+      setupDashboardListing([]);
+      await waitFor(() => {
+        expect(
+          screen.getByText("No dashboards have been publicly shared yet."),
+        ).toBeInTheDocument();
+      });
+    });
+
+    it("should render dashboard listings when loaded", async () => {
+      setupDashboardListing([
+        {
+          name: "Test Dashboard 1",
+          id: 1,
+          public_uuid: "16a4568d-c328-4306-9c4b-ec8fbd6e4c8e",
+        },
+        {
+          name: "Test Dashboard 2",
+          id: 2,
+          public_uuid: "9c92a915-1a78-4455-a99c-c973032960fb",
+        },
+        {
+          name: "Test Dashboard 3",
+          id: 2,
+          public_uuid: "2b80845e-0ff4-4182-8a9a-083cc904f5dd",
+        },
+      ]);
+
+      await waitFor(() => {
+        ["Test Dashboard 1", "Test Dashboard 2", "Test Dashboard 3"].forEach(
+          (name) => expect(screen.getByText(name)).toBeInTheDocument(),
+        );
+      });
+
+      const rows = screen.getAllByRole("row");
+      expect(rows).toHaveLength(4); // Header + 3 rows
+    });
+  });
+});
diff --git a/frontend/src/metabase/admin/settings/selectors/selectors.js b/frontend/src/metabase/admin/settings/selectors/selectors.js
index ac01199fcc7c..0888201dacdc 100644
--- a/frontend/src/metabase/admin/settings/selectors/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors/selectors.js
@@ -25,15 +25,11 @@ import {
 import SettingsLicense from "../components/SettingsLicense";
 import { EmailSettingsPage } from "../components/SettingsPages/EmailSettingsPage";
 import { GeneralSettingsPage } from "../components/SettingsPages/GeneralSettingsPage";
+import { PublicSharingSettingsPage } from "../components/SettingsPages/PublicSharingSettingsPage";
 import { UpdatesSettingsPage } from "../components/SettingsPages/UpdatesSettingsPage";
 import { UploadSettingsPage } from "../components/SettingsPages/UploadSettingsPage";
 import CustomGeoJSONWidget from "../components/widgets/CustomGeoJSONWidget";
 import FormattingWidget from "../components/widgets/FormattingWidget";
-import {
-  PublicLinksActionListing,
-  PublicLinksDashboardListing,
-  PublicLinksQuestionListing,
-} from "../components/widgets/PublicLinksListing";
 import { NotificationSettings } from "../notifications/NotificationSettings";
 import SlackSettings from "../slack/containers/SlackSettings";
 
@@ -252,39 +248,11 @@ export const ADMIN_SETTINGS_SECTIONS = {
     component: UploadSettingsPage,
     settings: [],
   },
-
   "public-sharing": {
     name: t`Public Sharing`,
     order: 90,
-    settings: [
-      {
-        key: "enable-public-sharing",
-        display_name: t`Enable Public Sharing`,
-        description: t`Enable admins to create publicly viewable links (and embeddable iframes) for Questions and Dashboards.`,
-        type: "boolean",
-      },
-      {
-        key: "-public-sharing-dashboards",
-        display_name: t`Shared Dashboards`,
-        widget: PublicLinksDashboardListing,
-        getHidden: (_, derivedSettings) =>
-          !derivedSettings["enable-public-sharing"],
-      },
-      {
-        key: "-public-sharing-questions",
-        display_name: t`Shared Questions`,
-        widget: PublicLinksQuestionListing,
-        getHidden: (_, derivedSettings) =>
-          !derivedSettings["enable-public-sharing"],
-      },
-      {
-        key: "-public-sharing-actions",
-        display_name: t`Shared Action Forms`,
-        widget: PublicLinksActionListing,
-        getHidden: (_, derivedSettings) =>
-          !derivedSettings["enable-public-sharing"],
-      },
-    ],
+    component: PublicSharingSettingsPage,
+    settings: [],
   },
   "embedding-in-other-applications": {
     key: "enable-embedding",
diff --git a/frontend/test/__support__/server-mocks/action.ts b/frontend/test/__support__/server-mocks/action.ts
index 0b1026016c5b..a9baf291d5a8 100644
--- a/frontend/test/__support__/server-mocks/action.ts
+++ b/frontend/test/__support__/server-mocks/action.ts
@@ -1,6 +1,10 @@
 import fetchMock from "fetch-mock";
 
-import type { CardId, WritebackAction } from "metabase-types/api";
+import type {
+  CardId,
+  GetPublicAction,
+  WritebackAction,
+} from "metabase-types/api";
 import {
   createMockImplicitQueryAction,
   createMockQueryAction,
@@ -54,3 +58,9 @@ export function setupModelActionsEndpoints(
 
   actions.forEach((action) => setupActionEndpoints(action));
 }
+
+export function setupListPublicActionsEndpoint(
+  publicActions: GetPublicAction[],
+) {
+  fetchMock.get("path:/api/action/public", publicActions);
+}
diff --git a/frontend/test/__support__/server-mocks/card.ts b/frontend/test/__support__/server-mocks/card.ts
index 43a00dc3f9ca..a58067072b0f 100644
--- a/frontend/test/__support__/server-mocks/card.ts
+++ b/frontend/test/__support__/server-mocks/card.ts
@@ -6,6 +6,7 @@ import type {
   CardId,
   CardQueryMetadata,
   Dataset,
+  GetPublicCard,
 } from "metabase-types/api";
 import { createMockCard } from "metabase-types/api/mocks";
 
@@ -74,3 +75,7 @@ export function setupCardPublicLinkEndpoints(cardId: CardId) {
     id: cardId,
   });
 }
+
+export function setupListPublicCardsEndpoint(publicCards: GetPublicCard[]) {
+  fetchMock.get("path:/api/card/public", publicCards);
+}
diff --git a/frontend/test/__support__/server-mocks/dashboard.ts b/frontend/test/__support__/server-mocks/dashboard.ts
index fe8290783d89..bdce8ce3afc7 100644
--- a/frontend/test/__support__/server-mocks/dashboard.ts
+++ b/frontend/test/__support__/server-mocks/dashboard.ts
@@ -5,6 +5,7 @@ import type {
   Dashboard,
   DashboardId,
   DashboardQueryMetadata,
+  GetPublicDashboard,
 } from "metabase-types/api";
 import { createMockDashboard } from "metabase-types/api/mocks";
 
@@ -51,3 +52,9 @@ export function setupDashboardPublicLinkEndpoints(dashboardId: DashboardId) {
     id: dashboardId,
   });
 }
+
+export function setupListPublicDashboardsEndpoint(
+  publicDashboards: GetPublicDashboard[],
+) {
+  fetchMock.get("path:/api/dashboard/public", publicDashboards);
+}


Reviews:
user: login: iethree
body: Please add a PR description with screenshots 🙏 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:19:35Z
user: login: iethree
body: 💎 Beautiful 💎 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T23:07:07Z
user: login: fraserdrops
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T00:43:24Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T01:23:28Z
user: login: fraserdrops
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T01:43:43Z

Review comments:
diff_hunk: @@ -0,0 +1,48 @@
+import { t } from "ttag";
+
+import { useSetting } from "metabase/common/hooks";
+import { Stack, Text } from "metabase/ui";
+
+import { AdminSettingInput } from "../widgets/AdminSettingInput";
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "../widgets/PublicLinksListing";
+
+export function PublicSharingSettingsPage() {
+  const publicSharingEnabled = useSetting("enable-public-sharing");
+  return (
+    <Stack gap="xl" p="2rem 2rem 2rem 1rem">
+      <AdminSettingInput
+        name="enable-public-sharing"
+        title={t`Enable Public Sharing`}
+        inputType="boolean"
+      />
+      {publicSharingEnabled && (
+        <Stack gap="xl">
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Dashboards`}
+            </Text>
+
+            <PublicLinksDashboardListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
path: frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
user: login: iethree
body: you probably just want to
```
import { SettingTitle } from "../SettingHeader";
```
created_at: 2025-05-05T23:01:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+import { t } from "ttag";
+
+import { useSetting } from "metabase/common/hooks";
+import { Stack, Text } from "metabase/ui";
+
+import { AdminSettingInput } from "../widgets/AdminSettingInput";
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "../widgets/PublicLinksListing";
+
+export function PublicSharingSettingsPage() {
+  const publicSharingEnabled = useSetting("enable-public-sharing");
+  return (
+    <Stack gap="xl" p="2rem 2rem 2rem 1rem">
+      <AdminSettingInput
+        name="enable-public-sharing"
+        title={t`Enable Public Sharing`}
+        inputType="boolean"
+      />
+      {publicSharingEnabled && (
+        <Stack gap="xl">
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Dashboards`}
+            </Text>
+
+            <PublicLinksDashboardListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
path: frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
user: login: fraserdrops
body: The reason I didn't is because label doesn't feel right for these headings because they don't belong to an individual input ie `htmlFor` doesn't really make sense IMO
created_at: 2025-05-06T00:43:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+import { t } from "ttag";
+
+import { useSetting } from "metabase/common/hooks";
+import { Stack, Text } from "metabase/ui";
+
+import { AdminSettingInput } from "../widgets/AdminSettingInput";
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "../widgets/PublicLinksListing";
+
+export function PublicSharingSettingsPage() {
+  const publicSharingEnabled = useSetting("enable-public-sharing");
+  return (
+    <Stack gap="xl" p="2rem 2rem 2rem 1rem">
+      <AdminSettingInput
+        name="enable-public-sharing"
+        title={t`Enable Public Sharing`}
+        inputType="boolean"
+      />
+      {publicSharingEnabled && (
+        <Stack gap="xl">
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Dashboards`}
+            </Text>
+
+            <PublicLinksDashboardListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
path: frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
user: login: iethree
body: you could make htmlFor/id optional?
created_at: 2025-05-06T01:23:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+import { t } from "ttag";
+
+import { useSetting } from "metabase/common/hooks";
+import { Stack, Text } from "metabase/ui";
+
+import { AdminSettingInput } from "../widgets/AdminSettingInput";
+import {
+  PublicLinksActionListing,
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+} from "../widgets/PublicLinksListing";
+
+export function PublicSharingSettingsPage() {
+  const publicSharingEnabled = useSetting("enable-public-sharing");
+  return (
+    <Stack gap="xl" p="2rem 2rem 2rem 1rem">
+      <AdminSettingInput
+        name="enable-public-sharing"
+        title={t`Enable Public Sharing`}
+        inputType="boolean"
+      />
+      {publicSharingEnabled && (
+        <Stack gap="xl">
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
+              {t`Shared Dashboards`}
+            </Text>
+
+            <PublicLinksDashboardListing />
+          </Stack>
+          <Stack gap="sm">
+            <Text c="text-medium" fw="bold" tt="uppercase" display="block">
path: frontend/src/metabase/admin/settings/components/SettingsPages/PublicSharingSettingsPage.tsx
user: login: fraserdrops
body: If it was just for the benefit of this page I would vote against loosening the types. But if it's common I guess we could. 

Fundamentally I believe this header is a different component than the `SettingInput`, it's just that the original design displays them similarly
- `SettingHeader`: Header for a setting input
- `PublicListingHeader`: Header for a setting section

When the new designs come in, this will be rectified so the SettingSectionHeader looks visibly different form the SettingHeader
<img width="500" alt="Screenshot 2025-05-06 at 1 39 28 PM" src="https://github.com/user-attachments/assets/50ca9862-f04f-44f3-8dc2-de8efb593057" />
created_at: 2025-05-06T01:43:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-05-01T00:58:11Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/14500_tests-2b7cf35f-purple)](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/2-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > dashboard > filters > query stages 2-stage queries Q8 - Q4 + 2nd stage with join, custom column, 2 aggregations, 2 breakouts applies fi...</code> | Unable to find a label with the text 'Gadget' within the allowed time. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment&runConclusion=FAILURE&runId=072fb47b-5287-5c97-b1ed-a21ea11e6ab3) |
| <code>scenarios > dashboard warn before leave should warn a user before leaving after adding, removed, moving, or duplicating a tab</code> | Unable to find an element with the text "You're editing this dashboard" in the expected time. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment&runConclusion=FAILURE&runId=b890855a-45e6-5b7e-bb02-5458fe2adb99) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57465?repo=metabase/metabase&commitHash=2b7cf35fe4bcc06e492017c32c08e0c130747a96&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-04T23:56:03Z
body: 
  ## e2e tests failed on `2b7cf35fe4bcc06e492017c32c08e0c130747a96-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/14826344651?pr=57465)

  File | Test Name
  ---- | ---------
  `dashboard.cy.spec.js` | (_flaky_) scenarios > dashboard > warn before leave > should warn a user before leaving after adding, removed, moving, or duplicating a tab
`dashboard-filters-2-stage.cy.spec.ts` | scenarios > dashboard > filters > query stages > 2-stage queries > Q8 - Q4 + 2nd stage with join, custom column, 2 aggregations, 2 breakouts > applies filter to the the dashcard and allows to drill via dashcard header > 2nd stage breakout

author_association: CONTRIBUTOR