diff --git a/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js index 0a62347fd178e15d26fc19abc7067cfc894d88a2..fe64f112148ed2a8354c3cb5e93e8d40148810ff 100644 --- a/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js +++ b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js @@ -297,7 +297,6 @@ describe("scenarios > admin > databases > sample database", () => { cy.findByText("Browse data").click(); }); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByTestId("database-browser").within(() => { cy.findByText("Sample Database").should("exist"); }); diff --git a/e2e/test/scenarios/onboarding/auth/signin.cy.spec.js b/e2e/test/scenarios/onboarding/auth/signin.cy.spec.js index 3b3917d25456e20ed25d9359738a157df34b57ce..0128a129600fca941777ceeba20470a502714655 100644 --- a/e2e/test/scenarios/onboarding/auth/signin.cy.spec.js +++ b/e2e/test/scenarios/onboarding/auth/signin.cy.spec.js @@ -76,7 +76,6 @@ describe("scenarios > auth > signin", () => { cy.signInAsAdmin(); cy.visit("/"); browse().click(); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByRole("heading", { name: "Sample Database" }).click(); cy.findByRole("heading", { name: "Orders" }).click(); cy.wait("@dataset"); diff --git a/e2e/test/scenarios/onboarding/home/browse.cy.spec.js b/e2e/test/scenarios/onboarding/home/browse.cy.spec.js index cccb42dbb4aa9f75aa5d6278e6e39b198413337d..0d9c274e332891fe568526b3bceee6d2891ad603 100644 --- a/e2e/test/scenarios/onboarding/home/browse.cy.spec.js +++ b/e2e/test/scenarios/onboarding/home/browse.cy.spec.js @@ -1,14 +1,11 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; -import { ORDERS_MODEL_ID } from "e2e/support/cypress_sample_instance_data"; import { - restore, - setTokenFeatures, describeWithSnowplow, - describeWithSnowplowEE, + enableTracking, expectGoodSnowplowEvent, - resetSnowplow, expectNoBadSnowplowEvents, - enableTracking, + resetSnowplow, + restore, } from "e2e/support/helpers"; const { PRODUCTS_ID } = SAMPLE_DATABASE; @@ -20,24 +17,9 @@ describeWithSnowplow("scenarios > browse data", () => { cy.signInAsAdmin(); enableTracking(); }); - - it("can browse to a model", () => { - cy.visit("/"); - cy.findByRole("listitem", { name: "Browse data" }).click(); - cy.location("pathname").should("eq", "/browse/models"); - cy.findByTestId("browse-app").findByText("Browse data"); - cy.findByRole("heading", { name: "Orders Model" }).click(); - cy.url().should("include", `/model/${ORDERS_MODEL_ID}-`); - expectNoBadSnowplowEvents(); - expectGoodSnowplowEvent({ - event: "browse_data_model_clicked", - model_id: ORDERS_MODEL_ID, - }); - }); it("can browse to a table", () => { cy.visit("/"); cy.findByRole("listitem", { name: "Browse data" }).click(); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByRole("heading", { name: "Sample Database" }).click(); cy.findByRole("heading", { name: "Products" }).click(); cy.findByRole("button", { name: "Summarize" }); @@ -51,72 +33,11 @@ describeWithSnowplow("scenarios > browse data", () => { it("can visit 'Learn about our data' page", () => { cy.visit("/"); cy.findByRole("listitem", { name: "Browse data" }).click(); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByRole("link", { name: /Learn about our data/ }).click(); cy.location("pathname").should("eq", "/reference/databases"); cy.go("back"); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByRole("heading", { name: "Sample Database" }).click(); cy.findByRole("heading", { name: "Products" }).click(); cy.findByRole("gridcell", { name: "Rustic Paper Wallet" }); }); - it("the Browse data page shows the last-used tab by default", () => { - cy.visit("/"); - cy.findByRole("listitem", { name: "Browse data" }).click(); - cy.log( - "/browse/ defaults to /browse/models/ because no tabs have been visited yet and there are some models to show", - ); - cy.location("pathname").should("eq", "/browse/models"); - cy.findByRole("tab", { name: "Databases" }).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("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", - ); - }); -}); - -describeWithSnowplowEE("scenarios > browse data (EE)", () => { - beforeEach(() => { - resetSnowplow(); - restore(); - cy.signInAsAdmin(); - enableTracking(); - }); - - 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/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts index e8bfee00bcde48e6ea7af1be28f17d917dc60527..c1a61e5ca0ac6afbb0a78569ee28bf91e0d269b5 100644 --- a/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts +++ b/e2e/test/scenarios/onboarding/setup/setup.cy.spec.ts @@ -288,7 +288,6 @@ describe("scenarios > setup", () => { }); cy.visit("/browse"); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByTestId("database-browser").findByText(dbName); }); diff --git a/e2e/test/scenarios/onboarding/urls.cy.spec.js b/e2e/test/scenarios/onboarding/urls.cy.spec.js index 02765f460927db7dfb0545c52b08ed2c9040ebe6..01e3effd5c9d027323987fae049d4e61dc7af21a 100644 --- a/e2e/test/scenarios/onboarding/urls.cy.spec.js +++ b/e2e/test/scenarios/onboarding/urls.cy.spec.js @@ -25,7 +25,6 @@ describe("URLs", () => { describe("browse databases", () => { it('should slugify database name when opening it from /browse/databases"', () => { cy.visit("/browse/databases"); - cy.findByRole("tab", { name: "Databases" }).click(); cy.findByTextEnsureVisible("Sample Database").click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Sample Database"); diff --git a/e2e/test/scenarios/question/settings.cy.spec.js b/e2e/test/scenarios/question/settings.cy.spec.js index 4e8c092c802b8af7922fb6c40bd72ad03814aef2..8d42d46a7e25951fe1b8da6592eae4e221ccb8d8 100644 --- a/e2e/test/scenarios/question/settings.cy.spec.js +++ b/e2e/test/scenarios/question/settings.cy.spec.js @@ -454,7 +454,6 @@ describe("scenarios > question > settings", () => { // create a new question to see if the "add to a dashboard" modal is still there openNavigationSidebar(); browse().click(); - cy.findByRole("tab", { name: "Databases" }).click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.contains("Sample Database").click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx deleted file mode 100644 index 88e51c28cb81dad0fd95e15d49bdeb0d8e40a1f9..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 214412c455e125f4467694716d90bc69b8535a39..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { t } from "ttag"; - -import type { ModelFilterControlsProps } from "metabase/browse/utils"; -import { Switch, Text } from "metabase/ui"; - -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 1afb1266c45bf17ca9674e7c391e499bed433e04..aa679e97b5fae716dd9d5d46a663a125528d40ee 100644 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts +++ b/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts @@ -1,20 +1,10 @@ import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins"; import { hasPremiumFeature } from "metabase-enterprise/settings"; -import { ModelFilterControls } from "./ModelFilterControls"; import { VerifiedFilter } from "./VerifiedFilter"; -import { - availableModelFilters, - sortCollectionsByVerification, - sortModelsByVerification, -} from "./utils"; if (hasPremiumFeature("content_verification")) { 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 deleted file mode 100644 index 9ee6d5ddeba2b0ec79fa647692e9a6d77f65f0b6..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { AvailableModelFilters } from "metabase/browse/utils"; -import type { CollectionEssentials, SearchResult } from "metabase-types/api"; - -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 deleted file mode 100644 index 1407eae2487be31abd8a3308380e38159f233859..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -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/frontend/src/metabase/browse/analytics.ts b/frontend/src/metabase/browse/analytics.ts index 7569a89615f78fea0328237523352e42a11d78ed..5e5b0f6a7ab3d12c68406880e80d132721dbd3dc 100644 --- a/frontend/src/metabase/browse/analytics.ts +++ b/frontend/src/metabase/browse/analytics.ts @@ -1,11 +1,4 @@ import { trackSchemaEvent } from "metabase/lib/analytics"; -import type { SearchResultId } from "metabase-types/api"; - -export const trackModelClick = (modelId: SearchResultId) => - trackSchemaEvent("browse_data", "1-0-0", { - event: "browse_data_model_clicked", - model_id: modelId, - }); export const trackTableClick = (tableId: number) => trackSchemaEvent("browse_data", "1-0-0", { diff --git a/frontend/src/metabase/browse/components/BrowseApp.styled.tsx b/frontend/src/metabase/browse/components/BrowseApp.styled.tsx index 174e8a7779c278e4b53b263dc610c582b9cbee97..9b20bb84c2057e50d7ea9b8d71842de50a152897 100644 --- a/frontend/src/metabase/browse/components/BrowseApp.styled.tsx +++ b/frontend/src/metabase/browse/components/BrowseApp.styled.tsx @@ -6,39 +6,14 @@ import { breakpointMinMedium, breakpointMinSmall, } from "metabase/styled-components/theme"; -import { Grid, Icon, Tabs } from "metabase/ui"; +import { Grid, Icon } from "metabase/ui"; export const BrowseAppRoot = styled.div` flex: 1; height: 100%; `; -export const BrowseTabs = styled(Tabs)` - display: flex; - flex-flow: column nowrap; - flex: 1; -`; - -export const BrowseTabsList = styled(Tabs.List)` - padding: 0 2.5rem; - background-color: ${color("white")}; - border-bottom-width: 1px; -`; - -export const BrowseTab = styled(Tabs.Tab)` - top: 1px; - margin-bottom: 1px; - border-bottom-width: 3px !important; - padding: 10px 0px; - margin-right: 10px; - &:hover { - color: ${color("brand")}; - background-color: inherit; - border-color: transparent; - } -`; - -export const BrowseTabsPanel = styled(Tabs.Panel)` +export const BrowseMain = styled.div` display: flex; flex-flow: column nowrap; flex: 1; @@ -58,7 +33,6 @@ export const BrowseDataHeader = styled.header` padding: 1rem 2.5rem; padding-bottom: 0.375rem; color: ${color("dark")}; - background-color: ${color("white")}; `; export const BrowseGrid = styled(Grid)` diff --git a/frontend/src/metabase/browse/components/BrowseApp.tsx b/frontend/src/metabase/browse/components/BrowseApp.tsx index 4becf15f2327a4891d2727ed1131f08b7e5cd16f..8b68258fae246c8fdaa10acd20a4ccb810f60b58 100644 --- a/frontend/src/metabase/browse/components/BrowseApp.tsx +++ b/frontend/src/metabase/browse/components/BrowseApp.tsx @@ -1,168 +1,52 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { push } from "react-router-redux"; import { t } from "ttag"; -import _ from "underscore"; -import { - useDatabaseListQuery, - useSearchListQuery, -} from "metabase/common/hooks"; +import { useDatabaseListQuery } from "metabase/common/hooks"; import Link from "metabase/core/components/Link"; -import { useDispatch } from "metabase/lib/redux"; -import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins"; import type { FlexProps } from "metabase/ui"; import { Flex, Text } from "metabase/ui"; -import type { SearchResult } from "metabase-types/api"; - -import type { ActualModelFilters, BrowseTabId } from "../utils"; -import { filterModels, isValidBrowseTab } from "../utils"; import { BrowseAppRoot, BrowseContainer, BrowseDataHeader, - BrowseTab, - BrowseTabs, - BrowseTabsList, - BrowseTabsPanel, + BrowseMain, 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, - children, -}: { - tab: BrowseTabId; - children?: React.ReactNode; -}) => { - const dispatch = useDispatch(); - const modelsResult = useSearchListQuery<SearchResult>({ - query: { - models: ["dataset"], - filter_items_in_personal_collection: "exclude", - }, - }); +export const BrowseApp = ({ children }: { children?: React.ReactNode }) => { const databasesResult = useDatabaseListQuery(); - - useEffect(() => { - localStorage.setItem("defaultBrowseTab", tab); - }, [tab]); - - 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> <BrowseSection> <h2>{t`Browse data`}</h2> + <LearnAboutDataLink /> </BrowseSection> </BrowseDataHeader> - <BrowseTabs - value={tab} - onTabChange={value => { - if (isValidBrowseTab(value)) { - dispatch(push(`/browse/${value}`)); - } - }} - > - <BrowseTabsList> - <BrowseSection> - <BrowseTab key={"models"} value={"models"}> - {t`Models`} - </BrowseTab> - <BrowseTab key={"databases"} value={"databases"}> - {t`Databases`} - </BrowseTab> - {tab === "models" && ( - <PLUGIN_CONTENT_VERIFICATION.ModelFilterControls - actualModelFilters={actualModelFilters} - handleModelFilterChange={handleModelFilterChange} - /> - )} - {tab === "databases" && <LearnAboutDataLink />} - </BrowseSection> - </BrowseTabsList> - <BrowseTabsPanel key={tab} value={tab}> - <BrowseSection direction="column"> - <BrowseTabContent - tab={tab} - modelsResult={filteredModelsResult} - databasesResult={databasesResult} - > - {children} - </BrowseTabContent> - </BrowseSection> - </BrowseTabsPanel> - </BrowseTabs> + <BrowseMain> + <BrowseSection direction="column"> + <BrowseContent databasesResult={databasesResult}> + {children} + </BrowseContent> + </BrowseSection> + </BrowseMain> </BrowseContainer> </BrowseAppRoot> ); }; -const BrowseTabContent = ({ - tab, +const BrowseContent = ({ children, - modelsResult, databasesResult, }: { - tab: BrowseTabId; children?: React.ReactNode; - modelsResult: ReturnType<typeof useSearchListQuery<SearchResult>>; databasesResult: ReturnType<typeof useDatabaseListQuery>; }) => { if (children) { return <>{children}</>; - } - if (tab === "models") { - return <BrowseModels modelsResult={modelsResult} />; } else { return <BrowseDatabases databasesResult={databasesResult} />; } diff --git a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx b/frontend/src/metabase/browse/components/BrowseModels.styled.tsx deleted file mode 100644 index 3996ac847bb8163821f09334425160a60a87ea5e..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import styled from "@emotion/styled"; -import type { HTMLAttributes } from "react"; - -import Card from "metabase/components/Card"; -import IconButtonWrapper from "metabase/components/IconButtonWrapper"; -import { Ellipsified } from "metabase/core/components/Ellipsified"; -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; - padding-bottom: 1rem; - - height: 9rem; - display: flex; - flex-flow: column nowrap; - justify-content: flex-start; - align-items: flex-start; - - border: 1px solid ${color("border")}; - - box-shadow: none; - &:hover { - h1 { - color: ${color("brand")}; - } - } - transition: box-shadow 0.15s; - - h1 { - transition: color 0.15s; - } -`; - -export const MultilineEllipsified = styled(Ellipsified)` - white-space: pre-line; - overflow: hidden; - text-overflow: ellipsis; - - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - - // Without the following rule, the useIsTruncated hook, - // which Ellipsified calls, might think that this element - // is truncated when it is not - padding-bottom: 1px; -`; - -export const ModelGrid = styled(BrowseGrid)``; - -export const CollectionHeaderContainer = styled.button` - grid-column: 1 / -1; - 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)` - display: flex; - align-items: center; - &:hover { - color: ${color("brand")}; - } -`; - -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; - 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(FixedSizeIcon)` - color: ${color("text-dark")}; - margin-right: 0.5rem; -`; - -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 deleted file mode 100644 index b0a6cd0cec498e991e37c4aaf35a18aa7615d97e..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/BrowseModels.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useState } from "react"; -import { t } from "ttag"; - -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 { 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 { CenteredEmptyState } from "./BrowseApp.styled"; -import { ModelGrid } from "./BrowseModels.styled"; -import { ModelExplanationBanner } from "./ModelExplanationBanner"; -import { ModelGroup } from "./ModelGroup"; - -export const BrowseModels = ({ - modelsResult, -}: { - modelsResult: ReturnType<typeof useSearchListQuery<SearchResult>>; -}) => { - const { data: models = [], error, isLoading } = modelsResult; - const locale = useSelector(getLocale); - const localeCode: string | undefined = locale?.code; - const [collectionViewPreferences, setCollectionViewPreferences] = useState( - getCollectionViewPreferences, - ); - - if (error || isLoading) { - return ( - <LoadingAndErrorWrapper - error={error} - loading={isLoading} - style={{ display: "flex", flex: 1 }} - /> - ); - } - - 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 /> - <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> - </> - ); - } - - return ( - <CenteredEmptyState - title={<Box mb=".5rem">{t`No models here yet`}</Box>} - message={ - <Box maw="24rem">{t`Models help curate data to make it easier to find answers to questions all in one place.`}</Box> - } - illustrationElement={ - <Box mb=".5rem"> - <img src={NoResults} /> - </Box> - } - /> - ); -}; diff --git a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx b/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx deleted file mode 100644 index 7d0b817d050e4dfe3a1640fb379c604417cafd9f..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx +++ /dev/null @@ -1,365 +0,0 @@ -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 { - createMockCollection, - createMockModelResult, -} from "metabase-types/api/mocks"; -import { createMockSetupState } from "metabase-types/store/mocks"; - -import { BROWSE_MODELS_LOCALSTORAGE_KEY } from "../constants"; - -import { BrowseModels } from "./BrowseModels"; - -const renderBrowseModels = (modelCount: number) => { - const models = mockModels.slice(0, modelCount); - return renderWithProviders( - <BrowseModels - modelsResult={{ data: models, isLoading: false, error: false }} - />, - { - storeInitialState: { - setup: createMockSetupState({ - locale: { name: "English", code: "en" }, - }), - }, - }, - ); -}; - -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[] = [ - { - 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 21", - collection: defaultRootCollection, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 22, - 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(() => { - 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 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); - 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(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("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); - }); - - 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); - }); - - 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("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(); - }); - - 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/BrowseRedirect.tsx b/frontend/src/metabase/browse/components/BrowseRedirect.tsx deleted file mode 100644 index 8d9a4045d911ba43100a41b8363095821254afc8..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/BrowseRedirect.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect } from "react"; -import { replace } from "react-router-redux"; - -import { useSearchListQuery } from "metabase/common/hooks"; -import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; -import { useDispatch } from "metabase/lib/redux"; -import type { SearchResult } from "metabase-types/api"; - -export const BrowseRedirect = () => { - const defaultTab = localStorage.getItem("defaultBrowseTab"); - - const shouldQueryModels = !defaultTab; - - const { - data: models, - error, - isLoading, - } = useSearchListQuery<SearchResult>({ - query: { - models: ["dataset"], - filter_items_in_personal_collection: "exclude", - }, - enabled: shouldQueryModels, - }); - - const dispatch = useDispatch(); - - useEffect(() => { - switch (defaultTab) { - case "models": - dispatch(replace("/browse/models")); - break; - case "databases": - dispatch(replace("/browse/databases")); - break; - default: - if (models !== undefined) { - if (models.length > 0) { - dispatch(replace("/browse/models")); - } else { - dispatch(replace("/browse/databases")); - } - } - if (!error && !isLoading) { - dispatch(replace("/browse/models")); - } - } - }, [models, defaultTab, dispatch, error, isLoading]); - - if (error || isLoading) { - return ( - <LoadingAndErrorWrapper - error={error} - loading={isLoading} - style={{ display: "flex", flex: 1 }} - /> - ); - } - - return null; -}; diff --git a/frontend/src/metabase/browse/components/BrowseRedirect.unit.spec.tsx b/frontend/src/metabase/browse/components/BrowseRedirect.unit.spec.tsx deleted file mode 100644 index 7ee3e6e2adbe959770543e12020eb6c6a2177707..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/BrowseRedirect.unit.spec.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { replace } from "react-router-redux"; - -import { setupSearchEndpoints } from "__support__/server-mocks"; -import { renderWithProviders, waitFor } from "__support__/ui"; -import type { SearchResult } from "metabase-types/api"; -import { createMockModelResult } from "metabase-types/api/mocks"; - -import { BrowseRedirect } from "./BrowseRedirect"; - -const mockModels: SearchResult[] = [ - { - id: 0, - name: "Model 0", - collection: { id: 0, name: "Alpha" }, - }, - { - id: 1, - name: "Model 1", - collection: { id: 0, name: "Alpha" }, - }, -].map(model => createMockModelResult(model)); - -const setup = ({ - models, - defaultTab = null, -}: { - models: SearchResult[]; - defaultTab: string | null; -}) => { - setupSearchEndpoints(models); - if (defaultTab === null) { - localStorage.removeItem("defaultBrowseTab"); - } else { - localStorage.setItem("defaultBrowseTab", defaultTab); - } - return renderWithProviders(<BrowseRedirect />); -}; - -describe("BrowseRedirect", () => { - it("redirects to /browse/databases if there are no models and no saved setting", async () => { - const { store } = setup({ models: [], defaultTab: null }); - const mockDispatch = jest.spyOn(store, "dispatch"); - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith(replace("/browse/databases")); - }); - }); - it("redirects to /browse/models if there are some models but no saved setting", async () => { - const { store } = setup({ models: mockModels, defaultTab: null }); - const mockDispatch = jest.spyOn(store, "dispatch"); - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith(replace("/browse/models")); - }); - }); - - it("redirects to /browse/models if the user's defaultBrowseTab setting is 'models'", async () => { - const { store, rerender } = setup({ - models: [], - defaultTab: "models", - }); - const mockDispatch = jest.spyOn(store, "dispatch"); - rerender(<BrowseRedirect />); - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith(replace("/browse/models")); - }); - }); - it("redirects to /browse/databases if the user's defaultBrowseBab setting is 'databases'", async () => { - const { store, rerender } = setup({ - models: mockModels, - defaultTab: "databases", - }); - const mockDispatch = jest.spyOn(store, "dispatch"); - rerender(<BrowseRedirect />); - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith(replace("/browse/databases")); - }); - }); - it("redirects to /browse/models if the user has an invalid defaultBrowseTab setting, and some models exist", async () => { - const { store, rerender } = setup({ - models: mockModels, - defaultTab: "this is an invalid value", - }); - const mockDispatch = jest.spyOn(store, "dispatch"); - rerender(<BrowseRedirect />); - await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledWith(replace("/browse/models")); - }); - }); -}); diff --git a/frontend/src/metabase/browse/components/LastEdited.tsx b/frontend/src/metabase/browse/components/LastEdited.tsx deleted file mode 100644 index 8873d7feea3c63f4e4cb3a0b6e4e515fa8a75bff..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/LastEdited.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import dayjs from "dayjs"; -import relativeTime from "dayjs/plugin/relativeTime"; -import updateLocale from "dayjs/plugin/updateLocale"; -import { c, t } from "ttag"; -import _ from "underscore"; - -import { formatDateTimeWithUnit } from "metabase/lib/formatting"; -import { Text, Tooltip } from "metabase/ui"; - -dayjs.extend(updateLocale); -dayjs.extend(relativeTime); - -const timeFormattingRules: Record<string, unknown> = { - m: t`${1}min`, - mm: t`${"%d"}min`, - h: t`${1}h`, - hh: t`${"%d"}h`, - d: t`${1}d`, - dd: t`${"%d"}d`, - M: t`${1}mo`, - MM: t`${"%d"}mo`, - y: t`${1}yr`, - yy: t`${"%d"}yr`, - // Display any number of seconds as "1min" - s: () => t`${1}min`, - ss: () => t`${1}min`, - // Don't use "ago" - past: "%s", - // For the edge case where a model's last-edit date is somehow in the future - future: t`${"%s"} from now`, -}; - -const getTimePassedSince = (timestamp: string) => { - const date = dayjs(timestamp); - if (timestamp && date.isValid()) { - const locale = dayjs.locale(); - const cachedRules = dayjs.Ls[locale].relativeTime; - dayjs.updateLocale(locale, { relativeTime: timeFormattingRules }); - const timePassed = date.fromNow(); - dayjs.updateLocale(locale, { relativeTime: cachedRules }); - return timePassed; - } else { - return t`(invalid date)`; - } -}; - -export const LastEdited = ({ - editorFullName, - timestamp, -}: { - editorFullName: string | null; - timestamp: string; -}) => { - const timePassed = getTimePassedSince(timestamp); - const timeLabel = timestamp ? timePassed : ""; - const formattedDate = formatDateTimeWithUnit(timestamp, "day", {}); - const time = ( - <time key="time" dateTime={timestamp}> - {formattedDate} - </time> - ); - - const tooltipLabel = c( - "{0} is the full name (or if this is unavailable, the email address) of the last person who edited a model. {1} is a date", - ).jt`Last edited by ${editorFullName}${(<br key="br" />)}${time}`; - - return ( - <Tooltip label={tooltipLabel} disabled={!timeLabel}> - <Text role="note" size="small"> - {editorFullName} - {editorFullName && timePassed && ( - <Text span px=".33rem" color="text-light"> - • - </Text> - )} - {timePassed} - </Text> - </Tooltip> - ); -}; diff --git a/frontend/src/metabase/browse/components/ModelExplanationBanner.tsx b/frontend/src/metabase/browse/components/ModelExplanationBanner.tsx deleted file mode 100644 index d7aa67537a30630692ca0d19e2e0d7e362117178..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/ModelExplanationBanner.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useState } from "react"; -import { t } from "ttag"; - -import { useDispatch, useSelector } from "metabase/lib/redux"; -import { updateUserSetting } from "metabase/redux/settings"; -import { getSetting } from "metabase/selectors/settings"; -import { Flex, Paper, Icon, Text } from "metabase/ui"; - -import { BannerCloseButton, BannerModelIcon } from "./BrowseModels.styled"; - -export function ModelExplanationBanner() { - const hasDismissedBanner = useSelector(state => - getSetting(state, "dismissed-browse-models-banner"), - ); - const dispatch = useDispatch(); - - const [shouldShowBanner, setShouldShowBanner] = useState(!hasDismissedBanner); - - const dismissBanner = () => { - setShouldShowBanner(false); - dispatch( - updateUserSetting({ - key: "dismissed-browse-models-banner", - value: true, - }), - ); - }; - - if (!shouldShowBanner) { - return null; - } - - return ( - <Paper - mt="1rem" - mb="-0.5rem" - p="1rem" - color="text-dark" - bg="brand-lighter" - shadow="0" - radius="0.25rem" - role="complementary" - w="100%" - > - <Flex> - <BannerModelIcon name="model" /> - <Text size="md" lh="1rem" mr="1rem"> - {t`Models help curate data to make it easier to find answers to questions all in one place.`} - </Text> - <BannerCloseButton onClick={dismissBanner}> - <Icon name="close" /> - </BannerCloseButton> - </Flex> - </Paper> - ); -} diff --git a/frontend/src/metabase/browse/components/ModelGroup.tsx b/frontend/src/metabase/browse/components/ModelGroup.tsx deleted file mode 100644 index b17238d030413d34c5f4a11a99600deccce5019b..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/components/ModelGroup.tsx +++ /dev/null @@ -1,211 +0,0 @@ -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 { trackModelClick } from "../analytics"; -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>)} - onClick={() => trackModelClick(model.id)} - > - <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.ts b/frontend/src/metabase/browse/constants.ts index 12b4baf632d438b0bff45accc17f29746566b716..4c375c7e51d7ec01ecdef2a08e485cf15a756dfe 100644 --- a/frontend/src/metabase/browse/constants.ts +++ b/frontend/src/metabase/browse/constants.ts @@ -1,3 +1 @@ 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 deleted file mode 100644 index 7ce5132a7b227fe36903847e501d08ecea54dca2..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/utils.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { t } from "ttag"; -import _ from "underscore"; - -import { - canonicalCollectionId, - coerceCollectionId, - isInstanceAnalyticsCollection, - isRootCollection, - isValidCollectionId, -} from "metabase/collections/utils"; -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)) { - return t`Our analytics`; - } - return collection?.name || t`Untitled collection`; -}; - -/** The root collection's id might be null or 'root' in different contexts. - * Use 'root' instead of null, for the sake of sorting */ -export const getCollectionIdForSorting = (collection: CollectionEssentials) => { - return coerceCollectionId(canonicalCollectionId(collection.id)); -}; - -/** Group models by collection */ -export const groupModels = ( - models: SearchResult[], - locale: string | undefined, -) => { - const groupedModels = _.groupBy(models, model => - getCollectionIdForSorting(model.collection), - ); - const groupsOfModels: SearchResult[][] = Object.values(groupedModels); - const sortGroupsByCollection = (a: SearchResult[], b: SearchResult[]) => { - const collection1 = a[0].collection; - const collection2 = b[0].collection; - - // 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(sortGroupsByCollection); - return groupsOfModels; -}; - -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 deleted file mode 100644 index 8309d17ea50de9799352c71da34abe9a40bf4da5..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/browse/utils.unit.spec.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup"; -import type { SearchResult } from "metabase-types/api"; -import { - createMockCollection, - createMockModelResult, -} from "metabase-types/api/mocks"; - -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/plugins/index.ts b/frontend/src/metabase/plugins/index.ts index 2a05e86ec4b5a656f0340ad2cff449c0c7fd1c77..ed639bf7e20c1adac72b9fcc964c8d69656d80fa 100644 --- a/frontend/src/metabase/plugins/index.ts +++ b/frontend/src/metabase/plugins/index.ts @@ -8,10 +8,6 @@ import type { PermissionSubject, } from "metabase/admin/permissions/types"; import type { ADMIN_SETTINGS_SECTIONS } from "metabase/admin/settings/selectors"; -import type { - AvailableModelFilters, - ModelFilterControlsProps, -} from "metabase/browse/utils"; import PluginPlaceholder from "metabase/plugins/components/PluginPlaceholder"; import type { SearchFilterComponent } from "metabase/search/types"; import type { IconName, IconProps } from "metabase/ui"; @@ -21,7 +17,6 @@ import type { Bookmark, Collection, CollectionAuthorityLevelConfig, - CollectionEssentials, CollectionInstanceAnaltyicsConfig, Dashboard, Dataset, @@ -29,7 +24,6 @@ import type { GroupPermissions, GroupsPermissions, Revision, - SearchResult, User, UserListResult, } from "metabase-types/api"; @@ -340,13 +334,6 @@ 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/routes.jsx b/frontend/src/metabase/routes.jsx index 643d4a8874fd59e660716c5646acb5b779ea2317..b3fec9e8f69ac0bd2a6b4c2bd2f46f2d44161f31 100644 --- a/frontend/src/metabase/routes.jsx +++ b/frontend/src/metabase/routes.jsx @@ -60,7 +60,6 @@ import SearchApp from "metabase/search/containers/SearchApp"; import { Setup } from "metabase/setup/components/Setup"; import getCollectionTimelineRoutes from "metabase/timelines/collections/routes"; -import { BrowseRedirect } from "./browse/components/BrowseRedirect"; import { CanAccessMetabot, CanAccessSettings, @@ -195,37 +194,27 @@ export const getRoutes = store => { <Route path="metabot" component={QueryBuilder} /> </Route> - <Route path="browse"> - <IndexRoute component={BrowseRedirect} /> - <Route path="models" component={() => <BrowseApp tab="models" />} /> - <Route - path="databases" - component={() => <BrowseApp tab="databases" />} - /> + <Route path="browse" component={BrowseApp}> <Route path="databases/:slug" - component={({ params }) => ( - <BrowseApp tab="databases"> - <SchemaBrowser params={params} /> - </BrowseApp> - )} + component={({ params }) => <SchemaBrowser params={params} />} /> <Route path="databases/:dbId/schema/:schemaName" - component={({ params }) => ( - <BrowseApp tab="databases"> - <TableBrowser params={params} /> - </BrowseApp> - )} - /> - - {/* These two Redirects support legacy paths in v48 and earlier */} - <Redirect from=":dbId-:slug" to="databases/:dbId-:slug" /> - <Redirect - from=":dbId/schema/:schemaName" - to="databases/:dbId/schema/:schemaName" + component={({ params }) => <TableBrowser params={params} />} /> </Route> + {/* These Redirects support legacy paths in v49 and earlier */} + <Redirect from="/browse/models" to="/browse" /> + <Redirect from="/browse/databases" to="/browse" /> + <Redirect + from="/browse/:dbId-:slug" + to="/browse/databases/:dbId-:slug" + /> + <Redirect + from="/browse/:dbId/schema/:schemaName" + to="/browse/databases/:dbId/schema/:schemaName" + /> {/* INDIVIDUAL DASHBOARDS */}