From 213d77ff9b4c37d0cb4e3d38d5bad76eb7b06705 Mon Sep 17 00:00:00 2001
From: "metabase-bot[bot]"
 <109303359+metabase-bot[bot]@users.noreply.github.com>
Date: Thu, 22 Feb 2024 08:00:35 -0700
Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=92=20Backport=20Browse=20models=20imp?=
 =?UTF-8?q?rovements=20(#38661)=20(#38730)=20(#38704)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Browse models: Add one filter (#38661)
* Browse models: refactoring (#38730)
* Browse models: expandable/collapsible collections (#38704)
---
 .../onboarding/home/browse.cy.spec.js         |  39 ++-
 .../ModelFilterControls.styled.tsx            |   9 +
 .../ModelFilterControls.tsx                   |  42 +++
 .../content_verification/index.ts             |  16 +-
 .../content_verification/utils.ts             |  36 +++
 .../content_verification/utils.unit.spec.ts   |  42 +++
 .../metabase-enterprise/moderation/index.js   |   2 +
 .../metabase-enterprise/moderation/service.js |   7 +
 .../browse/components/BrowseApp.styled.tsx    |  42 ++-
 .../metabase/browse/components/BrowseApp.tsx  | 105 +++++--
 .../components/BrowseDatabases.styled.tsx     |  30 +-
 .../browse/components/BrowseDatabases.tsx     |  22 +-
 .../browse/components/BrowseHeader.styled.tsx |   2 +-
 .../browse/components/BrowseModels.styled.tsx | 116 ++++++--
 .../browse/components/BrowseModels.tsx        | 224 +++++---------
 .../components/BrowseModels.unit.spec.tsx     | 249 ++++++++++------
 .../metabase/browse/components/ModelGroup.tsx | 209 +++++++++++++
 frontend/src/metabase/browse/constants.js     |   1 -
 frontend/src/metabase/browse/constants.ts     |   3 +
 frontend/src/metabase/browse/utils.ts         | 157 +++++++++-
 .../src/metabase/browse/utils.unit.spec.ts    | 280 ++++++++++++++++++
 frontend/src/metabase/collections/utils.ts    |  12 +-
 frontend/src/metabase/entities/questions.js   |  13 +
 frontend/src/metabase/plugins/index.ts        |  14 +
 .../ui/components/icons/Icon/icons/index.ts   |   6 +
 .../icons/Icon/icons/model_with_badge.svg     |  18 ++
 .../ui/components/layout/Collapse/index.ts    |   2 +
 .../metabase/ui/components/layout/index.ts    |   1 +
 28 files changed, 1324 insertions(+), 375 deletions(-)
 create mode 100644 enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx
 create mode 100644 enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx
 create mode 100644 enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts
 create mode 100644 enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts
 create mode 100644 frontend/src/metabase/browse/components/ModelGroup.tsx
 delete mode 100644 frontend/src/metabase/browse/constants.js
 create mode 100644 frontend/src/metabase/browse/constants.ts
 create mode 100644 frontend/src/metabase/browse/utils.unit.spec.ts
 create mode 100644 frontend/src/metabase/ui/components/icons/Icon/icons/model_with_badge.svg
 create mode 100644 frontend/src/metabase/ui/components/layout/Collapse/index.ts

diff --git a/e2e/test/scenarios/onboarding/home/browse.cy.spec.js b/e2e/test/scenarios/onboarding/home/browse.cy.spec.js
index 63444c9752a..110f9af6001 100644
--- a/e2e/test/scenarios/onboarding/home/browse.cy.spec.js
+++ b/e2e/test/scenarios/onboarding/home/browse.cy.spec.js
@@ -1,4 +1,4 @@
-import { restore } from "e2e/support/helpers";
+import { restore, setTokenFeatures } from "e2e/support/helpers";
 
 describe("scenarios > browse data", () => {
   beforeEach(() => {
@@ -14,12 +14,6 @@ describe("scenarios > browse data", () => {
     cy.findByRole("heading", { name: "Orders Model" }).click();
     cy.findByRole("button", { name: "Filter" });
   });
-  it("can view summary of model's last edit", () => {
-    cy.visit("/");
-    cy.findByRole("listitem", { name: "Browse data" }).click();
-    cy.findByRole("note", /Bobby Tables/).realHover();
-    cy.findByRole("tooltip", { name: /Last edited by Bobby Tables/ });
-  });
   it("can browse to a database", () => {
     cy.visit("/");
     cy.findByRole("listitem", { name: "Browse data" }).click();
@@ -49,18 +43,45 @@ describe("scenarios > browse data", () => {
     );
     cy.location("pathname").should("eq", "/browse/models");
     cy.findByRole("tab", { name: "Databases" }).click();
-    cy.findByRole("heading", { name: "Sample Database" }).click();
     cy.findByRole("listitem", { name: "Browse data" }).click();
     cy.log(
       "/browse/ now defaults to /browse/databases/ because it was the last tab visited",
     );
     cy.location("pathname").should("eq", "/browse/databases");
     cy.findByRole("tab", { name: "Models" }).click();
-    cy.findByRole("heading", { name: "Orders Model" });
     cy.findByRole("listitem", { name: "Browse data" }).click();
     cy.log(
       "/browse/ now defaults to /browse/models/ because it was the last tab visited",
     );
     cy.location("pathname").should("eq", "/browse/models");
   });
+  it("/browse/models has no switch for controlling the 'only show verified models' filter, on an open-source instance", () => {
+    cy.visit("/");
+    cy.findByRole("listitem", { name: "Browse data" }).click();
+    cy.findByRole("switch", { name: /Only show verified models/ }).should(
+      "not.exist",
+    );
+  });
+  it("/browse/models allows models to be filtered, on an enterprise instance", () => {
+    const toggle = () =>
+      cy.findByRole("switch", { name: /Only show verified models/ });
+    setTokenFeatures("all");
+    cy.visit("/");
+    cy.findByRole("listitem", { name: "Browse data" }).click();
+    cy.findByRole("heading", { name: "Our analytics" }).should("not.exist");
+    cy.findByRole("heading", { name: "Orders Model" }).should("not.exist");
+    toggle().next("label").click();
+    toggle().should("have.attr", "aria-checked", "false");
+    cy.findByRole("heading", { name: "Orders Model" }).click();
+    cy.findByLabelText("Move, archive, and more...").click();
+    cy.findByRole("dialog", {
+      name: /ellipsis icon/i,
+    })
+      .findByText(/Verify this model/)
+      .click();
+    cy.visit("/browse");
+    toggle().next("label").click();
+    cy.findByRole("heading", { name: "Orders Model" }).should("be.visible");
+    toggle().should("have.attr", "aria-checked", "true");
+  });
 });
diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx
new file mode 100644
index 00000000000..2c4d1fef20b
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx
@@ -0,0 +1,9 @@
+import styled from "@emotion/styled";
+import { Text } from "metabase/ui";
+
+export const ModelFilterControlSwitchLabel = styled(Text)`
+  text-align: right;
+  font-weight: bold;
+  line-height: 1rem;
+  padding: 0 0.75rem;
+`;
diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx
new file mode 100644
index 00000000000..1ca5f5522b0
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx
@@ -0,0 +1,42 @@
+import { t } from "ttag";
+import { Switch, Text } from "metabase/ui";
+import type { ModelFilterControlsProps } from "metabase/browse/utils";
+
+export const ModelFilterControls = ({
+  actualModelFilters,
+  handleModelFilterChange,
+}: ModelFilterControlsProps) => {
+  const checked = actualModelFilters.onlyShowVerifiedModels;
+  return (
+    <Switch
+      label={
+        <Text
+          align="right"
+          weight="bold"
+          lh="1rem"
+          px=".75rem"
+        >{t`Only show verified models`}</Text>
+      }
+      role="switch"
+      checked={checked}
+      aria-checked={checked}
+      onChange={e => {
+        handleModelFilterChange("onlyShowVerifiedModels", e.target.checked);
+      }}
+      ml="auto"
+      size="sm"
+      labelPosition="left"
+      styles={{
+        root: { display: "flex", alignItems: "center" },
+        body: {
+          alignItems: "center",
+          // Align with tab labels:
+          position: "relative",
+          top: "-.5px",
+        },
+        labelWrapper: { justifyContent: "center", padding: 0 },
+        track: { marginTop: "-1.5px" },
+      }}
+    />
+  );
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts
index f560bc50579..982b0d932c1 100644
--- a/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts
+++ b/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts
@@ -1,7 +1,19 @@
 import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins";
 import { hasPremiumFeature } from "metabase-enterprise/settings";
-import { VerifiedFilter } from "metabase-enterprise/content_verification/VerifiedFilter";
+import { VerifiedFilter } from "./VerifiedFilter";
+import { ModelFilterControls } from "./ModelFilterControls";
+import {
+  availableModelFilters,
+  sortCollectionsByVerification,
+  sortModelsByVerification,
+} from "./utils";
 
 if (hasPremiumFeature("content_verification")) {
-  PLUGIN_CONTENT_VERIFICATION.VerifiedFilter = VerifiedFilter;
+  Object.assign(PLUGIN_CONTENT_VERIFICATION, {
+    VerifiedFilter,
+    ModelFilterControls,
+    availableModelFilters,
+    sortModelsByVerification,
+    sortCollectionsByVerification,
+  });
 }
diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts
new file mode 100644
index 00000000000..0ca62d7323c
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts
@@ -0,0 +1,36 @@
+import type { CollectionEssentials, SearchResult } from "metabase-types/api";
+import type { AvailableModelFilters } from "metabase/browse/utils";
+
+export const sortCollectionsByVerification = (
+  collection1: CollectionEssentials,
+  collection2: CollectionEssentials,
+) => {
+  const isCollection1Official = collection1.authority_level === "official";
+  const isCollection2Official = collection2.authority_level === "official";
+  if (isCollection1Official && !isCollection2Official) {
+    return -1;
+  }
+  if (isCollection2Official && !isCollection1Official) {
+    return 1;
+  }
+  return 0;
+};
+
+export const sortModelsByVerification = (a: SearchResult, b: SearchResult) => {
+  const aVerified = a.moderated_status === "verified";
+  const bVerified = b.moderated_status === "verified";
+  if (aVerified && !bVerified) {
+    return -1;
+  }
+  if (!aVerified && bVerified) {
+    return 1;
+  }
+  return 0;
+};
+
+export const availableModelFilters: AvailableModelFilters = {
+  onlyShowVerifiedModels: {
+    predicate: model => model.moderated_status === "verified",
+    activeByDefault: true,
+  },
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts
new file mode 100644
index 00000000000..489671201ca
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts
@@ -0,0 +1,42 @@
+import type { CollectionEssentials, SearchResult } from "metabase-types/api";
+import { createMockModelResult } from "metabase-types/api/mocks";
+import { availableModelFilters, sortCollectionsByVerification } from "./utils";
+
+describe("Utilities related to content verification", () => {
+  it("include a function that sorts verified collections before unverified collections", () => {
+    const unsorted: CollectionEssentials[] = [
+      {
+        id: 99,
+        authority_level: "official",
+        name: "Collection Zulu - verified",
+      },
+      {
+        id: 1,
+        authority_level: null,
+        name: "Collection Alpha - unverified",
+      },
+    ];
+    const sortFunction = (a: CollectionEssentials, b: CollectionEssentials) =>
+      sortCollectionsByVerification(a, b) || a.name.localeCompare(b.name);
+    const sorted = unsorted.sort(sortFunction);
+    expect(sorted[0].name).toBe("Collection Zulu - verified");
+    expect(sorted[1].name).toBe("Collection Alpha - unverified");
+  });
+  it("include a constant that defines a filter for only showing verified models", () => {
+    const models: SearchResult[] = [
+      createMockModelResult({
+        name: "A verified model",
+        moderated_status: "verified",
+      }),
+      createMockModelResult({
+        name: "An unverified model",
+        moderated_status: null,
+      }),
+    ];
+    const filteredModels = models.filter(
+      availableModelFilters.onlyShowVerifiedModels.predicate,
+    );
+    expect(filteredModels.length).toBe(1);
+    expect(filteredModels[0].name).toBe("A verified model");
+  });
+});
diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/index.js b/enterprise/frontend/src/metabase-enterprise/moderation/index.js
index dfcd4fb080f..2040f08f21d 100644
--- a/enterprise/frontend/src/metabase-enterprise/moderation/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/moderation/index.js
@@ -13,6 +13,7 @@ import {
   MODERATION_STATUS,
   getStatusIcon,
   getModerationTimelineEvents,
+  getQuestionIcon,
   verifyItem,
   removeReview,
   isItemVerified,
@@ -28,6 +29,7 @@ if (hasPremiumFeature("content_verification")) {
     ModerationReviewBanner,
     ModerationStatusIcon,
     getStatusIcon,
+    getQuestionIcon,
     getModerationTimelineEvents,
     getMenuItems: (model, isModerator, reload) => {
       const id = model.id();
diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/service.js b/enterprise/frontend/src/metabase-enterprise/moderation/service.js
index 87d76b795b4..ac7910da8e5 100644
--- a/enterprise/frontend/src/metabase-enterprise/moderation/service.js
+++ b/enterprise/frontend/src/metabase-enterprise/moderation/service.js
@@ -135,3 +135,10 @@ export function getModerationTimelineEvents(reviews, usersById, currentUser) {
     };
   });
 }
+
+export const getQuestionIcon = question => {
+  return (question.model === "dataset" || question.dataset) &&
+    question.moderated_status === "verified"
+    ? { icon: "model_with_badge", tooltip: "Verified model" }
+    : null;
+};
diff --git a/frontend/src/metabase/browse/components/BrowseApp.styled.tsx b/frontend/src/metabase/browse/components/BrowseApp.styled.tsx
index 6c4dee19fbe..64b7f01f315 100644
--- a/frontend/src/metabase/browse/components/BrowseApp.styled.tsx
+++ b/frontend/src/metabase/browse/components/BrowseApp.styled.tsx
@@ -1,7 +1,11 @@
 import styled from "@emotion/styled";
-import { Tabs } from "metabase/ui";
-import { color } from "metabase/lib/colors";
 import EmptyState from "metabase/components/EmptyState";
+import { color } from "metabase/lib/colors";
+import {
+  breakpointMinMedium,
+  breakpointMinSmall,
+} from "metabase/styled-components/theme";
+import { Grid, Icon, Tabs } from "metabase/ui";
 
 export const BrowseAppRoot = styled.div`
   flex: 1;
@@ -15,7 +19,7 @@ export const BrowseTabs = styled(Tabs)`
 `;
 
 export const BrowseTabsList = styled(Tabs.List)`
-  padding: 0 1rem;
+  padding: 0 2.5rem;
   background-color: ${color("white")};
   border-bottom-width: 1px;
 `;
@@ -24,7 +28,8 @@ export const BrowseTab = styled(Tabs.Tab)`
   top: 1px;
   margin-bottom: 1px;
   border-bottom-width: 3px !important;
-  padding: 10px;
+  padding: 10px 0px;
+  margin-right: 10px;
   &:hover {
     color: ${color("brand")};
     background-color: inherit;
@@ -37,7 +42,7 @@ export const BrowseTabsPanel = styled(Tabs.Panel)`
   flex-flow: column nowrap;
   flex: 1;
   height: 100%;
-  padding: 0 1rem;
+  padding: 0 2.5rem;
 `;
 
 export const BrowseContainer = styled.div`
@@ -49,23 +54,25 @@ export const BrowseContainer = styled.div`
 
 export const BrowseDataHeader = styled.header`
   display: flex;
-  padding: 1rem;
+  padding: 1rem 2.5rem;
   padding-bottom: 0.375rem;
   color: ${color("dark")};
   background-color: ${color("white")};
 `;
 
-export const BrowseSectionContainer = styled.div`
-  max-width: 1014px;
-  margin: 0 auto;
-  flex: 1;
-  display: flex;
+export const BrowseGrid = styled(Grid)`
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
+  gap: 0rem 1rem;
+  margin: 0;
   width: 100%;
-`;
 
-export const BrowseTabsContainer = styled(BrowseSectionContainer)`
-  flex-flow: column nowrap;
-  justify-content: flex-start;
+  ${breakpointMinSmall} {
+    padding-bottom: 2.5rem;
+  }
+  ${breakpointMinMedium} {
+    padding-bottom: 3rem;
+  }
 `;
 
 export const CenteredEmptyState = styled(EmptyState)`
@@ -76,3 +83,8 @@ export const CenteredEmptyState = styled(EmptyState)`
   justify-content: center;
   height: 100%;
 `;
+
+export const LearnAboutDataIcon = styled(Icon)`
+  min-width: 14px;
+  min-height: 14px;
+`;
diff --git a/frontend/src/metabase/browse/components/BrowseApp.tsx b/frontend/src/metabase/browse/components/BrowseApp.tsx
index c6ab6d6c34e..362b8827bc9 100644
--- a/frontend/src/metabase/browse/components/BrowseApp.tsx
+++ b/frontend/src/metabase/browse/components/BrowseApp.tsx
@@ -1,30 +1,35 @@
-import { useEffect } from "react";
-import { t } from "ttag";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { push } from "react-router-redux";
-import { Flex, Icon, Text } from "metabase/ui";
+import { t } from "ttag";
+import _ from "underscore";
+import type { SearchResult } from "metabase-types/api";
 import {
   useDatabaseListQuery,
   useSearchListQuery,
 } from "metabase/common/hooks";
-import type { SearchResult } from "metabase-types/api";
 import { useDispatch } from "metabase/lib/redux";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import type { FlexProps } from "metabase/ui";
+import { Flex, Text } from "metabase/ui";
+
 import Link from "metabase/core/components/Link";
-import { isValidBrowseTab, type BrowseTabId } from "../utils";
-import { BrowseDatabases } from "./BrowseDatabases";
-import { BrowseModels } from "./BrowseModels";
+import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins";
+import type { ActualModelFilters } from "../utils";
+import { isValidBrowseTab, type BrowseTabId, filterModels } from "../utils";
 import {
   BrowseAppRoot,
   BrowseContainer,
   BrowseDataHeader,
-  BrowseSectionContainer,
   BrowseTab,
   BrowseTabs,
-  BrowseTabsContainer,
   BrowseTabsList,
   BrowseTabsPanel,
+  LearnAboutDataIcon,
 } from "./BrowseApp.styled";
+import { BrowseDatabases } from "./BrowseDatabases";
 import { BrowseHeaderIconContainer } from "./BrowseHeader.styled";
+import { BrowseModels } from "./BrowseModels";
+
+const availableModelFilters = PLUGIN_CONTENT_VERIFICATION.availableModelFilters;
 
 export const BrowseApp = ({
   tab,
@@ -43,23 +48,60 @@ export const BrowseApp = ({
   const databasesResult = useDatabaseListQuery();
 
   useEffect(() => {
-    if (isValidBrowseTab(tab)) {
-      localStorage.setItem("defaultBrowseTab", tab);
-    }
+    localStorage.setItem("defaultBrowseTab", tab);
   }, [tab]);
 
-  if (!isValidBrowseTab(tab)) {
-    return <LoadingAndErrorWrapper error />;
-  }
+  const getInitialModelFilters = () => {
+    return _.reduce(
+      availableModelFilters,
+      (acc, filter, filterName) => {
+        const storedFilterStatus = localStorage.getItem(
+          `browseFilters.${filterName}`,
+        );
+        const shouldFilterBeActive =
+          storedFilterStatus === null
+            ? filter.activeByDefault
+            : storedFilterStatus === "on";
+        return {
+          ...acc,
+          [filterName]: shouldFilterBeActive,
+        };
+      },
+      {},
+    );
+  };
+
+  const [actualModelFilters, setActualModelFilters] =
+    useState<ActualModelFilters>(getInitialModelFilters);
+  const { data: unfilteredModels = [] } = modelsResult;
+
+  const filteredModels = useMemo(
+    () =>
+      filterModels(unfilteredModels, actualModelFilters, availableModelFilters),
+    [unfilteredModels, actualModelFilters],
+  );
+  const filteredModelsResult = { ...modelsResult, data: filteredModels };
+
+  const handleModelFilterChange = useCallback(
+    (modelFilterName: string, active: boolean) => {
+      localStorage.setItem(
+        `browseFilters.${modelFilterName}`,
+        active ? "on" : "off",
+      );
+      setActualModelFilters((prev: ActualModelFilters) => {
+        return { ...prev, [modelFilterName]: active };
+      });
+    },
+    [setActualModelFilters],
+  );
 
   return (
     <BrowseAppRoot data-testid="browse-app">
       <BrowseContainer>
         <BrowseDataHeader>
-          <BrowseSectionContainer>
+          <BrowseSection>
             <h2>{t`Browse data`}</h2>
-            {tab === "databases" && <LearnAboutDataLink />}
-          </BrowseSectionContainer>
+          </BrowseSection>
         </BrowseDataHeader>
         <BrowseTabs
           value={tab}
@@ -70,25 +112,32 @@ export const BrowseApp = ({
           }}
         >
           <BrowseTabsList>
-            <BrowseSectionContainer>
+            <BrowseSection>
               <BrowseTab key={"models"} value={"models"}>
                 {t`Models`}
               </BrowseTab>
               <BrowseTab key={"databases"} value={"databases"}>
                 {t`Databases`}
               </BrowseTab>
-            </BrowseSectionContainer>
+              {tab === "models" && (
+                <PLUGIN_CONTENT_VERIFICATION.ModelFilterControls
+                  actualModelFilters={actualModelFilters}
+                  handleModelFilterChange={handleModelFilterChange}
+                />
+              )}
+              {tab === "databases" && <LearnAboutDataLink />}
+            </BrowseSection>
           </BrowseTabsList>
           <BrowseTabsPanel key={tab} value={tab}>
-            <BrowseTabsContainer>
+            <BrowseSection direction="column">
               <BrowseTabContent
                 tab={tab}
-                modelsResult={modelsResult}
+                modelsResult={filteredModelsResult}
                 databasesResult={databasesResult}
               >
                 {children}
               </BrowseTabContent>
-            </BrowseTabsContainer>
+            </BrowseSection>
           </BrowseTabsPanel>
         </BrowseTabs>
       </BrowseContainer>
@@ -117,10 +166,10 @@ const BrowseTabContent = ({
   return <BrowseDatabases databasesResult={databasesResult} />;
 };
 const LearnAboutDataLink = () => (
-  <Flex ml="auto" justify="right" style={{ flexBasis: "40.0%" }}>
+  <Flex ml="auto" justify="right" align="center" style={{ flexBasis: "40.0%" }}>
     <Link to="reference">
       <BrowseHeaderIconContainer>
-        <Icon size={14} name="reference" />
+        <LearnAboutDataIcon size={14} name="reference" />
         <Text size="md" lh="1" fw="bold" ml=".5rem" c="inherit">
           {t`Learn about our data`}
         </Text>
@@ -128,3 +177,7 @@ const LearnAboutDataLink = () => (
     </Link>
   </Flex>
 );
+
+const BrowseSection = (props: FlexProps) => (
+  <Flex maw="64rem" m="0 auto" w="100%" {...props} />
+);
diff --git a/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx b/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx
index 95cfcdf6fc6..2fa9e592a8d 100644
--- a/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx
+++ b/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx
@@ -1,37 +1,25 @@
 import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
-import {
-  breakpointMinMedium,
-  breakpointMinSmall,
-} from "metabase/styled-components/theme";
+import { Link } from "react-router";
+
 import Card from "metabase/components/Card";
-import { GridItem, Grid } from "metabase/components/Grid";
+import { color } from "metabase/lib/colors";
+import { BrowseGrid } from "./BrowseApp.styled";
 
-export const DatabaseGrid = styled(Grid)`
-  width: 100%;
+export const DatabaseGrid = styled(BrowseGrid)`
+  margin-top: 1rem;
 `;
 
 export const DatabaseCard = styled(Card)`
   padding: 1.5rem;
-  box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.06) !important;
-
+  margin-bottom: 1rem;
+  box-shadow: none;
   &:hover {
     color: ${color("brand")};
   }
 `;
 
-export const DatabaseGridItem = styled(GridItem)`
-  width: 100%;
-
+export const DatabaseCardLink = styled(Link)`
   &:hover {
     color: ${color("brand")};
   }
-
-  ${breakpointMinSmall} {
-    width: 50%;
-  }
-
-  ${breakpointMinMedium} {
-    width: 33.33%;
-  }
 `;
diff --git a/frontend/src/metabase/browse/components/BrowseDatabases.tsx b/frontend/src/metabase/browse/components/BrowseDatabases.tsx
index ac5ce5e766d..9c369a8c68a 100644
--- a/frontend/src/metabase/browse/components/BrowseDatabases.tsx
+++ b/frontend/src/metabase/browse/components/BrowseDatabases.tsx
@@ -1,22 +1,20 @@
-import _ from "underscore";
 import { t } from "ttag";
 
-import * as Urls from "metabase/lib/urls";
 import { color } from "metabase/lib/colors";
+import * as Urls from "metabase/lib/urls";
 
-import { Icon, Box } from "metabase/ui";
-import Link from "metabase/core/components/Link";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import { Box, Icon, Title } from "metabase/ui";
 
 import type { useDatabaseListQuery } from "metabase/common/hooks";
 
 import NoResults from "assets/img/no_results.svg";
+import { CenteredEmptyState } from "./BrowseApp.styled";
 import {
   DatabaseCard,
+  DatabaseCardLink,
   DatabaseGrid,
-  DatabaseGridItem,
 } from "./BrowseDatabases.styled";
-import { CenteredEmptyState } from "./BrowseApp.styled";
 
 export const BrowseDatabases = ({
   databasesResult,
@@ -36,8 +34,8 @@ export const BrowseDatabases = ({
   return databases.length ? (
     <DatabaseGrid data-testid="database-browser">
       {databases.map(database => (
-        <DatabaseGridItem key={database.id}>
-          <Link to={Urls.browseDatabase(database)}>
+        <div key={database.id}>
+          <DatabaseCardLink to={Urls.browseDatabase(database)}>
             <DatabaseCard>
               <Icon
                 name="database"
@@ -45,10 +43,12 @@ export const BrowseDatabases = ({
                 className="mb3"
                 size={32}
               />
-              <h3 className="text-wrap">{database.name}</h3>
+              <Title order={2} size="1rem" lh="1rem" color="inherit">
+                {database.name}
+              </Title>
             </DatabaseCard>
-          </Link>
-        </DatabaseGridItem>
+          </DatabaseCardLink>
+        </div>
       ))}
     </DatabaseGrid>
   ) : (
diff --git a/frontend/src/metabase/browse/components/BrowseHeader.styled.tsx b/frontend/src/metabase/browse/components/BrowseHeader.styled.tsx
index f1ae9bb0e7a..e9e14b85df3 100644
--- a/frontend/src/metabase/browse/components/BrowseHeader.styled.tsx
+++ b/frontend/src/metabase/browse/components/BrowseHeader.styled.tsx
@@ -4,7 +4,7 @@ import { color } from "metabase/lib/colors";
 export const BrowseHeaderContent = styled.div`
   display: flex;
   align-items: center;
-  padding: 1rem 0.5rem 0.5rem 0.5rem;
+  padding: 1rem 0.5rem 0.5rem 0;
 `;
 
 export const BrowseHeaderIconContainer = styled.div`
diff --git a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx b/frontend/src/metabase/browse/components/BrowseModels.styled.tsx
index 22043bc9600..3423f15815a 100644
--- a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx
+++ b/frontend/src/metabase/browse/components/BrowseModels.styled.tsx
@@ -1,14 +1,18 @@
 import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
-import {
-  breakpointMinMedium,
-  breakpointMinSmall,
-} from "metabase/styled-components/theme";
+import type { HTMLAttributes } from "react";
+
 import Card from "metabase/components/Card";
 import { Ellipsified } from "metabase/core/components/Ellipsified";
-import Link from "metabase/core/components/Link";
-import { Flex, Grid, Group, Icon } from "metabase/ui";
 import IconButtonWrapper from "metabase/components/IconButtonWrapper";
+import Link from "metabase/core/components/Link";
+import { color } from "metabase/lib/colors";
+import { Collapse, Icon, type ButtonProps, Box } from "metabase/ui";
+
+import { BrowseGrid } from "./BrowseApp.styled";
+
+export const ModelCardLink = styled(Link)`
+  margin: 0.5rem 0;
+`;
 
 export const ModelCard = styled(Card)`
   padding: 1.5rem;
@@ -50,45 +54,95 @@ export const MultilineEllipsified = styled(Ellipsified)`
   padding-bottom: 1px;
 `;
 
-export const GridContainer = styled(Grid)`
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
-  gap: 1.5rem 1rem;
-  margin: 0;
-  width: 100%;
-
-  ${breakpointMinSmall} {
-    padding-bottom: 1rem;
-  }
-  ${breakpointMinMedium} {
-    padding-bottom: 3rem;
-  }
-`;
+export const ModelGrid = styled(BrowseGrid)``;
 
-export const CollectionHeaderContainer = styled(Flex)`
+export const CollectionHeaderContainer = styled.button`
   grid-column: 1 / -1;
-  &:not(:first-of-type) {
-    border-top: 1px solid #f0f0f0;
+  display: flex;
+  align-items: center;
+  border-top: 1px solid ${color("border")};
+  margin-top: 0.75rem;
+  cursor: pointer;
+  color: ${color("text-dark")};
+  &:hover {
+    color: ${color("brand")};
+  }
+  :first-of-type {
+    margin-top: 1rem;
+    border-top: none;
   }
 `;
 
 export const CollectionHeaderLink = styled(Link)`
-  &:hover * {
+  display: flex;
+  align-items: center;
+  &:hover {
     color: ${color("brand")};
   }
 `;
 
-export const CollectionHeaderGroup = styled(Group)`
+export const BannerCloseButton = styled(IconButtonWrapper)`
+  color: ${color("text-light")};
+  margin-left: auto;
+`;
+
+export const CollectionCollapse = styled(Collapse)`
+  display: contents;
+`;
+
+export const ContainerExpandCollapseButton = styled.div`
+  border: 0;
+  background-color: inherit;
+`;
+
+export const CollectionExpandCollapseContainer = styled(Box)<
+  ButtonProps & HTMLAttributes<HTMLButtonElement>
+>`
+  display: flex;
+  gap: 0.25rem;
+  justify-content: flex-start;
+  align-items: center;
+  grid-column: 1 / -1;
+  margin: 1rem 0.25rem;
+`;
+
+export const CollectionHeaderToggleContainer = styled.div`
+  padding: 0.5rem;
+  padding-right: 0.75rem;
   position: relative;
-  top: 0.5rem;
+  margin-left: -2.25rem;
+  margin-top: 0.75rem;
+  border: none;
+  background-color: transparent;
+  overflow: unset;
+  &:hover {
+    background-color: inherit;
+    div,
+    svg {
+      color: ${color("brand")};
+    }
+  }
+`;
+
+export const CollectionSummary = styled.div`
+  margin-left: auto;
+  white-space: nowrap;
+  font-size: 0.75rem;
+  color: ${color("text-medium")};
+`;
+
+export const FixedSizeIcon = styled(Icon)<{ size?: number }>`
+  min-width: ${({ size }) => size ?? 16}px;
+  min-height: ${({ size }) => size ?? 16}px;
 `;
 
-export const BannerModelIcon = styled(Icon)`
+export const BannerModelIcon = styled(FixedSizeIcon)`
   color: ${color("text-dark")};
   margin-right: 0.5rem;
 `;
 
-export const BannerCloseButton = styled(IconButtonWrapper)`
-  color: ${color("text-light")};
-  margin-left: auto;
+export const HoverUnderlineLink = styled(Link)`
+  &:hover {
+    text-decoration: underline;
+  }
 `;
diff --git a/frontend/src/metabase/browse/components/BrowseModels.tsx b/frontend/src/metabase/browse/components/BrowseModels.tsx
index 3e63e9e1349..b0a6cd0cec4 100644
--- a/frontend/src/metabase/browse/components/BrowseModels.tsx
+++ b/frontend/src/metabase/browse/components/BrowseModels.tsx
@@ -1,39 +1,21 @@
-import _ from "underscore";
+import { useState } from "react";
 import { t } from "ttag";
 
-import type {
-  Card,
-  CollectionEssentials,
-  SearchResult,
-} from "metabase-types/api";
-import * as Urls from "metabase/lib/urls";
-
-import Link from "metabase/core/components/Link";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import Search from "metabase/entities/search";
-import { useDispatch, useSelector } from "metabase/lib/redux";
-
-import type { useSearchListQuery } from "metabase/common/hooks";
-
-import { Box, Group, Icon, Text, Title } from "metabase/ui";
 import NoResults from "assets/img/no_results.svg";
-
+import type { useSearchListQuery } from "metabase/common/hooks";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import { useSelector } from "metabase/lib/redux";
 import { getLocale } from "metabase/setup/selectors";
-import { isInstanceAnalyticsCollection } from "metabase/collections/utils";
+import { Box } from "metabase/ui";
+import type { SearchResult, CollectionId } from "metabase-types/api";
+
+import { BROWSE_MODELS_LOCALSTORAGE_KEY } from "../constants";
+import { getCollectionViewPreferences, groupModels } from "../utils";
 
-import { color } from "metabase/lib/colors";
-import { getCollectionName, groupModels } from "../utils";
 import { CenteredEmptyState } from "./BrowseApp.styled";
-import {
-  CollectionHeaderContainer,
-  CollectionHeaderGroup,
-  CollectionHeaderLink,
-  GridContainer,
-  ModelCard,
-  MultilineEllipsified,
-} from "./BrowseModels.styled";
-import { LastEdited } from "./LastEdited";
+import { ModelGrid } from "./BrowseModels.styled";
 import { ModelExplanationBanner } from "./ModelExplanationBanner";
+import { ModelGroup } from "./ModelGroup";
 
 export const BrowseModels = ({
   modelsResult,
@@ -43,10 +25,9 @@ export const BrowseModels = ({
   const { data: models = [], error, isLoading } = modelsResult;
   const locale = useSelector(getLocale);
   const localeCode: string | undefined = locale?.code;
-  const modelsFiltered = models.filter(
-    model => !isInstanceAnalyticsCollection(model.collection),
+  const [collectionViewPreferences, setCollectionViewPreferences] = useState(
+    getCollectionViewPreferences,
   );
-  const groupsOfModels = groupModels(modelsFiltered, localeCode);
 
   if (error || isLoading) {
     return (
@@ -58,19 +39,66 @@ export const BrowseModels = ({
     );
   }
 
-  if (modelsFiltered.length) {
+  const handleToggleCollectionExpand = (collectionId: CollectionId) => {
+    const newPreferences = {
+      ...collectionViewPreferences,
+      [collectionId]: {
+        expanded: !(
+          collectionViewPreferences?.[collectionId]?.expanded ?? true
+        ),
+        showAll: !!collectionViewPreferences?.[collectionId]?.showAll,
+      },
+    };
+    setCollectionViewPreferences(newPreferences);
+    localStorage.setItem(
+      BROWSE_MODELS_LOCALSTORAGE_KEY,
+      JSON.stringify(newPreferences),
+    );
+  };
+
+  const handleToggleCollectionShowAll = (collectionId: CollectionId) => {
+    const newPreferences = {
+      ...collectionViewPreferences,
+      [collectionId]: {
+        expanded: collectionViewPreferences?.[collectionId]?.expanded ?? true,
+        showAll: !collectionViewPreferences?.[collectionId]?.showAll,
+      },
+    };
+    setCollectionViewPreferences(newPreferences);
+    localStorage.setItem(
+      BROWSE_MODELS_LOCALSTORAGE_KEY,
+      JSON.stringify(newPreferences),
+    );
+  };
+
+  const groupsOfModels = groupModels(models, localeCode);
+
+  if (models.length) {
     return (
       <>
         <ModelExplanationBanner />
-        <GridContainer role="grid">
-          {groupsOfModels.map(groupOfModels => (
-            <ModelGroup
-              models={groupOfModels}
-              key={`modelgroup-${groupOfModels[0].collection.id}`}
-              localeCode={localeCode}
-            />
-          ))}
-        </GridContainer>
+        <ModelGrid role="grid">
+          {groupsOfModels.map(groupOfModels => {
+            const collectionId = groupOfModels[0].collection.id;
+            return (
+              <ModelGroup
+                expanded={
+                  collectionViewPreferences?.[collectionId]?.expanded ?? true
+                }
+                showAll={!!collectionViewPreferences?.[collectionId]?.showAll}
+                toggleExpanded={() =>
+                  handleToggleCollectionExpand(collectionId)
+                }
+                toggleShowAll={() =>
+                  handleToggleCollectionShowAll(collectionId)
+                }
+                models={groupOfModels}
+                key={`modelgroup-${collectionId}`}
+                localeCode={localeCode}
+              />
+            );
+          })}
+        </ModelGrid>
       </>
     );
   }
@@ -89,115 +117,3 @@ export const BrowseModels = ({
     />
   );
 };
-
-const ModelGroup = ({
-  models,
-  localeCode,
-}: {
-  models: SearchResult[];
-  localeCode: string | undefined;
-}) => {
-  const sortedModels = models.sort((a, b) => {
-    if (!a.name && b.name) {
-      return 1;
-    }
-    if (a.name && !b.name) {
-      return -1;
-    }
-    if (!a.name && !b.name) {
-      return 0;
-    }
-    const nameA = a.name.toLowerCase();
-    const nameB = b.name.toLowerCase();
-    return nameA.localeCompare(nameB, localeCode);
-  });
-  const collection = models[0].collection;
-
-  /** This id is used by aria-labelledby */
-  const collectionHtmlId = `collection-${collection.id}`;
-
-  // TODO: Check padding above the collection header
-  return (
-    <>
-      <CollectionHeader
-        collection={collection}
-        key={collectionHtmlId}
-        id={collectionHtmlId}
-      />
-      {sortedModels.map(model => (
-        <ModelCell
-          model={model}
-          collectionHtmlId={collectionHtmlId}
-          key={`model-${model.id}`}
-        />
-      ))}
-    </>
-  );
-};
-
-interface ModelCellProps {
-  model: SearchResult;
-  collectionHtmlId: string;
-}
-
-const ModelCell = ({ model, collectionHtmlId }: ModelCellProps) => {
-  const headingId = `heading-for-model-${model.id}`;
-
-  const lastEditorFullName =
-    model.last_editor_common_name ?? model.creator_common_name;
-  const timestamp = model.last_edited_at ?? model.created_at ?? "";
-
-  return (
-    <Link
-      aria-labelledby={`${collectionHtmlId} ${headingId}`}
-      key={model.id}
-      to={Urls.model(model as unknown as Partial<Card>)}
-    >
-      <ModelCard>
-        <Box mb="auto">
-          <Icon name="model" size={20} color={color("brand")} />
-        </Box>
-        <Title mb=".25rem" size="1rem">
-          <MultilineEllipsified tooltipMaxWidth="20rem" id={headingId}>
-            {model.name}
-          </MultilineEllipsified>
-        </Title>
-        <LastEdited editorFullName={lastEditorFullName} timestamp={timestamp} />
-      </ModelCard>
-    </Link>
-  );
-};
-
-const CollectionHeader = ({
-  collection,
-  id,
-}: {
-  collection: CollectionEssentials;
-  id: string;
-}) => {
-  const dispatch = useDispatch();
-  const wrappable = { ...collection, model: "collection" };
-  const wrappedCollection = Search.wrapEntity(wrappable, dispatch);
-  const icon = wrappedCollection.getIcon();
-
-  return (
-    <CollectionHeaderContainer
-      id={id}
-      role="heading"
-      pt={"1rem"}
-      mr="1rem"
-      align="center"
-    >
-      <CollectionHeaderGroup grow noWrap>
-        <CollectionHeaderLink to={Urls.collection(collection)}>
-          <Group spacing=".25rem">
-            <Icon {...icon} />
-            <Text weight="bold" color="text-dark">
-              {getCollectionName(collection)}
-            </Text>
-          </Group>
-        </CollectionHeaderLink>
-      </CollectionHeaderGroup>
-    </CollectionHeaderContainer>
-  );
-};
diff --git a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx b/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx
index c019c2cce74..7d0b817d050 100644
--- a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx
+++ b/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx
@@ -1,19 +1,16 @@
-import fetchMock from "fetch-mock";
-import { renderWithProviders, screen, within } from "__support__/ui";
+import userEvent from "@testing-library/user-event";
+
+import { renderWithProviders, screen } from "__support__/ui";
+import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup";
 import type { SearchResult } from "metabase-types/api";
-import { createMockSetupState } from "metabase-types/store/mocks";
 import {
   createMockCollection,
   createMockModelResult,
-  createMockSettingDefinition,
-  createMockSettings,
 } from "metabase-types/api/mocks";
-import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup";
-import {
-  setupPropertiesEndpoints,
-  setupSettingsEndpoints,
-} from "__support__/server-mocks";
-import { groupModels } from "../utils";
+import { createMockSetupState } from "metabase-types/store/mocks";
+
+import { BROWSE_MODELS_LOCALSTORAGE_KEY } from "../constants";
+
 import { BrowseModels } from "./BrowseModels";
 
 const renderBrowseModels = (modelCount: number) => {
@@ -32,13 +29,14 @@ const renderBrowseModels = (modelCount: number) => {
   );
 };
 
-const collectionAlpha = createMockCollection({ id: 0, name: "Alpha" });
+const collectionAlpha = createMockCollection({ id: 99, name: "Alpha" });
 const collectionBeta = createMockCollection({ id: 1, name: "Beta" });
 const collectionCharlie = createMockCollection({ id: 2, name: "Charlie" });
 const collectionDelta = createMockCollection({ id: 3, name: "Delta" });
 const collectionZulu = createMockCollection({ id: 4, name: "Zulu" });
 const collectionAngstrom = createMockCollection({ id: 5, name: "Ångström" });
 const collectionOzgur = createMockCollection({ id: 6, name: "Özgür" });
+const collectionGrande = createMockCollection({ id: 7, name: "Grande" });
 
 const mockModels: SearchResult[] = [
   {
@@ -190,117 +188,178 @@ const mockModels: SearchResult[] = [
   },
   {
     id: 21,
-    name: "Model 20",
+    name: "Model 21",
     collection: defaultRootCollection,
     last_editor_common_name: "Bobby",
     last_edited_at: "2000-01-01T00:00:00.000Z",
   },
   {
     id: 22,
-    name: "Model 21",
+    name: "Model 22",
     collection: defaultRootCollection,
     last_editor_common_name: "Bobby",
     last_edited_at: "2000-01-01T00:00:00.000Z",
   },
+  ...new Array(100).fill(null).map((_, i) => {
+    return createMockModelResult({
+      id: i + 300,
+      name: `Model ${i + 300}`,
+      collection: collectionGrande,
+      last_editor_common_name: "Bobby",
+      last_edited_at: "2000-01-01T00:00:00.000Z",
+    });
+  }),
 ].map(model => createMockModelResult(model));
 
 describe("BrowseModels", () => {
   beforeEach(() => {
-    setupPropertiesEndpoints(createMockSettings());
-    setupSettingsEndpoints([createMockSettingDefinition()]);
-    fetchMock.put("path:/api/setting/default-browse-tab", 200);
-  });
-  it("displays models", async () => {
-    renderBrowseModels(10);
-    for (let i = 0; i < 10; i++) {
-      expect(await screen.findByText(`Model ${i}`)).toBeInTheDocument();
-    }
+    localStorage.clear();
   });
   it("displays a 'no models' message in the Models tab when no models exist", async () => {
     renderBrowseModels(0);
     expect(await screen.findByText("No models here yet")).toBeInTheDocument();
   });
-  it("displays models, organized by parent collection", async () => {
+
+  it("displays collection groups", async () => {
+    renderBrowseModels(10);
+    expect(await screen.findByText("Alpha")).toBeInTheDocument();
+    expect(await screen.findByText("Beta")).toBeInTheDocument();
+    expect(await screen.findByText("Charlie")).toBeInTheDocument();
+    expect(await screen.findByText("Delta")).toBeInTheDocument();
+  });
+
+  it("displays models in collections by default", () => {
+    const modelCount = 22;
+    renderBrowseModels(modelCount);
+    expect(screen.queryByText("No models here yet")).not.toBeInTheDocument();
+    assertThatModelsExist(0, modelCount - 1);
+  });
+
+  it("can collapse collections to hide models within them", async () => {
     renderBrowseModels(10);
-    // Three <a> tags representing models have aria-labelledby="collection-1 model-$id",
-    // and "collection-1" is the id of an element containing text 'Collection 1',
-    // so the following line finds those <a> tags.
-    const modelsInCollection1 = await screen.findAllByLabelText("Alpha");
-    expect(modelsInCollection1).toHaveLength(3);
-    const modelsInCollection2 = await screen.findAllByLabelText("Beta");
-    expect(modelsInCollection2).toHaveLength(3);
+    userEvent.click(await screen.findByLabelText("collapse Alpha"));
+    expect(screen.queryByText("Model 0")).not.toBeInTheDocument();
+    expect(screen.queryByText("Model 1")).not.toBeInTheDocument();
+    expect(screen.queryByText("Model 2")).not.toBeInTheDocument();
+
+    userEvent.click(await screen.findByLabelText("collapse Beta"));
+    expect(screen.queryByText("Model 3")).not.toBeInTheDocument();
+    expect(screen.queryByText("Model 4")).not.toBeInTheDocument();
+    expect(screen.queryByText("Model 5")).not.toBeInTheDocument();
   });
+
+  it("can expand a collection to see models within it", async () => {
+    renderBrowseModels(10);
+    userEvent.click(await screen.findByLabelText("collapse Alpha"));
+    expect(screen.queryByText("Model 0")).not.toBeInTheDocument();
+    userEvent.click(await screen.findByLabelText("expand Alpha"));
+    expect(await screen.findByText("Model 0")).toBeInTheDocument();
+  });
+
   it("displays the Our Analytics collection if it has a model", async () => {
-    renderBrowseModels(23);
-    const modelsInOurAnalytics = await screen.findAllByLabelText(
-      "Our analytics",
-    );
-    expect(modelsInOurAnalytics).toHaveLength(2);
+    renderBrowseModels(25);
+    await screen.findByText("Alpha");
+    await screen.findByText("Our analytics");
+    expect(await screen.findByText("Model 20")).toBeInTheDocument();
+    expect(await screen.findByText("Model 21")).toBeInTheDocument();
+    expect(await screen.findByText("Model 22")).toBeInTheDocument();
   });
-  it("displays last edited information about models", async () => {
-    jest.useFakeTimers().setSystemTime(new Date("2024-12-15T12:00:00.000Z"));
 
-    renderBrowseModels(12);
-    const howLongAgo = /\d+(min|h|d|mo|yr)/;
-    const findWhenModelWasEdited = async (modelName: string) =>
-      (
-        await within(await screen.findByLabelText(modelName)).findByText(
-          howLongAgo,
-        )
-      )?.textContent?.match(howLongAgo)?.[0];
+  it("shows the first six models in a collection by default", async () => {
+    renderBrowseModels(9999);
+    expect(await screen.findByText("100 models")).toBeInTheDocument();
+    expect(await screen.findByText("Show all")).toBeInTheDocument();
+    assertThatModelsExist(300, 305);
+  });
 
-    expect(await findWhenModelWasEdited("Model 0")).toBe("1min");
-    expect(await findWhenModelWasEdited("Model 1")).toBe("1min");
-    expect(await findWhenModelWasEdited("Model 2")).toBe("1min");
-    expect(await findWhenModelWasEdited("Model 3")).toBe("10min");
-    expect(await findWhenModelWasEdited("Model 4")).toBe("1h");
-    expect(await findWhenModelWasEdited("Model 5")).toBe("14h");
-    expect(await findWhenModelWasEdited("Model 6")).toBe("1d");
-    expect(await findWhenModelWasEdited("Model 7")).toBe("5d");
-    expect(await findWhenModelWasEdited("Model 8")).toBe("1mo");
-    expect(await findWhenModelWasEdited("Model 9")).toBe("10mo");
-    expect(await findWhenModelWasEdited("Model 10")).toBe("1yr");
-    expect(await findWhenModelWasEdited("Model 11")).toBe("5yr");
+  it("can show more than 6 models by clicking 'Show all'", async () => {
+    renderBrowseModels(9999);
+    await screen.findByText("6 of 100");
+    expect(screen.queryByText("Model 350")).not.toBeInTheDocument();
+    userEvent.click(await screen.findByText("Show all"));
+    assertThatModelsExist(300, 399);
+  });
 
-    jest.useRealTimers();
+  it("can show less than all models by clicking 'Show less'", async () => {
+    renderBrowseModels(9999);
+    expect(screen.queryByText("Model 399")).not.toBeInTheDocument();
+    userEvent.click(await screen.findByText("Show all"));
+    await screen.findByText("Model 301");
+    expect(screen.getByText("Model 399")).toBeInTheDocument();
+    userEvent.click(await screen.findByText("Show less"));
+    await screen.findByText("Model 301");
+    expect(screen.queryByText("Model 399")).not.toBeInTheDocument();
   });
-  it("has a function that groups models by collection, sorting the collections alphabetically when English is the locale", () => {
-    const groupedModels = groupModels(mockModels, "en-US");
-    expect(groupedModels[0][0].collection.name).toEqual("Alpha");
-    expect(groupedModels[0]).toHaveLength(3);
-    expect(groupedModels[1][0].collection.name).toEqual("Ångström");
-    expect(groupedModels[1]).toHaveLength(3);
-    expect(groupedModels[2][0].collection.name).toEqual("Beta");
-    expect(groupedModels[2]).toHaveLength(3);
-    expect(groupedModels[3][0].collection.name).toEqual("Charlie");
-    expect(groupedModels[3]).toHaveLength(3);
-    expect(groupedModels[4][0].collection.name).toEqual("Delta");
-    expect(groupedModels[4]).toHaveLength(3);
-    expect(groupedModels[5][0].collection.name).toEqual("Our analytics");
-    expect(groupedModels[5]).toHaveLength(2);
-    expect(groupedModels[6][0].collection.name).toEqual("Özgür");
-    expect(groupedModels[6]).toHaveLength(3);
-    expect(groupedModels[7][0].collection.name).toEqual("Zulu");
-    expect(groupedModels[7]).toHaveLength(3);
+
+  it("persists show-all state when expanding and collapsing collections", async () => {
+    renderBrowseModels(9999);
+    userEvent.click(screen.getByText("Show all"));
+    expect(await screen.findByText("Model 301")).toBeInTheDocument();
+    expect(screen.getByText("Model 399")).toBeInTheDocument();
+
+    userEvent.click(screen.getByLabelText("collapse Grande"));
+    expect(screen.queryByText("Model 301")).not.toBeInTheDocument();
+    expect(screen.queryByText("Model 399")).not.toBeInTheDocument();
+
+    userEvent.click(screen.getByLabelText("expand Grande"));
+    expect(await screen.findByText("Model 301")).toBeInTheDocument();
+    expect(screen.getByText("Model 399")).toBeInTheDocument();
   });
 
-  it("has a function that groups models by collection, sorting the collections alphabetically when Swedish is the locale", () => {
-    const groupedModels = groupModels(mockModels, "sv-SV");
-    expect(groupedModels[0][0].collection.name).toEqual("Alpha");
-    expect(groupedModels[0]).toHaveLength(3);
-    expect(groupedModels[1][0].collection.name).toEqual("Beta");
-    expect(groupedModels[1]).toHaveLength(3);
-    expect(groupedModels[2][0].collection.name).toEqual("Charlie");
-    expect(groupedModels[2]).toHaveLength(3);
-    expect(groupedModels[3][0].collection.name).toEqual("Delta");
-    expect(groupedModels[3]).toHaveLength(3);
-    expect(groupedModels[4][0].collection.name).toEqual("Our analytics");
-    expect(groupedModels[4]).toHaveLength(2);
-    expect(groupedModels[5][0].collection.name).toEqual("Zulu");
-    expect(groupedModels[5]).toHaveLength(3);
-    expect(groupedModels[6][0].collection.name).toEqual("Ångström");
-    expect(groupedModels[6]).toHaveLength(3);
-    expect(groupedModels[7][0].collection.name).toEqual("Özgür");
-    expect(groupedModels[7]).toHaveLength(3);
+  describe("local storage", () => {
+    it("persists the expanded state of collections in local storage", async () => {
+      renderBrowseModels(10);
+      userEvent.click(await screen.findByLabelText("collapse Alpha"));
+      expect(screen.queryByText("Model 0")).not.toBeInTheDocument();
+      expect(localStorage.getItem(BROWSE_MODELS_LOCALSTORAGE_KEY)).toEqual(
+        JSON.stringify({ 99: { expanded: false, showAll: false } }),
+      );
+    });
+
+    it("loads the collapsed state of collections from local storage", async () => {
+      localStorage.setItem(
+        BROWSE_MODELS_LOCALSTORAGE_KEY,
+        JSON.stringify({ 99: { expanded: false, showAll: false } }),
+      );
+      renderBrowseModels(10);
+      expect(screen.queryByText("Model 0")).not.toBeInTheDocument();
+    });
+
+    it("persists the 'show all' state of collections in local storage", async () => {
+      renderBrowseModels(9999);
+      userEvent.click(await screen.findByText("Show all"));
+      await screen.findByText("Model 399");
+      expect(localStorage.getItem(BROWSE_MODELS_LOCALSTORAGE_KEY)).toEqual(
+        JSON.stringify({ 7: { expanded: true, showAll: true } }),
+      );
+    });
+
+    it("loads the 'show all' state of collections from local storage", async () => {
+      localStorage.setItem(
+        BROWSE_MODELS_LOCALSTORAGE_KEY,
+        JSON.stringify({ 7: { expanded: true, showAll: true } }),
+      );
+      renderBrowseModels(9999);
+      expect(await screen.findByText("Show less")).toBeInTheDocument();
+      assertThatModelsExist(300, 399);
+    });
+
+    it("can deal with invalid local storage data", async () => {
+      localStorage.setItem(BROWSE_MODELS_LOCALSTORAGE_KEY, "{invalid json[[[}");
+      renderBrowseModels(10);
+      expect(await screen.findByText("Model 0")).toBeInTheDocument();
+      userEvent.click(await screen.findByLabelText("collapse Alpha"));
+      expect(screen.queryByText("Model 0")).not.toBeInTheDocument();
+      // ignores invalid data and persists the new state
+      expect(localStorage.getItem(BROWSE_MODELS_LOCALSTORAGE_KEY)).toEqual(
+        JSON.stringify({ 99: { expanded: false, showAll: false } }),
+      );
+    });
   });
 });
+
+function assertThatModelsExist(startId: number, endId: number) {
+  for (let i = startId; i <= endId; i++) {
+    expect(screen.getByText(`Model ${i}`)).toBeInTheDocument();
+  }
+}
diff --git a/frontend/src/metabase/browse/components/ModelGroup.tsx b/frontend/src/metabase/browse/components/ModelGroup.tsx
new file mode 100644
index 00000000000..981eb74f6cd
--- /dev/null
+++ b/frontend/src/metabase/browse/components/ModelGroup.tsx
@@ -0,0 +1,209 @@
+import { useMemo } from "react";
+import { t, c, msgid } from "ttag";
+
+import { color } from "metabase/lib/colors";
+import * as Urls from "metabase/lib/urls";
+import { Box, Icon, Title, Button, Flex } from "metabase/ui";
+import type {
+  Card,
+  SearchResult,
+  CollectionEssentials,
+} from "metabase-types/api";
+
+import { getCollectionName, sortModels, getIcon } from "../utils";
+
+import {
+  CollectionCollapse,
+  CollectionExpandCollapseContainer,
+  CollectionHeaderContainer,
+  CollectionHeaderToggleContainer,
+  CollectionSummary,
+  FixedSizeIcon,
+  ModelCard,
+  ModelCardLink,
+  MultilineEllipsified,
+  HoverUnderlineLink,
+} from "./BrowseModels.styled";
+
+const MAX_COLLAPSED_MODELS = 6;
+
+export const ModelGroup = ({
+  models,
+  localeCode,
+  expanded,
+  showAll,
+  toggleExpanded,
+  toggleShowAll,
+}: {
+  models: SearchResult[];
+  localeCode: string | undefined;
+  expanded: boolean;
+  showAll: boolean;
+  toggleExpanded: () => void;
+  toggleShowAll: () => void;
+}) => {
+  const { sortedModels, aboveFoldModelsCount } = useMemo(() => {
+    const sortedModels = [...models].sort((a, b) =>
+      sortModels(a, b, localeCode),
+    );
+
+    const aboveFoldModelsCount =
+      models.length >= MAX_COLLAPSED_MODELS
+        ? MAX_COLLAPSED_MODELS
+        : models.length;
+
+    return { sortedModels, aboveFoldModelsCount };
+  }, [models, localeCode]);
+
+  const visibleModels = useMemo(() => {
+    return showAll ? sortedModels : sortedModels.slice(0, MAX_COLLAPSED_MODELS);
+  }, [sortedModels, showAll]);
+
+  const collection = models[0].collection;
+
+  /** This id is used by aria-labelledby */
+  const collectionHtmlId = `collection-${collection.id}`;
+
+  return (
+    <>
+      <CollectionHeader
+        collection={collection}
+        onClick={toggleExpanded}
+        expanded={expanded}
+        modelsCount={models.length}
+      />
+      <CollectionCollapse in={expanded} transitionDuration={0}>
+        {visibleModels.map(model => (
+          <ModelCell
+            model={model}
+            collectionHtmlId={collectionHtmlId}
+            key={`model-${model.id}`}
+          />
+        ))}
+        <ShowMoreFooter
+          hasMoreModels={models.length > MAX_COLLAPSED_MODELS}
+          shownModelsCount={aboveFoldModelsCount}
+          allModelsCount={models.length}
+          showAll={showAll}
+          onClick={toggleShowAll}
+        />
+      </CollectionCollapse>
+    </>
+  );
+};
+
+const CollectionHeader = ({
+  collection,
+  onClick,
+  expanded,
+  modelsCount,
+}: {
+  collection: CollectionEssentials;
+  onClick: () => void;
+  expanded: boolean;
+  modelsCount: number;
+}) => {
+  const icon = getIcon({ ...collection, model: "collection" });
+  const collectionHtmlId = `collection-${collection.id}`;
+
+  return (
+    <CollectionHeaderContainer
+      id={collectionHtmlId}
+      role="heading"
+      onClick={onClick}
+    >
+      <CollectionHeaderToggleContainer>
+        <FixedSizeIcon
+          aria-label={
+            expanded
+              ? t`collapse ${getCollectionName(collection)}`
+              : t`expand ${getCollectionName(collection)}`
+          }
+          name={expanded ? "chevrondown" : "chevronright"}
+        />
+      </CollectionHeaderToggleContainer>
+      <Flex pt="1.5rem" pb="0.75rem" w="100%">
+        <Flex>
+          <FixedSizeIcon {...icon} />
+          <Title size="1rem" lh="1rem" ml=".25rem" mr="1rem" color="inherit">
+            {getCollectionName(collection)}
+          </Title>
+        </Flex>
+        <CollectionSummary>
+          <HoverUnderlineLink
+            to={Urls.collection(collection)}
+            onClick={e => e.stopPropagation() /* prevent collapse */}
+          >
+            {c("{0} is the number of models in a collection").ngettext(
+              msgid`${modelsCount} model`,
+              `${modelsCount} models`,
+              modelsCount,
+            )}
+          </HoverUnderlineLink>
+        </CollectionSummary>
+      </Flex>
+    </CollectionHeaderContainer>
+  );
+};
+
+const ShowMoreFooter = ({
+  hasMoreModels,
+  shownModelsCount,
+  allModelsCount,
+  onClick,
+  showAll,
+}: {
+  hasMoreModels: boolean;
+  shownModelsCount: number;
+  allModelsCount: number;
+  showAll: boolean;
+  onClick: () => void;
+}) => {
+  if (!hasMoreModels) {
+    return null;
+  }
+
+  return (
+    <CollectionExpandCollapseContainer>
+      {!showAll && `${shownModelsCount} of ${allModelsCount}`}
+      <Button variant="subtle" lh="inherit" p="0" onClick={onClick}>
+        {showAll
+          ? c("For a button that collapses a list of models").t`Show less`
+          : c("For a button that expands a list of models").t`Show all`}
+      </Button>
+    </CollectionExpandCollapseContainer>
+  );
+};
+
+interface ModelCellProps {
+  model: SearchResult;
+  collectionHtmlId: string;
+}
+
+const ModelCell = ({ model, collectionHtmlId }: ModelCellProps) => {
+  const headingId = `heading-for-model-${model.id}`;
+
+  const icon = getIcon(model);
+
+  return (
+    <ModelCardLink
+      aria-labelledby={`${collectionHtmlId} ${headingId}`}
+      key={model.id}
+      to={Urls.model(model as unknown as Partial<Card>)}
+    >
+      <ModelCard>
+        <Box mb="auto">
+          <Icon {...icon} size={20} color={color("brand")} />
+        </Box>
+        <Title mb=".25rem" size="1rem">
+          <MultilineEllipsified tooltipMaxWidth="20rem" id={headingId}>
+            {model.name}
+          </MultilineEllipsified>
+        </Title>
+        <MultilineEllipsified tooltipMaxWidth="20rem">
+          {model.description}
+        </MultilineEllipsified>
+      </ModelCard>
+    </ModelCardLink>
+  );
+};
diff --git a/frontend/src/metabase/browse/constants.js b/frontend/src/metabase/browse/constants.js
deleted file mode 100644
index 4c375c7e51d..00000000000
--- a/frontend/src/metabase/browse/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const RELOAD_INTERVAL = 2000;
diff --git a/frontend/src/metabase/browse/constants.ts b/frontend/src/metabase/browse/constants.ts
new file mode 100644
index 00000000000..12b4baf632d
--- /dev/null
+++ b/frontend/src/metabase/browse/constants.ts
@@ -0,0 +1,3 @@
+export const RELOAD_INTERVAL = 2000;
+
+export const BROWSE_MODELS_LOCALSTORAGE_KEY = "browseModelsViewPreferences";
diff --git a/frontend/src/metabase/browse/utils.ts b/frontend/src/metabase/browse/utils.ts
index bdc9e8e2224..7c8b7a37de6 100644
--- a/frontend/src/metabase/browse/utils.ts
+++ b/frontend/src/metabase/browse/utils.ts
@@ -3,9 +3,20 @@ import { t } from "ttag";
 import {
   canonicalCollectionId,
   coerceCollectionId,
+  isInstanceAnalyticsCollection,
   isRootCollection,
+  isValidCollectionId,
 } from "metabase/collections/utils";
-import type { CollectionEssentials, SearchResult } from "metabase-types/api";
+import { entityForObject } from "metabase/lib/schema";
+import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins";
+import type { IconName } from "metabase/ui";
+import type {
+  CollectionEssentials,
+  SearchResult,
+  CollectionId,
+} from "metabase-types/api";
+
+import { BROWSE_MODELS_LOCALSTORAGE_KEY } from "./constants";
 
 export const getCollectionName = (collection: CollectionEssentials) => {
   if (isRootCollection(collection)) {
@@ -29,21 +40,42 @@ export const groupModels = (
     getCollectionIdForSorting(model.collection),
   );
   const groupsOfModels: SearchResult[][] = Object.values(groupedModels);
-  const sortFunction = (a: SearchResult[], b: SearchResult[]) => {
+  const sortGroupsByCollection = (a: SearchResult[], b: SearchResult[]) => {
     const collection1 = a[0].collection;
     const collection2 = b[0].collection;
 
-    const collection1AL = collection1.authority_level ?? "z";
-    const collection2AL = collection2.authority_level ?? "z";
-    if (collection1AL !== collection2AL) {
-      return collection1AL.localeCompare(collection2AL, locale);
+    // Sort instance analytics collection to the end
+    const collection1IsInstanceAnalyticsCollection =
+      isInstanceAnalyticsCollection(collection1);
+    const collection2IsInstanceAnalyticsCollection =
+      isInstanceAnalyticsCollection(collection2);
+    if (
+      collection1IsInstanceAnalyticsCollection &&
+      !collection2IsInstanceAnalyticsCollection
+    ) {
+      return 1;
+    }
+    if (
+      collection2IsInstanceAnalyticsCollection &&
+      !collection1IsInstanceAnalyticsCollection
+    ) {
+      return -1;
+    }
+
+    const sortValueFromPlugin =
+      PLUGIN_CONTENT_VERIFICATION.sortCollectionsByVerification(
+        collection1,
+        collection2,
+      );
+    if (sortValueFromPlugin) {
+      return sortValueFromPlugin;
     }
 
     const name1 = getCollectionName(collection1);
     const name2 = getCollectionName(collection2);
     return name1.localeCompare(name2, locale);
   };
-  groupsOfModels.sort(sortFunction);
+  groupsOfModels.sort(sortGroupsByCollection);
   return groupsOfModels;
 };
 
@@ -51,3 +83,114 @@ export type BrowseTabId = "models" | "databases";
 
 export const isValidBrowseTab = (value: unknown): value is BrowseTabId =>
   value === "models" || value === "databases";
+
+export type AvailableModelFilters = Record<
+  string,
+  {
+    predicate: (value: SearchResult) => boolean;
+    activeByDefault: boolean;
+  }
+>;
+
+export type ModelFilterControlsProps = {
+  actualModelFilters: ActualModelFilters;
+  handleModelFilterChange: (filterName: string, active: boolean) => void;
+};
+
+export const sortModels = (
+  a: SearchResult,
+  b: SearchResult,
+  localeCode?: string,
+) => {
+  const sortValueFromPlugin =
+    PLUGIN_CONTENT_VERIFICATION.sortModelsByVerification(a, b);
+  if (sortValueFromPlugin) {
+    return sortValueFromPlugin;
+  }
+
+  if (a.name && !b.name) {
+    return -1;
+  }
+  if (!a.name && !b.name) {
+    return 0;
+  }
+  if (!a.name && b.name) {
+    return 1;
+  }
+  if (a.name && !b.name) {
+    return -1;
+  }
+  if (!a.name && !b.name) {
+    return 0;
+  }
+  const nameA = a.name.toLowerCase();
+  const nameB = b.name.toLowerCase();
+  return nameA.localeCompare(nameB, localeCode);
+};
+
+export type ActualModelFilters = Record<string, boolean>;
+
+export const filterModels = (
+  unfilteredModels: SearchResult[],
+  actualModelFilters: ActualModelFilters,
+  availableModelFilters: AvailableModelFilters,
+) => {
+  return _.reduce(
+    actualModelFilters,
+    (acc, shouldFilterBeActive, filterName) =>
+      shouldFilterBeActive
+        ? acc.filter(availableModelFilters[filterName].predicate)
+        : acc,
+    unfilteredModels,
+  );
+};
+
+type CollectionPrefs = Partial<Record<CollectionId, ModelVisibilityPrefs>>;
+
+type ModelVisibilityPrefs = {
+  expanded: boolean;
+  showAll: boolean;
+};
+
+const isRecordWithCollectionIdKeys = (
+  prefs: unknown,
+): prefs is Record<CollectionId, any> =>
+  !!prefs &&
+  typeof prefs === "object" &&
+  !Array.isArray(prefs) &&
+  Object.keys(prefs).every(isValidCollectionId);
+
+const isValidModelVisibilityPrefs = (
+  value: unknown,
+): value is ModelVisibilityPrefs =>
+  typeof value === "object" &&
+  value !== null &&
+  Object.keys(value).includes("expanded") &&
+  Object.keys(value).includes("showAll") &&
+  Object.values(value).every(_.isBoolean);
+
+const isValidCollectionPrefs = (prefs: unknown): prefs is CollectionPrefs =>
+  isRecordWithCollectionIdKeys(prefs) &&
+  Object.values(prefs).every(isValidModelVisibilityPrefs);
+
+export const getCollectionViewPreferences = (): CollectionPrefs => {
+  try {
+    const collectionPrefs = JSON.parse(
+      localStorage.getItem(BROWSE_MODELS_LOCALSTORAGE_KEY) ?? "{}",
+    );
+
+    if (isValidCollectionPrefs(collectionPrefs)) {
+      return collectionPrefs;
+    }
+
+    return {};
+  } catch (err) {
+    console.error(err);
+    return {};
+  }
+};
+
+export const getIcon = (item: unknown): { name: IconName; color: string } => {
+  const entity = entityForObject(item);
+  return entity?.objectSelectors?.getIcon?.(item) || { name: "folder" };
+};
diff --git a/frontend/src/metabase/browse/utils.unit.spec.ts b/frontend/src/metabase/browse/utils.unit.spec.ts
new file mode 100644
index 00000000000..c6304c1fae8
--- /dev/null
+++ b/frontend/src/metabase/browse/utils.unit.spec.ts
@@ -0,0 +1,280 @@
+import type { SearchResult } from "metabase-types/api";
+import {
+  createMockCollection,
+  createMockModelResult,
+} from "metabase-types/api/mocks";
+import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup";
+import type { ActualModelFilters, AvailableModelFilters } from "./utils";
+import { filterModels, groupModels } from "./utils";
+
+const collectionAlpha = createMockCollection({ id: 0, name: "Alpha" });
+const collectionBeta = createMockCollection({ id: 1, name: "Beta" });
+const collectionCharlie = createMockCollection({ id: 2, name: "Charlie" });
+const collectionDelta = createMockCollection({ id: 3, name: "Delta" });
+const collectionZulu = createMockCollection({ id: 4, name: "Zulu" });
+const collectionAngstrom = createMockCollection({ id: 5, name: "Ångström" });
+const collectionOzgur = createMockCollection({ id: 6, name: "Özgür" });
+
+const mockModels: SearchResult[] = [
+  {
+    id: 0,
+    name: "Model 0",
+    collection: collectionAlpha,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-15T11:59:59.000Z",
+  },
+  {
+    id: 1,
+    name: "Model 1",
+    collection: collectionAlpha,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-15T11:59:30.000Z",
+  },
+  {
+    id: 2,
+    name: "Model 2",
+    collection: collectionAlpha,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-15T11:59:00.000Z",
+  },
+  {
+    id: 3,
+    name: "Model 3",
+    collection: collectionBeta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-15T11:50:00.000Z",
+  },
+  {
+    id: 4,
+    name: "Model 4",
+    collection: collectionBeta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-15T11:00:00.000Z",
+  },
+  {
+    id: 5,
+    name: "Model 5",
+    collection: collectionBeta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-14T22:00:00.000Z",
+  },
+  {
+    id: 6,
+    name: "Model 6",
+    collection: collectionCharlie,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-14T12:00:00.000Z",
+  },
+  {
+    id: 7,
+    name: "Model 7",
+    collection: collectionCharlie,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-12-10T12:00:00.000Z",
+  },
+  {
+    id: 8,
+    name: "Model 8",
+    collection: collectionCharlie,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-11-15T12:00:00.000Z",
+  },
+  {
+    id: 9,
+    name: "Model 9",
+    collection: collectionDelta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2024-02-15T12:00:00.000Z",
+  },
+  {
+    id: 10,
+    name: "Model 10",
+    collection: collectionDelta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2023-12-15T12:00:00.000Z",
+  },
+  {
+    id: 11,
+    name: "Model 11",
+    collection: collectionDelta,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2020-01-01T00:00:00.000Z",
+  },
+  {
+    id: 12,
+    name: "Model 12",
+    collection: collectionZulu,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 13,
+    name: "Model 13",
+    collection: collectionZulu,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 14,
+    name: "Model 14",
+    collection: collectionZulu,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 15,
+    name: "Model 15",
+    collection: collectionAngstrom,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 16,
+    name: "Model 16",
+    collection: collectionAngstrom,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 17,
+    name: "Model 17",
+    collection: collectionAngstrom,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 18,
+    name: "Model 18",
+    collection: collectionOzgur,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 19,
+    name: "Model 19",
+    collection: collectionOzgur,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 20,
+    name: "Model 20",
+    collection: collectionOzgur,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 21,
+    name: "Model 20",
+    collection: defaultRootCollection,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+  {
+    id: 22,
+    name: "Model 21",
+    collection: defaultRootCollection,
+    last_editor_common_name: "Bobby",
+    last_edited_at: "2000-01-01T00:00:00.000Z",
+  },
+].map(model => createMockModelResult(model));
+
+describe("Browse utils", () => {
+  it("include a function that groups models by collection, sorting the collections alphabetically when English is the locale", () => {
+    const groupedModels = groupModels(mockModels, "en-US");
+    expect(groupedModels[0][0].collection.name).toEqual("Alpha");
+    expect(groupedModels[0]).toHaveLength(3);
+    expect(groupedModels[1][0].collection.name).toEqual("Ångström");
+    expect(groupedModels[1]).toHaveLength(3);
+    expect(groupedModels[2][0].collection.name).toEqual("Beta");
+    expect(groupedModels[2]).toHaveLength(3);
+    expect(groupedModels[3][0].collection.name).toEqual("Charlie");
+    expect(groupedModels[3]).toHaveLength(3);
+    expect(groupedModels[4][0].collection.name).toEqual("Delta");
+    expect(groupedModels[4]).toHaveLength(3);
+    expect(groupedModels[5][0].collection.name).toEqual("Our analytics");
+    expect(groupedModels[5]).toHaveLength(2);
+    expect(groupedModels[6][0].collection.name).toEqual("Özgür");
+    expect(groupedModels[6]).toHaveLength(3);
+    expect(groupedModels[7][0].collection.name).toEqual("Zulu");
+    expect(groupedModels[7]).toHaveLength(3);
+  });
+
+  it("include a function that groups models by collection, sorting the collections alphabetically when Swedish is the locale", () => {
+    const groupedModels = groupModels(mockModels, "sv-SV");
+    expect(groupedModels[0][0].collection.name).toEqual("Alpha");
+    expect(groupedModels[0]).toHaveLength(3);
+    expect(groupedModels[1][0].collection.name).toEqual("Beta");
+    expect(groupedModels[1]).toHaveLength(3);
+    expect(groupedModels[2][0].collection.name).toEqual("Charlie");
+    expect(groupedModels[2]).toHaveLength(3);
+    expect(groupedModels[3][0].collection.name).toEqual("Delta");
+    expect(groupedModels[3]).toHaveLength(3);
+    expect(groupedModels[4][0].collection.name).toEqual("Our analytics");
+    expect(groupedModels[4]).toHaveLength(2);
+    expect(groupedModels[5][0].collection.name).toEqual("Zulu");
+    expect(groupedModels[5]).toHaveLength(3);
+    expect(groupedModels[6][0].collection.name).toEqual("Ångström");
+    expect(groupedModels[6]).toHaveLength(3);
+    expect(groupedModels[7][0].collection.name).toEqual("Özgür");
+    expect(groupedModels[7]).toHaveLength(3);
+  });
+
+  const diverseModels = mockModels.map((model, index) => ({
+    ...model,
+    name: index % 2 === 0 ? `red ${index}` : `blue ${index}`,
+    moderated_status: index % 3 === 0 ? `good ${index}` : `bad ${index}`,
+  }));
+  const availableModelFilters: AvailableModelFilters = {
+    onlyRed: {
+      predicate: (model: SearchResult) => model.name.startsWith("red"),
+      activeByDefault: false,
+    },
+    onlyGood: {
+      predicate: (model: SearchResult) =>
+        Boolean(model.moderated_status?.startsWith("good")),
+      activeByDefault: false,
+    },
+    onlyBig: {
+      predicate: (model: SearchResult) =>
+        Boolean(model.description?.startsWith("big")),
+      activeByDefault: true,
+    },
+  };
+
+  it("include a function that filters models, based on the object provided", () => {
+    const onlyRedAndGood: ActualModelFilters = {
+      onlyRed: true,
+      onlyGood: true,
+      onlyBig: false,
+    };
+    const onlyRedAndGoodModels = filterModels(
+      diverseModels,
+      onlyRedAndGood,
+      availableModelFilters,
+    );
+    const everySixthModel = diverseModels.reduce<SearchResult[]>(
+      (acc, model, index) => {
+        return index % 6 === 0 ? [...acc, model] : acc;
+      },
+      [],
+    );
+    // Since every other model is red and every third model is good,
+    // we expect every sixth model to be both red and good
+    expect(onlyRedAndGoodModels).toEqual(everySixthModel);
+  });
+
+  it("filterModels does not filter out models if no filters are active", () => {
+    const noActiveFilters: ActualModelFilters = {
+      onlyRed: false,
+      onlyGood: false,
+      onlyBig: false,
+    };
+    const filteredModels = filterModels(
+      diverseModels,
+      noActiveFilters,
+      availableModelFilters,
+    );
+    expect(filteredModels).toEqual(diverseModels);
+  });
+});
diff --git a/frontend/src/metabase/collections/utils.ts b/frontend/src/metabase/collections/utils.ts
index 82cfcbc2b08..e988a7145d0 100644
--- a/frontend/src/metabase/collections/utils.ts
+++ b/frontend/src/metabase/collections/utils.ts
@@ -195,8 +195,16 @@ export function canonicalCollectionId(
 }
 
 export function isValidCollectionId(
-  collectionId: string | number | null | undefined,
-): boolean {
+  collectionId: unknown,
+): collectionId is CollectionId {
+  if (
+    typeof collectionId !== "string" &&
+    typeof collectionId !== "number" &&
+    collectionId !== null &&
+    collectionId !== undefined
+  ) {
+    return false;
+  }
   const id = canonicalCollectionId(collectionId);
   return id === null || typeof id === "number";
 }
diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js
index 2f704040132..5fc08685b40 100644
--- a/frontend/src/metabase/entities/questions.js
+++ b/frontend/src/metabase/entities/questions.js
@@ -18,6 +18,8 @@ import {
   SOFT_RELOAD_CARD,
 } from "metabase/query_builder/actions";
 
+import { PLUGIN_MODERATION } from "metabase/plugins";
+
 import { canonicalCollectionId } from "metabase/collections/utils";
 import forms from "./questions/forms";
 
@@ -153,9 +155,20 @@ const Questions = createEntity({
 });
 
 export function getIcon(question) {
+  const type = PLUGIN_MODERATION.getQuestionIcon(question);
+
+  if (type) {
+    return {
+      name: type.icon,
+      color: type.color ? color(type.color) : undefined,
+      tooltip: type.tooltip,
+    };
+  }
+
   if (question.dataset || question.model === "dataset") {
     return { name: "model" };
   }
+
   const visualization = require("metabase/visualizations").default.get(
     question.display,
   );
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 241eefe68ba..e8ed7055079 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -14,6 +14,7 @@ import type {
   Bookmark,
   Collection,
   CollectionAuthorityLevelConfig,
+  CollectionEssentials,
   CollectionInstanceAnaltyicsConfig,
   Dashboard,
   Dataset,
@@ -21,6 +22,7 @@ import type {
   GroupPermissions,
   GroupsPermissions,
   Revision,
+  SearchResult,
   User,
   UserListResult,
 } from "metabase-types/api";
@@ -30,6 +32,10 @@ import { UNABLE_TO_CHANGE_ADMIN_PERMISSIONS } from "metabase/admin/permissions/c
 import type { AdminPathKey, State } from "metabase-types/store";
 import type { ADMIN_SETTINGS_SECTIONS } from "metabase/admin/settings/selectors";
 import type { SearchFilterComponent } from "metabase/search/types";
+import type {
+  AvailableModelFilters,
+  ModelFilterControlsProps,
+} from "metabase/browse/utils";
 import type Question from "metabase-lib/Question";
 
 import type Database from "metabase-lib/metadata/Database";
@@ -224,6 +230,7 @@ export const PLUGIN_MODERATION = {
   QuestionModerationButton: PluginPlaceholder,
   ModerationReviewBanner: PluginPlaceholder,
   ModerationStatusIcon: PluginPlaceholder,
+  getQuestionIcon: PluginPlaceholder,
   getStatusIcon: (_moderated_status?: string): string | IconProps | undefined =>
     undefined,
   getModerationTimelineEvents: (
@@ -326,6 +333,13 @@ export const PLUGIN_EMBEDDING = {
 
 export const PLUGIN_CONTENT_VERIFICATION = {
   VerifiedFilter: {} as SearchFilterComponent<"verified">,
+  availableModelFilters: {} as AvailableModelFilters,
+  ModelFilterControls: (() => null) as ComponentType<ModelFilterControlsProps>,
+  sortModelsByVerification: (_a: SearchResult, _b: SearchResult) => 0,
+  sortCollectionsByVerification: (
+    _a: CollectionEssentials,
+    _b: CollectionEssentials,
+  ) => 0,
 };
 
 export const PLUGIN_DASHBOARD_HEADER = {
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 277d1b643a1..4179387603a 100644
--- a/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts
+++ b/frontend/src/metabase/ui/components/icons/Icon/icons/index.ts
@@ -211,6 +211,8 @@ import metric_component from "./metric.svg?component";
 import metric_source from "./metric.svg?source";
 import model_component from "./model.svg?component";
 import model_source from "./model.svg?source";
+import model_with_badge_component from "./model_with_badge.svg?component";
+import model_with_badge_source from "./model_with_badge.svg?source";
 import moon_component from "./moon.svg?component";
 import moon_source from "./moon.svg?source";
 import move_component from "./move.svg?component";
@@ -785,6 +787,10 @@ export const Icons = {
     component: model_component,
     source: model_source,
   },
+  model_with_badge: {
+    component: model_with_badge_component,
+    source: model_with_badge_source,
+  },
   moon: {
     component: moon_component,
     source: moon_source,
diff --git a/frontend/src/metabase/ui/components/icons/Icon/icons/model_with_badge.svg b/frontend/src/metabase/ui/components/icons/Icon/icons/model_with_badge.svg
new file mode 100644
index 00000000000..b0442616538
--- /dev/null
+++ b/frontend/src/metabase/ui/components/icons/Icon/icons/model_with_badge.svg
@@ -0,0 +1,18 @@
+<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.22615 1.42064C6.53555 1.24201 6.91675 1.24201 7.22615 1.42064L12.1723 4.2763C12.4817 4.45494 12.6723 4.78506 12.6723 5.14233V7.6874L12.6254 7.69647L11.9895 7.14473C11.8151 6.99353 11.5561 6.99353 11.3818 7.14473L11.1723 7.32654V6.20506L7.47635 8.4226V12.6989L8.14581 12.3123L8.01861 12.9699C7.97481 13.1965 8.10435 13.4208 8.32248 13.4962L8.80601 13.6632L7.22615 14.5754C6.91675 14.754 6.53555 14.754 6.22615 14.5754L1.28 11.7197C0.970599 11.5411 0.779999 11.2109 0.779999 10.8537V5.14233C0.779999 4.78506 0.970599 4.45494 1.28 4.2763L6.22615 1.42064ZM2.28 10.565V6.20477L5.97634 8.4226V12.6991L2.28 10.565ZM6.72615 2.86402L10.344 4.95277L6.72635 7.12334L3.10857 4.95263L6.72615 2.86402Z" fill="#509EE3"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9945 7.77067C11.8491 7.64447 11.633 7.64447 11.4875 7.77067L10.9569 8.23107L10.2671 8.09767C10.0781 8.06113 9.89094 8.1692 9.82807 8.3512L9.59867 9.0152L8.93467 9.2446C8.75267 9.30747 8.64461 9.4946 8.68114 9.68367L8.81454 10.3734L8.35414 10.9041C8.22794 11.0495 8.22794 11.2657 8.35414 11.4111L8.81454 11.9417L8.68114 12.6315C8.64461 12.8205 8.75267 13.0077 8.93467 13.0706L9.59867 13.2999L9.82807 13.9639C9.89094 14.146 10.0781 14.254 10.2671 14.2175L10.9569 14.0841L11.4875 14.5445C11.633 14.6707 11.8491 14.6707 11.9945 14.5445L12.5252 14.0841L13.2149 14.2175C13.404 14.254 13.5911 14.146 13.654 13.9639L13.8834 13.2999L14.5474 13.0706C14.7295 13.0077 14.8375 12.8205 14.8009 12.6315L14.6675 11.9417L15.1279 11.4111C15.2541 11.2657 15.2541 11.0495 15.1279 10.9041L14.6675 10.3734L14.8009 9.68367C14.8375 9.4946 14.7295 9.30747 14.5474 9.2446L13.8834 9.0152L13.654 8.3512C13.5911 8.1692 13.404 8.06113 13.2149 8.09767L12.5252 8.23107L11.9945 7.77067ZM13.5619 9.85247C13.713 10.0035 13.713 10.2485 13.5619 10.3995L11.4988 12.4627C11.3477 12.6137 11.1028 12.6137 10.9517 12.4627L9.92014 11.4311C9.76907 11.2801 9.76907 11.0351 9.92014 10.8841C10.0712 10.733 10.3161 10.733 10.4672 10.8841L11.2253 11.6421L13.0149 9.85247C13.1659 9.7014 13.4109 9.7014 13.5619 9.85247Z" fill="#509EE3"/>
+<mask id="mask0_1054_4923" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="1" width="16" height="14">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.22615 1.42064C6.53555 1.24201 6.91675 1.24201 7.22615 1.42064L12.1723 4.2763C12.4817 4.45494 12.6723 4.78506 12.6723 5.14233V7.6874L12.6254 7.69647L11.9895 7.14473C11.8151 6.99353 11.5561 6.99353 11.3818 7.14473L11.1723 7.32654V6.20506L7.47635 8.4226V12.6989L8.14581 12.3123L8.01861 12.9699C7.97481 13.1965 8.10435 13.4208 8.32248 13.4962L8.80601 13.6632L7.22615 14.5754C6.91675 14.754 6.53555 14.754 6.22615 14.5754L1.28 11.7197C0.970599 11.5411 0.779999 11.2109 0.779999 10.8537V5.14233C0.779999 4.78506 0.970599 4.45494 1.28 4.2763L6.22615 1.42064ZM2.28 10.565V6.20477L5.97634 8.4226V12.6991L2.28 10.565ZM6.72615 2.86402L10.344 4.95277L6.72635 7.12334L3.10857 4.95263L6.72615 2.86402Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9945 7.77067C11.8491 7.64447 11.633 7.64447 11.4875 7.77067L10.9569 8.23107L10.2671 8.09767C10.0781 8.06113 9.89094 8.1692 9.82807 8.3512L9.59867 9.0152L8.93467 9.2446C8.75267 9.30747 8.64461 9.4946 8.68114 9.68367L8.81454 10.3734L8.35414 10.9041C8.22794 11.0495 8.22794 11.2657 8.35414 11.4111L8.81454 11.9417L8.68114 12.6315C8.64461 12.8205 8.75267 13.0077 8.93467 13.0706L9.59867 13.2999L9.82807 13.9639C9.89094 14.146 10.0781 14.254 10.2671 14.2175L10.9569 14.0841L11.4875 14.5445C11.633 14.6707 11.8491 14.6707 11.9945 14.5445L12.5252 14.0841L13.2149 14.2175C13.404 14.254 13.5911 14.146 13.654 13.9639L13.8834 13.2999L14.5474 13.0706C14.7295 13.0077 14.8375 12.8205 14.8009 12.6315L14.6675 11.9417L15.1279 11.4111C15.2541 11.2657 15.2541 11.0495 15.1279 10.9041L14.6675 10.3734L14.8009 9.68367C14.8375 9.4946 14.7295 9.30747 14.5474 9.2446L13.8834 9.0152L13.654 8.3512C13.5911 8.1692 13.404 8.06113 13.2149 8.09767L12.5252 8.23107L11.9945 7.77067ZM13.5619 9.85247C13.713 10.0035 13.713 10.2485 13.5619 10.3995L11.4988 12.4627C11.3477 12.6137 11.1028 12.6137 10.9517 12.4627L9.92014 11.4311C9.76907 11.2801 9.76907 11.0351 9.92014 10.8841C10.0712 10.733 10.3161 10.733 10.4672 10.8841L11.2253 11.6421L13.0149 9.85247C13.1659 9.7014 13.4109 9.7014 13.5619 9.85247Z" fill="white"/>
+</mask>
+<g mask="url(#mask0_1054_4923)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.22615 1.42064C6.53555 1.24201 6.91675 1.24201 7.22615 1.42064L12.1723 4.2763C12.4817 4.45494 12.6723 4.78506 12.6723 5.14233V7.6874L12.6254 7.69647L11.9895 7.14473C11.8151 6.99353 11.5561 6.99353 11.3818 7.14473L11.1723 7.32654V6.20506L7.47635 8.4226V12.6989L8.14581 12.3123L8.01861 12.9699C7.97481 13.1965 8.10435 13.4208 8.32248 13.4962L8.80601 13.6632L7.22615 14.5754C6.91675 14.754 6.53555 14.754 6.22615 14.5754L1.28 11.7197C0.970599 11.5411 0.779999 11.2109 0.779999 10.8537V5.14233C0.779999 4.78506 0.970599 4.45494 1.28 4.2763L6.22615 1.42064ZM2.28 10.565V6.20477L5.97634 8.4226V12.6991L2.28 10.565ZM6.72615 2.86402L10.344 4.95277L6.72635 7.12334L3.10857 4.95263L6.72615 2.86402Z" stroke="#509EE3" stroke-width="2"/>
+</g>
+<mask id="mask1_1054_4923" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="1" width="16" height="14">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.22615 1.42064C6.53555 1.24201 6.91675 1.24201 7.22615 1.42064L12.1723 4.2763C12.4817 4.45494 12.6723 4.78506 12.6723 5.14233V7.6874L12.6254 7.69647L11.9895 7.14473C11.8151 6.99353 11.5561 6.99353 11.3818 7.14473L11.1723 7.32654V6.20506L7.47635 8.4226V12.6989L8.14581 12.3123L8.01861 12.9699C7.97481 13.1965 8.10435 13.4208 8.32248 13.4962L8.80601 13.6632L7.22615 14.5754C6.91675 14.754 6.53555 14.754 6.22615 14.5754L1.28 11.7197C0.970599 11.5411 0.779999 11.2109 0.779999 10.8537V5.14233C0.779999 4.78506 0.970599 4.45494 1.28 4.2763L6.22615 1.42064ZM2.28 10.565V6.20477L5.97634 8.4226V12.6991L2.28 10.565ZM6.72615 2.86402L10.344 4.95277L6.72635 7.12334L3.10857 4.95263L6.72615 2.86402Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9945 7.77067C11.8491 7.64447 11.633 7.64447 11.4875 7.77067L10.9569 8.23107L10.2671 8.09767C10.0781 8.06113 9.89094 8.1692 9.82807 8.3512L9.59867 9.0152L8.93467 9.2446C8.75267 9.30747 8.64461 9.4946 8.68114 9.68367L8.81454 10.3734L8.35414 10.9041C8.22794 11.0495 8.22794 11.2657 8.35414 11.4111L8.81454 11.9417L8.68114 12.6315C8.64461 12.8205 8.75267 13.0077 8.93467 13.0706L9.59867 13.2999L9.82807 13.9639C9.89094 14.146 10.0781 14.254 10.2671 14.2175L10.9569 14.0841L11.4875 14.5445C11.633 14.6707 11.8491 14.6707 11.9945 14.5445L12.5252 14.0841L13.2149 14.2175C13.404 14.254 13.5911 14.146 13.654 13.9639L13.8834 13.2999L14.5474 13.0706C14.7295 13.0077 14.8375 12.8205 14.8009 12.6315L14.6675 11.9417L15.1279 11.4111C15.2541 11.2657 15.2541 11.0495 15.1279 10.9041L14.6675 10.3734L14.8009 9.68367C14.8375 9.4946 14.7295 9.30747 14.5474 9.2446L13.8834 9.0152L13.654 8.3512C13.5911 8.1692 13.404 8.06113 13.2149 8.09767L12.5252 8.23107L11.9945 7.77067ZM13.5619 9.85247C13.713 10.0035 13.713 10.2485 13.5619 10.3995L11.4988 12.4627C11.3477 12.6137 11.1028 12.6137 10.9517 12.4627L9.92014 11.4311C9.76907 11.2801 9.76907 11.0351 9.92014 10.8841C10.0712 10.733 10.3161 10.733 10.4672 10.8841L11.2253 11.6421L13.0149 9.85247C13.1659 9.7014 13.4109 9.7014 13.5619 9.85247Z" fill="white"/>
+</mask>
+<g mask="url(#mask1_1054_4923)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9945 7.77067C11.8491 7.64447 11.633 7.64447 11.4875 7.77067L10.9569 8.23107L10.2671 8.09767C10.0781 8.06113 9.89094 8.1692 9.82807 8.3512L9.59867 9.0152L8.93467 9.2446C8.75267 9.30747 8.64461 9.4946 8.68114 9.68367L8.81454 10.3734L8.35414 10.9041C8.22794 11.0495 8.22794 11.2657 8.35414 11.4111L8.81454 11.9417L8.68114 12.6315C8.64461 12.8205 8.75267 13.0077 8.93467 13.0706L9.59867 13.2999L9.82807 13.9639C9.89094 14.146 10.0781 14.254 10.2671 14.2175L10.9569 14.0841L11.4875 14.5445C11.633 14.6707 11.8491 14.6707 11.9945 14.5445L12.5252 14.0841L13.2149 14.2175C13.404 14.254 13.5911 14.146 13.654 13.9639L13.8834 13.2999L14.5474 13.0706C14.7295 13.0077 14.8375 12.8205 14.8009 12.6315L14.6675 11.9417L15.1279 11.4111C15.2541 11.2657 15.2541 11.0495 15.1279 10.9041L14.6675 10.3734L14.8009 9.68367C14.8375 9.4946 14.7295 9.30747 14.5474 9.2446L13.8834 9.0152L13.654 8.3512C13.5911 8.1692 13.404 8.06113 13.2149 8.09767L12.5252 8.23107L11.9945 7.77067ZM13.5619 9.85247C13.713 10.0035 13.713 10.2485 13.5619 10.3995L11.4988 12.4627C11.3477 12.6137 11.1028 12.6137 10.9517 12.4627L9.92014 11.4311C9.76907 11.2801 9.76907 11.0351 9.92014 10.8841C10.0712 10.733 10.3161 10.733 10.4672 10.8841L11.2253 11.6421L13.0149 9.85247C13.1659 9.7014 13.4109 9.7014 13.5619 9.85247Z" stroke="#509EE3" stroke-width="2"/>
+</g>
+</svg>
diff --git a/frontend/src/metabase/ui/components/layout/Collapse/index.ts b/frontend/src/metabase/ui/components/layout/Collapse/index.ts
new file mode 100644
index 00000000000..36a8e9719bc
--- /dev/null
+++ b/frontend/src/metabase/ui/components/layout/Collapse/index.ts
@@ -0,0 +1,2 @@
+export { Collapse } from "@mantine/core";
+export type { CollapseProps } from "@mantine/core";
diff --git a/frontend/src/metabase/ui/components/layout/index.ts b/frontend/src/metabase/ui/components/layout/index.ts
index 7548eb41248..714a6489b1e 100644
--- a/frontend/src/metabase/ui/components/layout/index.ts
+++ b/frontend/src/metabase/ui/components/layout/index.ts
@@ -1,4 +1,5 @@
 export * from "./Center";
+export * from "./Collapse";
 export * from "./Flex";
 export * from "./Grid";
 export * from "./Group";
-- 
GitLab