From 9f4591dbb345a96697e953e3c830d433a5c4401e Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Thu, 7 Apr 2022 22:49:15 +0400 Subject: [PATCH] Cleanup new homepage (#21448) --- frontend/src/metabase-types/api/activity.ts | 4 +- .../src/metabase-types/api/mocks/activity.ts | 14 +-- frontend/src/metabase-types/api/mocks/user.ts | 1 + frontend/src/metabase-types/api/user.ts | 1 + frontend/src/metabase/entities/index.js | 4 +- .../{popular-views.js => popular-items.js} | 14 +-- .../{recent-views.js => recent-items.js} | 12 +- .../HomeCaption/HomeCaption.styled.tsx | 20 +++ .../components/HomeCaption/HomeCaption.tsx | 13 ++ .../HomeCaption/HomeCaption.unit.spec.tsx | 11 ++ .../homepage/components/HomeCaption/index.ts | 1 + .../components/HomeCard/HomeCard.styled.tsx | 9 +- .../HomeCard/HomeCard.unit.spec.tsx | 11 ++ .../components/HomeContent/HomeContent.tsx | 18 ++- .../HomeContent/HomeContent.unit.spec.tsx | 115 ++++++++++++++++++ .../HomeGreeting/HomeGreeting.styled.tsx | 9 ++ .../HomeGreeting/HomeGreeting.unit.spec.tsx | 36 ++++++ .../HomeHelpCard/HomeHelpCard.styled.tsx | 5 + .../HomeHelpCard/HomeHelpCard.unit.spec.tsx | 11 ++ .../HomeLayout/HomeLayout.styled.tsx | 29 ++++- .../HomeLayout/HomeLayout.unit.spec.tsx | 21 ++++ .../HomeModelCard/HomeModelCard.unit.spec.tsx | 24 ++++ .../HomePage/HomePage.unit.spec.tsx | 42 +++++++ .../HomePopularSection.styled.tsx | 12 -- .../HomePopularSection/HomePopularSection.tsx | 23 ++-- .../HomePopularSection.unit.spec.tsx | 61 ++++++++++ .../HomeRecentSection.styled.tsx | 9 -- .../HomeRecentSection/HomeRecentSection.tsx | 15 +-- .../HomeRecentSection.unit.spec.tsx | 31 +++++ .../HomeXrayCard/HomeXrayCard.unit.spec.tsx | 24 ++++ .../HomeXraySection.styled.tsx | 31 +++-- .../HomeXraySection/HomeXraySection.tsx | 113 +++++++++++++---- .../HomeXraySection.unit.spec.tsx | 66 ++++++++++ .../containers/HomeContent/HomeContent.tsx | 4 +- .../HomePopularSection/HomePopularSection.tsx | 4 +- .../HomeRecentSection/HomeRecentSection.tsx | 4 +- .../HomeXraySection/HomeXraySection.tsx | 2 +- .../metabase/nav/components/RecentsList.jsx | 4 +- frontend/src/metabase/schema.js | 4 +- .../onboarding/home/homepage.cy.spec.js | 97 ++++++++++++++- .../test/snapshot-creators/default.cy.snap.js | 1 + 41 files changed, 807 insertions(+), 123 deletions(-) rename frontend/src/metabase/entities/{popular-views.js => popular-items.js} (73%) rename frontend/src/metabase/entities/{recent-views.js => recent-items.js} (78%) create mode 100644 frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.styled.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeCaption/index.ts create mode 100644 frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeModelCard/HomeModelCard.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomePage/HomePage.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeXrayCard/HomeXrayCard.unit.spec.tsx create mode 100644 frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.unit.spec.tsx diff --git a/frontend/src/metabase-types/api/activity.ts b/frontend/src/metabase-types/api/activity.ts index 36aea637e19..c84572be246 100644 --- a/frontend/src/metabase-types/api/activity.ts +++ b/frontend/src/metabase-types/api/activity.ts @@ -4,12 +4,12 @@ export interface ModelObject { name: string; } -export interface RecentView { +export interface RecentItem { model: ModelType; model_object: ModelObject; } -export interface PopularView { +export interface PopularItem { model: ModelType; model_object: ModelObject; } diff --git a/frontend/src/metabase-types/api/mocks/activity.ts b/frontend/src/metabase-types/api/mocks/activity.ts index 22a0d6d0321..c54c2ceee91 100644 --- a/frontend/src/metabase-types/api/mocks/activity.ts +++ b/frontend/src/metabase-types/api/mocks/activity.ts @@ -1,4 +1,4 @@ -import { ModelObject, PopularView, RecentView } from "metabase-types/api"; +import { ModelObject, PopularItem, RecentItem } from "metabase-types/api"; export const createMockModelObject = ( opts?: Partial<ModelObject>, @@ -7,17 +7,17 @@ export const createMockModelObject = ( ...opts, }); -export const createMockRecentView = ( - opts?: Partial<RecentView>, -): RecentView => ({ +export const createMockRecentItem = ( + opts?: Partial<RecentItem>, +): RecentItem => ({ model: "table", model_object: createMockModelObject(), ...opts, }); -export const createMockPopularView = ( - opts?: Partial<PopularView>, -): PopularView => ({ +export const createMockPopularItem = ( + opts?: Partial<PopularItem>, +): PopularItem => ({ model: "table", model_object: createMockModelObject(), ...opts, diff --git a/frontend/src/metabase-types/api/mocks/user.ts b/frontend/src/metabase-types/api/mocks/user.ts index 6f024d06629..d77823344cb 100644 --- a/frontend/src/metabase-types/api/mocks/user.ts +++ b/frontend/src/metabase-types/api/mocks/user.ts @@ -15,6 +15,7 @@ export const createMockUser = (opts?: Partial<User>): User => ({ has_question_and_dashboard: false, personal_collection_id: 1, date_joined: new Date().toISOString(), + first_login: new Date().toISOString(), last_login: new Date().toISOString(), ...opts, }); diff --git a/frontend/src/metabase-types/api/user.ts b/frontend/src/metabase-types/api/user.ts index 718feb28649..275bf70f4f7 100644 --- a/frontend/src/metabase-types/api/user.ts +++ b/frontend/src/metabase-types/api/user.ts @@ -12,6 +12,7 @@ export interface User { has_invited_second_user: boolean; has_question_and_dashboard: boolean; date_joined: string; + first_login: string; last_login: string; personal_collection_id: number; } diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js index 96383657e90..2061c15cf1e 100644 --- a/frontend/src/metabase/entities/index.js +++ b/frontend/src/metabase/entities/index.js @@ -24,6 +24,6 @@ export { default as users } from "./users"; export { default as groups } from "./groups"; export { default as search } from "./search"; -export { default as popularViews } from "./popular-views"; -export { default as recentViews } from "./recent-views"; +export { default as recentItems } from "./recent-items"; +export { default as popularItems } from "./popular-items"; export { default as snippets } from "./snippets"; diff --git a/frontend/src/metabase/entities/popular-views.js b/frontend/src/metabase/entities/popular-items.js similarity index 73% rename from frontend/src/metabase/entities/popular-views.js rename to frontend/src/metabase/entities/popular-items.js index 6102930ffcb..a6b5805c69a 100644 --- a/frontend/src/metabase/entities/popular-views.js +++ b/frontend/src/metabase/entities/popular-items.js @@ -1,6 +1,6 @@ import { createEntity } from "metabase/lib/entities"; import { entityTypeForObject } from "metabase/lib/schema"; -import { PopularViewsSchema } from "metabase/schema"; +import { PopularItemSchema } from "metabase/schema"; export const getEntity = item => { const entities = require("metabase/entities"); @@ -16,11 +16,11 @@ export const getIcon = item => { return entity.objectSelectors.getIcon(item.model_object); }; -const PopularViews = createEntity({ - name: "popularViews", - nameOne: "popularView", - path: "/api/activity/popular_views", - schema: PopularViewsSchema, +const PopularItems = createEntity({ + name: "popularItems", + nameOne: "popularItem", + path: "/api/activity/popular_items", + schema: PopularItemSchema, wrapEntity(item, dispatch = null) { const entity = getEntity(item); @@ -33,4 +33,4 @@ const PopularViews = createEntity({ }, }); -export default PopularViews; +export default PopularItems; diff --git a/frontend/src/metabase/entities/recent-views.js b/frontend/src/metabase/entities/recent-items.js similarity index 78% rename from frontend/src/metabase/entities/recent-views.js rename to frontend/src/metabase/entities/recent-items.js index 594bbfb0cdc..0687a5089a2 100644 --- a/frontend/src/metabase/entities/recent-views.js +++ b/frontend/src/metabase/entities/recent-items.js @@ -1,6 +1,6 @@ import { createEntity } from "metabase/lib/entities"; import { entityTypeForObject } from "metabase/lib/schema"; -import { RecentViewsSchema } from "metabase/schema"; +import { RecentItemSchema } from "metabase/schema"; export const getEntity = item => { const entities = require("metabase/entities"); @@ -16,11 +16,11 @@ export const getIcon = item => { return entity.objectSelectors.getIcon(item.model_object); }; -const RecentViews = createEntity({ - name: "recentViews", - nameOne: "recentView", +const RecentItems = createEntity({ + name: "recentItems", + nameOne: "recentItem", path: "/api/activity/recent_views", - schema: RecentViewsSchema, + schema: RecentItemSchema, wrapEntity(item, dispatch = null) { const entity = getEntity(item); @@ -33,4 +33,4 @@ const RecentViews = createEntity({ }, }); -export default RecentViews; +export default RecentItems; diff --git a/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.styled.tsx new file mode 100644 index 00000000000..7c4cfcb6100 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.styled.tsx @@ -0,0 +1,20 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; +import { breakpointMinExtraLarge } from "metabase/styled-components/theme"; + +export interface CaptionProps { + primary?: boolean; +} + +export const CaptionRoot = styled.div<CaptionProps>` + display: flex; + align-items: center; + color: ${props => + props.primary ? color("text-dark") : color("text-medium")}; + font-weight: bold; + margin-bottom: 1.5rem; + + ${breakpointMinExtraLarge} { + margin-bottom: 2rem; + } +`; diff --git a/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.tsx b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.tsx new file mode 100644 index 00000000000..ca922b65503 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from "react"; +import { CaptionRoot } from "./HomeCaption.styled"; + +export interface HomeCaptionProps { + primary?: boolean; + children?: ReactNode; +} + +const HomeCaption = ({ primary, children }: HomeCaptionProps): JSX.Element => { + return <CaptionRoot primary={primary}>{children}</CaptionRoot>; +}; + +export default HomeCaption; diff --git a/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.unit.spec.tsx new file mode 100644 index 00000000000..dbe6ccad15f --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeCaption/HomeCaption.unit.spec.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeCaption from "./HomeCaption"; + +describe("HomeCaption", () => { + it("should render correctly", () => { + render(<HomeCaption>Title</HomeCaption>); + + expect(screen.getByText("Title")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeCaption/index.ts b/frontend/src/metabase/home/homepage/components/HomeCaption/index.ts new file mode 100644 index 00000000000..2c0370af339 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeCaption/index.ts @@ -0,0 +1 @@ +export { default } from "./HomeCaption"; diff --git a/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.styled.tsx index b9a6e4c1c14..ee0b1b61b3e 100644 --- a/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.styled.tsx @@ -1,7 +1,10 @@ import styled from "@emotion/styled"; import { alpha, color } from "metabase/lib/colors"; import Link from "metabase/core/components/Link"; -import { breakpointMinSmall } from "metabase/styled-components/theme"; +import { + breakpointMinLarge, + breakpointMinSmall, +} from "metabase/styled-components/theme"; export const CardRoot = styled(Link)` display: flex; @@ -17,6 +20,10 @@ export const CardRoot = styled(Link)` max-width: 50%; } + ${breakpointMinLarge} { + padding: 1.5rem; + } + &:hover { box-shadow: 0 10px 22px ${alpha("shadow", 0.09)}; } diff --git a/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.unit.spec.tsx new file mode 100644 index 00000000000..a4bfe24e179 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeCard/HomeCard.unit.spec.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeCard from "./HomeCard"; + +describe("HomeCard", () => { + it("should render correctly", () => { + render(<HomeCard>A look at table</HomeCard>); + + expect(screen.getByText("A look at table")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.tsx b/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.tsx index 733f1283720..41139c5e97c 100644 --- a/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.tsx @@ -2,7 +2,7 @@ import React from "react"; import moment from "moment"; import { parseTimestamp } from "metabase/lib/time"; import { isSyncCompleted } from "metabase/lib/syncing"; -import { Database, RecentView, User } from "metabase-types/api"; +import { Database, RecentItem, User } from "metabase-types/api"; import HomePopularSection from "../../containers/HomePopularSection"; import HomeRecentSection from "../../containers/HomeRecentSection"; import HomeXraySection from "../../containers/HomeXraySection"; @@ -10,7 +10,7 @@ import HomeXraySection from "../../containers/HomeXraySection"; export interface HomeContentProps { user: User; databases: Database[]; - recentViews: RecentView[]; + recentItems: RecentItem[]; } const HomeContent = (props: HomeContentProps): JSX.Element | null => { @@ -29,16 +29,16 @@ const HomeContent = (props: HomeContentProps): JSX.Element | null => { return null; }; -const isPopularSection = ({ user, recentViews }: HomeContentProps) => { +const isPopularSection = ({ user, recentItems }: HomeContentProps) => { return ( !user.is_installer && user.has_question_and_dashboard && - (isWithinWeek(user.date_joined) || !recentViews.length) + (isWithinWeek(user.first_login) || !recentItems.length) ); }; -const isRecentSection = ({ recentViews }: HomeContentProps) => { - return recentViews.length > 0; +const isRecentSection = ({ recentItems }: HomeContentProps) => { + return recentItems.length > 0; }; const isXraySection = ({ databases }: HomeContentProps) => { @@ -47,10 +47,8 @@ const isXraySection = ({ databases }: HomeContentProps) => { const isWithinWeek = (timestamp: string) => { const date = parseTimestamp(timestamp); - const today = moment(); - const weekAgo = today.clone().subtract(1, "week"); - - return date.isBetween(weekAgo, today); + const weekAgo = moment().subtract(1, "week"); + return date.isAfter(weekAgo); }; export default HomeContent; diff --git a/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.unit.spec.tsx new file mode 100644 index 00000000000..4856f0426c7 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeContent/HomeContent.unit.spec.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { + createMockDatabase, + createMockRecentItem, + createMockUser, +} from "metabase-types/api/mocks"; +import HomeContent, { HomeContentProps } from "./HomeContent"; + +const PopularSectionMock = () => <div>PopularSection</div>; +jest.mock("../../containers/HomePopularSection", () => PopularSectionMock); + +const RecentSectionMock = () => <div>RecentSection</div>; +jest.mock("../../containers/HomeRecentSection", () => RecentSectionMock); + +const XraySectionMock = () => <div>XraySection</div>; +jest.mock("../../containers/HomeXraySection", () => XraySectionMock); + +describe("HomeContent", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date(2020, 0, 10)); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should render popular items for a new user", () => { + const props = getProps({ + user: createMockUser({ + is_installer: false, + has_question_and_dashboard: true, + first_login: "2020-01-05T00:00:00Z", + }), + databases: [createMockDatabase()], + recentItems: [createMockRecentItem()], + }); + + render(<HomeContent {...props} />); + + expect(screen.getByText("PopularSection")).toBeInTheDocument(); + }); + + it("should render popular items for a user without recent items", () => { + const props = getProps({ + user: createMockUser({ + is_installer: false, + has_question_and_dashboard: true, + first_login: "2020-01-05T00:00:00Z", + }), + databases: [createMockDatabase()], + recentItems: [], + }); + + render(<HomeContent {...props} />); + + expect(screen.getByText("PopularSection")).toBeInTheDocument(); + }); + + it("should render recent items for an existing user", () => { + const props = getProps({ + user: createMockUser({ + is_installer: false, + has_question_and_dashboard: true, + first_login: "2020-01-01T00:00:00Z", + }), + databases: [createMockDatabase()], + recentItems: [createMockRecentItem()], + }); + + render(<HomeContent {...props} />); + + expect(screen.getByText("RecentSection")).toBeInTheDocument(); + }); + + it("should render x-rays for an installer after the setup", () => { + const props = getProps({ + user: createMockUser({ + is_installer: true, + has_question_and_dashboard: false, + first_login: "2020-01-10T00:00:00Z", + }), + databases: [createMockDatabase()], + recentItems: [], + }); + + render(<HomeContent {...props} />); + + expect(screen.getByText("XraySection")).toBeInTheDocument(); + }); + + it("should render nothing if there are no databases", () => { + const props = getProps({ + user: createMockUser({ + is_installer: true, + has_question_and_dashboard: false, + first_login: "2020-01-10T00:00:00Z", + }), + databases: [], + recentItems: [], + }); + + render(<HomeContent {...props} />); + + expect(screen.queryByText("XraySection")).not.toBeInTheDocument(); + }); +}); + +const getProps = (opts?: Partial<HomeContentProps>): HomeContentProps => ({ + user: createMockUser(), + databases: [], + recentItems: [], + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.styled.tsx index 257065f86e4..6ee18643010 100644 --- a/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.styled.tsx @@ -1,6 +1,7 @@ import styled from "@emotion/styled"; import { color } from "metabase/lib/colors"; import MetabotLogo from "metabase/components/MetabotLogo"; +import { breakpointMinExtraLarge } from "metabase/styled-components/theme"; export const GreetingRoot = styled.div` display: flex; @@ -9,6 +10,10 @@ export const GreetingRoot = styled.div` export const GreetingLogo = styled(MetabotLogo)` height: 2rem; + + ${breakpointMinExtraLarge} { + height: 2.5rem; + } `; export interface GreetingMessageProps { @@ -21,4 +26,8 @@ export const GreetingMessage = styled.span<GreetingMessageProps>` font-weight: bold; line-height: 1.5rem; margin-left: ${props => props.showLogo && "0.5rem"}; + + ${breakpointMinExtraLarge} { + font-size: ${props => (props.showLogo ? "1.25rem" : "1.5rem")}; + } `; diff --git a/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.unit.spec.tsx new file mode 100644 index 00000000000..cc9c39e3ac3 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeGreeting/HomeGreeting.unit.spec.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { createMockUser } from "metabase-types/api/mocks"; +import HomeGreeting, { HomeGreetingProps } from "./HomeGreeting"; + +describe("HomeGreeting", () => { + it("should render with logo", () => { + const props = getProps({ + user: createMockUser({ first_name: "John" }), + showLogo: true, + }); + + render(<HomeGreeting {...props} />); + + expect(screen.getByText(/John/)).toBeInTheDocument(); + expect(screen.getByRole("img")).toBeInTheDocument(); + }); + + it("should render without logo", () => { + const props = getProps({ + user: createMockUser({ first_name: "John" }), + showLogo: false, + }); + + render(<HomeGreeting {...props} />); + + expect(screen.getByText(/John/)).toBeInTheDocument(); + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + }); +}); + +const getProps = (opts?: Partial<HomeGreetingProps>): HomeGreetingProps => ({ + user: createMockUser(), + showLogo: false, + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.styled.tsx index f77d1bd084d..0e3f63983fd 100644 --- a/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.styled.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import { color } from "metabase/lib/colors"; import Icon from "metabase/components/Icon"; import ExternalLink from "metabase/core/components/ExternalLink"; +import { breakpointMinLarge } from "metabase/styled-components/theme"; export const CardRoot = styled(ExternalLink)` display: flex; @@ -9,6 +10,10 @@ export const CardRoot = styled(ExternalLink)` padding: 1rem; border: 1px solid ${color("focus")}; border-radius: 0.5rem; + + ${breakpointMinLarge} { + padding: 1.5rem; + } `; export const CardIcon = styled(Icon)` diff --git a/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.unit.spec.tsx new file mode 100644 index 00000000000..8b0bccaebf9 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeHelpCard/HomeHelpCard.unit.spec.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeHelpCard from "./HomeHelpCard"; + +describe("HomeHelpCard", () => { + it("should render correctly", () => { + render(<HomeHelpCard />); + + expect(screen.getByText("Metabase tips")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.styled.tsx index 30f0a7f6f27..7fda46edab6 100644 --- a/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.styled.tsx @@ -1,6 +1,11 @@ import styled from "@emotion/styled"; import { css } from "@emotion/react"; import { alpha, color } from "metabase/lib/colors"; +import { + breakpointMinExtraLarge, + breakpointMinLarge, + breakpointMinMedium, +} from "metabase/styled-components/theme"; export interface LayoutProps { showScene?: boolean; @@ -24,10 +29,30 @@ const gradientStyles = css` export const LayoutRoot = styled.div<LayoutProps>` min-height: 100%; - padding: 4rem 7rem; + padding: 1rem; ${props => (props.showScene ? sceneStyles : gradientStyles)}; + + ${breakpointMinMedium} { + padding: 3rem 4rem; + } + + ${breakpointMinLarge} { + padding: 4rem 7rem 2rem; + } + + ${breakpointMinExtraLarge} { + padding: 10rem 15rem 4rem; + } `; export const LayoutBody = styled.div` - margin-top: 6rem; + margin-top: 2.5rem; + + ${breakpointMinMedium} { + margin-top: 4rem; + } + + ${breakpointMinLarge} { + margin-top: 6rem; + } `; diff --git a/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.unit.spec.tsx new file mode 100644 index 00000000000..40db6cd646f --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeLayout/HomeLayout.unit.spec.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeLayout, { HomeLayoutProps } from "./HomeLayout"; + +const HomeGreetingMock = () => <div>Hey there</div>; +jest.mock("../../containers/HomeGreeting", () => HomeGreetingMock); + +describe("HomeLayout", () => { + it("should render correctly", () => { + const props = getProps(); + + render(<HomeLayout {...props} />); + + expect(screen.getByText("Hey there")).toBeInTheDocument(); + }); +}); + +const getProps = (opts?: Partial<HomeLayoutProps>): HomeLayoutProps => ({ + showScene: false, + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeModelCard/HomeModelCard.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeModelCard/HomeModelCard.unit.spec.tsx new file mode 100644 index 00000000000..e9f485f5754 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeModelCard/HomeModelCard.unit.spec.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeModelCard, { HomeModelCardProps } from "./HomeModelCard"; + +describe("HomeModelCard", () => { + it("should render correctly", () => { + const props = getProps({ + title: "Orders", + icon: { name: "table" }, + }); + + render(<HomeModelCard {...props} />); + + expect(screen.getByText("Orders")).toBeInTheDocument(); + expect(screen.getByLabelText("table icon")).toBeInTheDocument(); + }); +}); + +const getProps = (opts?: Partial<HomeModelCardProps>): HomeModelCardProps => ({ + title: "Orders", + icon: { name: "card" }, + url: "/question/1", + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomePage/HomePage.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomePage/HomePage.unit.spec.tsx new file mode 100644 index 00000000000..c934721207c --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomePage/HomePage.unit.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import * as dom from "metabase/lib/dom"; +import HomePage from "./HomePage"; + +jest.mock("metabase/lib/dom"); + +const LayoutMock = () => <div />; +jest.mock("../../containers/HomeLayout", () => LayoutMock); + +const ContentMock = () => <div />; +jest.mock("../../containers/HomeContent", () => ContentMock); + +describe("HomePage", () => { + let isSmallScreenSpy: jest.SpyInstance; + + beforeEach(() => { + isSmallScreenSpy = jest.spyOn(dom, "isSmallScreen"); + }); + + afterEach(() => { + isSmallScreenSpy.mockRestore(); + }); + + it("should open the navbar on a regular screen", () => { + const onOpenNavbar = jest.fn(); + isSmallScreenSpy.mockReturnValue(false); + + render(<HomePage onOpenNavbar={onOpenNavbar} />); + + expect(onOpenNavbar).toHaveBeenCalled(); + }); + + it("should not open the navbar on a small screen", () => { + const onOpenNavbar = jest.fn(); + isSmallScreenSpy.mockReturnValue(true); + + render(<HomePage onOpenNavbar={onOpenNavbar} />); + + expect(onOpenNavbar).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.styled.tsx b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.styled.tsx index d37a1a3cc1e..282480366e3 100644 --- a/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.styled.tsx @@ -1,16 +1,4 @@ import styled from "@emotion/styled"; -import { color } from "metabase/lib/colors"; -import HomeCard from "metabase/home/homepage/components/HomeCard"; -import Icon from "metabase/components/Icon"; -import Ellipsified from "metabase/components/Ellipsified"; - -export const SectionTitle = styled.div` - display: flex; - align-items: center; - color: ${color("text-medium")}; - font-weight: bold; - margin-bottom: 1.5rem; -`; export const SectionBody = styled.div` display: flex; diff --git a/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.tsx b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.tsx index 27161a7a6f4..74ebd1cd1be 100644 --- a/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.tsx +++ b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.tsx @@ -2,24 +2,25 @@ import React from "react"; import { t } from "ttag"; import _ from "underscore"; import * as Urls from "metabase/lib/urls"; -import { getIcon, getName } from "metabase/entities/popular-views"; -import { PopularView } from "metabase-types/api"; +import { getIcon, getName } from "metabase/entities/popular-items"; +import { PopularItem } from "metabase-types/api"; +import HomeCaption from "../HomeCaption"; import HomeHelpCard from "../HomeHelpCard"; import HomeModelCard from "../HomeModelCard"; -import { SectionBody, SectionTitle } from "./HomePopularSection.styled"; +import { SectionBody } from "./HomePopularSection.styled"; export interface HomePopularSectionProps { - popularViews: PopularView[]; + popularItems: PopularItem[]; } const HomePopularSection = ({ - popularViews, + popularItems, }: HomePopularSectionProps): JSX.Element => { return ( <div> - <SectionTitle>{getTitle(popularViews)}</SectionTitle> + <HomeCaption>{getTitle(popularItems)}</HomeCaption> <SectionBody> - {popularViews.map((item, index) => ( + {popularItems.map((item, index) => ( <HomeModelCard key={index} title={getName(item)} @@ -33,11 +34,11 @@ const HomePopularSection = ({ ); }; -const getTitle = (popularViews: PopularView[]) => { - const models = _.uniq(popularViews.map(item => item.model)); +const getTitle = (popularItems: PopularItem[]) => { + const models = _.uniq(popularItems.map(item => item.model)); if (models.length !== 1) { - return t`Here is some popular stuff`; + return t`Here are some popular items`; } switch (models[0]) { @@ -50,7 +51,7 @@ const getTitle = (popularViews: PopularView[]) => { case "dashboard": return t`Here are some popular dashboards`; default: - return t`Here is some popular stuff`; + return t`Here are some popular items`; } }; diff --git a/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.unit.spec.tsx new file mode 100644 index 00000000000..d3a78db89e3 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomePopularSection/HomePopularSection.unit.spec.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { createMockPopularItem } from "metabase-types/api/mocks"; +import HomePopularSection, { + HomePopularSectionProps, +} from "./HomePopularSection"; + +describe("HomePopularSection", () => { + it("should render a list of items of the same type", () => { + const props = getProps({ + popularItems: [ + createMockPopularItem({ + model: "dashboard", + model_object: { + name: "Metrics", + }, + }), + createMockPopularItem({ + model: "dashboard", + model_object: { + name: "Revenue", + }, + }), + ], + }); + + render(<HomePopularSection {...props} />); + + expect(screen.getByText(/popular dashboards/)).toBeInTheDocument(); + }); + + it("should render a list of items of different types", () => { + const props = getProps({ + popularItems: [ + createMockPopularItem({ + model: "dashboard", + model_object: { + name: "Metrics", + }, + }), + createMockPopularItem({ + model: "card", + model_object: { + name: "Revenue", + }, + }), + ], + }); + + render(<HomePopularSection {...props} />); + + expect(screen.getByText(/popular items/)).toBeInTheDocument(); + }); +}); + +const getProps = ( + opts?: Partial<HomePopularSectionProps>, +): HomePopularSectionProps => ({ + popularItems: [], + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.styled.tsx index e82e897ce87..282480366e3 100644 --- a/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.styled.tsx @@ -1,13 +1,4 @@ import styled from "@emotion/styled"; -import { color } from "metabase/lib/colors"; - -export const SectionTitle = styled.div` - display: flex; - align-items: center; - color: ${color("text-medium")}; - font-weight: bold; - margin-bottom: 1.5rem; -`; export const SectionBody = styled.div` display: flex; diff --git a/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.tsx b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.tsx index 7bce8f97ebf..8d62d0f5752 100644 --- a/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.tsx @@ -1,23 +1,24 @@ import React from "react"; import { t } from "ttag"; import * as Urls from "metabase/lib/urls"; -import { getIcon, getName } from "metabase/entities/recent-views"; -import { RecentView } from "metabase-types/api"; +import { getIcon, getName } from "metabase/entities/recent-items"; +import { RecentItem } from "metabase-types/api"; +import HomeCaption from "../HomeCaption"; import HomeModelCard from "../HomeModelCard"; -import { SectionBody, SectionTitle } from "./HomeRecentSection.styled"; +import { SectionBody } from "./HomeRecentSection.styled"; export interface HomeRecentSectionProps { - recentViews: RecentView[]; + recentItems: RecentItem[]; } const HomeRecentSection = ({ - recentViews, + recentItems, }: HomeRecentSectionProps): JSX.Element => { return ( <div> - <SectionTitle>{t`Pick up where you left off`}</SectionTitle> + <HomeCaption>{t`Pick up where you left off`}</HomeCaption> <SectionBody> - {recentViews.map((item, index) => ( + {recentItems.map((item, index) => ( <HomeModelCard key={index} title={getName(item)} diff --git a/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.unit.spec.tsx new file mode 100644 index 00000000000..d7822fac5da --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeRecentSection/HomeRecentSection.unit.spec.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeRecentSection, { HomeRecentSectionProps } from "./HomeRecentSection"; +import { createMockRecentItem } from "metabase-types/api/mocks"; + +describe("HomeRecentSection", () => { + it("should render a list of recent items", () => { + const props = getProps({ + recentItems: [ + createMockRecentItem({ + model: "table", + model_object: { + name: "Orders", + }, + }), + ], + }); + + render(<HomeRecentSection {...props} />); + + expect(screen.getByText("Pick up where you left off")).toBeInTheDocument(); + expect(screen.getByText("Orders")).toBeInTheDocument(); + }); +}); + +const getProps = ( + opts?: Partial<HomeRecentSectionProps>, +): HomeRecentSectionProps => ({ + recentItems: [], + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeXrayCard/HomeXrayCard.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeXrayCard/HomeXrayCard.unit.spec.tsx new file mode 100644 index 00000000000..3f987a71d69 --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeXrayCard/HomeXrayCard.unit.spec.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import HomeXrayCard, { HomeXrayCardProps } from "./HomeXrayCard"; + +describe("HomeXrayCard", () => { + it("should render correctly", () => { + const props = getProps({ + title: "Orders", + message: "A look at", + }); + + render(<HomeXrayCard {...props} />); + + expect(screen.getByText("Orders")).toBeInTheDocument(); + expect(screen.getByText("A look at")).toBeInTheDocument(); + }); +}); + +const getProps = (opts?: Partial<HomeXrayCardProps>): HomeXrayCardProps => ({ + title: "Orders", + message: "A look at", + url: "/question/1", + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.styled.tsx b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.styled.tsx index 0b260cdf1aa..e849ea1a4a3 100644 --- a/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.styled.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.styled.tsx @@ -3,12 +3,10 @@ import { color } from "metabase/lib/colors"; import Link from "metabase/core/components/Link"; import Icon from "metabase/components/Icon"; -export const SectionTitle = styled.div` +export const SectionBody = styled.div` display: flex; - align-items: center; - color: ${color("text-dark")}; - font-weight: bold; - margin-bottom: 1.5rem; + gap: 1.5rem; + flex-wrap: wrap; `; export const DatabaseLink = styled(Link)` @@ -17,20 +15,33 @@ export const DatabaseLink = styled(Link)` margin-left: 0.5rem; `; -export const DatabaseIcon = styled(Icon)` +export const DatabaseLinkIcon = styled(Icon)` color: ${color("focus")}; width: 1rem; height: 1rem; margin-right: 0.25rem; `; -export const DatabaseTitle = styled.span` +export const DatabaseLinkText = styled.span` color: ${color("brand")}; font-weight: bold; `; -export const XrayList = styled.div` +export const SchemaTrigger = styled.span` display: flex; - gap: 1.5rem; - flex-wrap: wrap; + align-items: center; + margin: 0 0.5rem; + cursor: pointer; +`; + +export const SchemaTriggerIcon = styled(Icon)` + color: ${color("brand")}; + width: 0.625rem; + height: 0.625rem; + margin-left: 0.25rem; +`; + +export const SchemaTriggerText = styled.span` + color: ${color("brand")}; + font-weight: bold; `; diff --git a/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.tsx b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.tsx index 9b947700e31..be44c6a3d2c 100644 --- a/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.tsx +++ b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.tsx @@ -1,48 +1,63 @@ -import React, { useMemo } from "react"; +import React, { ChangeEvent, useCallback, useMemo, useState } from "react"; import _ from "underscore"; import { t } from "ttag"; import * as Urls from "metabase/lib/urls"; +import Select from "metabase/core/components/Select"; import { Database, DatabaseCandidate } from "metabase-types/api"; +import HomeCaption from "../HomeCaption"; import HomeXrayCard from "../HomeXrayCard"; import { - DatabaseIcon, + DatabaseLinkIcon, DatabaseLink, - DatabaseTitle, - SectionTitle, - XrayList, + DatabaseLinkText, + SectionBody, + SchemaTrigger, + SchemaTriggerText, + SchemaTriggerIcon, } from "./HomeXraySection.styled"; -export interface XraySectionProps { - database?: Database; - databaseCandidates: DatabaseCandidate[]; +export interface HomeXraySectionProps { + database: Database; + candidates: DatabaseCandidate[]; } const HomeXraySection = ({ database, - databaseCandidates, -}: XraySectionProps): JSX.Element => { - const isSample = !database || database.is_sample; - const tables = databaseCandidates.flatMap(d => d.tables); - const tableCount = tables.length; + candidates, +}: HomeXraySectionProps): JSX.Element => { + const isSample = database.is_sample; + const schemas = candidates.map(d => d.schema); + const [schema, setSchema] = useState(schemas[0]); + const candidate = candidates.find(d => d.schema === schema); + const tableCount = candidate ? candidate.tables.length : 0; const tableMessages = useMemo(() => getMessages(tableCount), [tableCount]); + const canSelectSchema = schemas.length > 1; return ( <div> {isSample ? ( - <SectionTitle> + <HomeCaption primary> {t`Try out these sample x-rays to see what Metabase can do.`} - </SectionTitle> + </HomeCaption> + ) : canSelectSchema ? ( + <HomeCaption primary> + {t`Here are some explorations of the`} + <SchemaSelect + schema={schema} + schemas={schemas} + onChange={setSchema} + /> + {t`schema in`} + <DatabaseInfo database={database} /> + </HomeCaption> ) : ( - <SectionTitle> + <HomeCaption primary> {t`Here are some explorations of`} - <DatabaseLink to={Urls.browseDatabase(database)}> - <DatabaseIcon name="database" /> - <DatabaseTitle>{database.name}</DatabaseTitle> - </DatabaseLink> - </SectionTitle> + <DatabaseInfo database={database} /> + </HomeCaption> )} - <XrayList> - {tables.map((table, index) => ( + <SectionBody> + {candidate?.tables.map((table, index) => ( <HomeXrayCard key={table.url} title={table.title} @@ -50,11 +65,57 @@ const HomeXraySection = ({ message={tableMessages[index]} /> ))} - </XrayList> + </SectionBody> </div> ); }; +interface SchemaSelectProps { + schema: string; + schemas: string[]; + onChange?: (schema: string) => void; +} + +const SchemaSelect = ({ schema, schemas, onChange }: SchemaSelectProps) => { + const trigger = ( + <SchemaTrigger> + <SchemaTriggerText>{schema}</SchemaTriggerText> + <SchemaTriggerIcon name="chevrondown" /> + </SchemaTrigger> + ); + + const handleChange = useCallback( + (event: ChangeEvent<HTMLSelectElement>) => { + onChange?.(event.target.value); + }, + [onChange], + ); + + return ( + <Select + value={schema} + options={schemas} + optionNameFn={getSchemaOption} + optionValueFn={getSchemaOption} + onChange={handleChange} + triggerElement={trigger} + /> + ); +}; + +interface DatabaseInfoProps { + database: Database; +} + +const DatabaseInfo = ({ database }: DatabaseInfoProps) => { + return ( + <DatabaseLink to={Urls.browseDatabase(database)}> + <DatabaseLinkIcon name="database" /> + <DatabaseLinkText>{database.name}</DatabaseLinkText> + </DatabaseLink> + ); +}; + const getMessages = (count: number) => { const options = [ t`A look at`, @@ -70,4 +131,8 @@ const getMessages = (count: number) => { .value(); }; +const getSchemaOption = (schema: string) => { + return schema; +}; + export default HomeXraySection; diff --git a/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.unit.spec.tsx b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.unit.spec.tsx new file mode 100644 index 00000000000..e70ba019ced --- /dev/null +++ b/frontend/src/metabase/home/homepage/components/HomeXraySection/HomeXraySection.unit.spec.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { + createMockDatabase, + createMockDatabaseCandidate, + createMockTableCandidate, +} from "metabase-types/api/mocks"; +import HomeXraySection, { HomeXraySectionProps } from "./HomeXraySection"; + +describe("HomeXraySection", () => { + it("should show x-rays for a sample database", () => { + const props = getProps({ + database: createMockDatabase({ + is_sample: true, + }), + candidates: [ + createMockDatabaseCandidate({ + tables: [ + createMockTableCandidate({ title: "Orders" }), + createMockTableCandidate({ title: "People" }), + ], + }), + ], + }); + + render(<HomeXraySection {...props} />); + + expect(screen.getByText(/Try out these sample x-rays/)).toBeInTheDocument(); + expect(screen.getByText("Orders")).toBeInTheDocument(); + expect(screen.getByText("People")).toBeInTheDocument(); + }); + + it("should show x-rays for a user database", () => { + const props = getProps({ + database: createMockDatabase({ + name: "H2", + is_sample: false, + }), + candidates: [ + createMockDatabaseCandidate({ + schema: "public", + tables: [createMockTableCandidate({ title: "Orders" })], + }), + createMockDatabaseCandidate({ + schema: "internal", + tables: [createMockTableCandidate({ title: "People" })], + }), + ], + }); + + render(<HomeXraySection {...props} />); + + expect(screen.getByText(/Here are some explorations/)).toBeInTheDocument(); + expect(screen.getByText("H2")).toBeInTheDocument(); + expect(screen.getByText("Orders")).toBeInTheDocument(); + expect(screen.queryByText("People")).not.toBeInTheDocument(); + }); +}); + +const getProps = ( + opts?: Partial<HomeXraySectionProps>, +): HomeXraySectionProps => ({ + database: createMockDatabase(), + candidates: [], + ...opts, +}); diff --git a/frontend/src/metabase/home/homepage/containers/HomeContent/HomeContent.tsx b/frontend/src/metabase/home/homepage/containers/HomeContent/HomeContent.tsx index 6039a94a9a2..3e8119cfb42 100644 --- a/frontend/src/metabase/home/homepage/containers/HomeContent/HomeContent.tsx +++ b/frontend/src/metabase/home/homepage/containers/HomeContent/HomeContent.tsx @@ -1,7 +1,7 @@ import { connect } from "react-redux"; import _ from "underscore"; import Databases from "metabase/entities/databases"; -import RecentViews from "metabase/entities/recent-views"; +import RecentItems from "metabase/entities/recent-items"; import { getUser } from "metabase/selectors/user"; import { State } from "metabase-types/store"; import HomeContent from "../../components/HomeContent"; @@ -12,6 +12,6 @@ const mapStateToProps = (state: State) => ({ export default _.compose( Databases.loadList(), - RecentViews.loadList({ reload: true }), + RecentItems.loadList({ reload: true }), connect(mapStateToProps), )(HomeContent); diff --git a/frontend/src/metabase/home/homepage/containers/HomePopularSection/HomePopularSection.tsx b/frontend/src/metabase/home/homepage/containers/HomePopularSection/HomePopularSection.tsx index 1aca5deadd6..636ced3e075 100644 --- a/frontend/src/metabase/home/homepage/containers/HomePopularSection/HomePopularSection.tsx +++ b/frontend/src/metabase/home/homepage/containers/HomePopularSection/HomePopularSection.tsx @@ -1,4 +1,4 @@ -import PopularViews from "metabase/entities/popular-views"; +import PopularItems from "metabase/entities/popular-items"; import HomePopularSection from "../../components/HomePopularSection"; -export default PopularViews.loadList({ reload: true })(HomePopularSection); +export default PopularItems.loadList({ reload: true })(HomePopularSection); diff --git a/frontend/src/metabase/home/homepage/containers/HomeRecentSection/HomeRecentSection.tsx b/frontend/src/metabase/home/homepage/containers/HomeRecentSection/HomeRecentSection.tsx index b0ba5e69af9..d846b20bc7f 100644 --- a/frontend/src/metabase/home/homepage/containers/HomeRecentSection/HomeRecentSection.tsx +++ b/frontend/src/metabase/home/homepage/containers/HomeRecentSection/HomeRecentSection.tsx @@ -1,4 +1,4 @@ -import RecentViews from "metabase/entities/recent-views"; +import RecentItems from "metabase/entities/recent-items"; import HomeRecentSection from "../../components/HomeRecentSection"; -export default RecentViews.loadList()(HomeRecentSection); +export default RecentItems.loadList()(HomeRecentSection); diff --git a/frontend/src/metabase/home/homepage/containers/HomeXraySection/HomeXraySection.tsx b/frontend/src/metabase/home/homepage/containers/HomeXraySection/HomeXraySection.tsx index 6d8da58fbae..65d573d230b 100644 --- a/frontend/src/metabase/home/homepage/containers/HomeXraySection/HomeXraySection.tsx +++ b/frontend/src/metabase/home/homepage/containers/HomeXraySection/HomeXraySection.tsx @@ -29,5 +29,5 @@ const mapStateToProps = (state: State, props: XraySectionProps) => ({ export default _.compose( Databases.loadList(), connect(mapStateToProps), - DatabaseCandidates.loadList({ query: getXrayQuery }), + DatabaseCandidates.loadList({ query: getXrayQuery, listName: "candidates" }), )(HomeXraySection); diff --git a/frontend/src/metabase/nav/components/RecentsList.jsx b/frontend/src/metabase/nav/components/RecentsList.jsx index 4ce1cfbf33b..34e83e89a3f 100644 --- a/frontend/src/metabase/nav/components/RecentsList.jsx +++ b/frontend/src/metabase/nav/components/RecentsList.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { t } from "ttag"; import _ from "underscore"; -import RecentViews from "metabase/entities/recent-views"; +import RecentItems from "metabase/entities/recent-items"; import Text from "metabase/components/type/Text"; import * as Urls from "metabase/lib/urls"; import { isSyncCompleted } from "metabase/lib/syncing"; @@ -138,7 +138,7 @@ const isItemLoading = ({ model, model_object }) => { }; export default _.compose( - RecentViews.loadList({ + RecentItems.loadList({ wrapped: true, reload: true, loadingAndErrorWrapper: false, diff --git a/frontend/src/metabase/schema.js b/frontend/src/metabase/schema.js index c391b20d475..82e49b2bfa7 100644 --- a/frontend/src/metabase/schema.js +++ b/frontend/src/metabase/schema.js @@ -110,11 +110,11 @@ CollectionSchema.define({ items: [ObjectUnionSchema], }); -export const RecentViewsSchema = new schema.Entity("recentViews", undefined, { +export const RecentItemSchema = new schema.Entity("recentItems", undefined, { idAttribute: ({ model, model_id }) => `${model}:${model_id}`, }); -export const PopularViewsSchema = new schema.Entity("popularViews", undefined, { +export const PopularItemSchema = new schema.Entity("popularItems", undefined, { idAttribute: ({ model, model_id }) => `${model}:${model_id}`, }); diff --git a/frontend/test/metabase/scenarios/onboarding/home/homepage.cy.spec.js b/frontend/test/metabase/scenarios/onboarding/home/homepage.cy.spec.js index d0ffb096a06..97b414cca4b 100644 --- a/frontend/test/metabase/scenarios/onboarding/home/homepage.cy.spec.js +++ b/frontend/test/metabase/scenarios/onboarding/home/homepage.cy.spec.js @@ -1,7 +1,100 @@ -import { restore } from "__support__/e2e/cypress"; +import { restore, visitDashboard } from "__support__/e2e/cypress"; describe("scenarios > home > homepage", () => { beforeEach(() => { - restore(); + cy.intercept("GET", `/api/dashboard/**`).as("getDashboard"); + cy.intercept("GET", "/api/automagic-*/table/**").as("getXrayDashboard"); + cy.intercept("GET", "/api/automagic-*/database/**").as("getXrayCandidates"); + cy.intercept("GET", "/api/activity/recent_views").as("getRecentItems"); + cy.intercept("GET", "/api/activity/popular_items").as("getPopularItems"); + }); + + it("should display x-rays for the sample database", () => { + restore("setup"); + cy.signInAsAdmin(); + + cy.visit("/"); + cy.wait("@getXrayCandidates"); + cy.findByText("Try out these sample x-rays to see what Metabase can do."); + cy.findByText("Orders").click(); + + cy.wait("@getXrayDashboard"); + cy.findByText("More X-rays"); + }); + + it("should display x-rays for a user database", () => { + restore("setup"); + cy.signInAsAdmin(); + cy.addH2SampleDatabase({ name: "H2" }); + + cy.visit("/"); + cy.wait("@getXrayCandidates"); + cy.findByText("Here are some explorations of"); + cy.findByText("H2"); + cy.findByText("Orders").click(); + + cy.wait("@getXrayDashboard"); + cy.findByText("More X-rays"); + }); + + it("should allow switching between multiple schemas for x-rays", () => { + restore("setup"); + cy.signInAsAdmin(); + cy.addH2SampleDatabase({ name: "H2" }); + cy.intercept("/api/automagic-*/database/**", getXrayCandidates()); + + cy.visit("/"); + cy.findByText(/Here are some explorations of the/); + cy.findByText("public"); + cy.findByText("H2"); + cy.findByText("Orders"); + cy.findByText("People").should("not.exist"); + + cy.findByText("public").click(); + cy.findByText("private").click(); + cy.findByText("People"); + cy.findByText("Orders").should("not.exist"); + }); + + it("should display recent items", () => { + restore("default"); + cy.signInAsAdmin(); + + visitDashboard(1); + cy.findByText("Orders in a dashboard"); + + cy.visit("/"); + cy.wait("@getRecentItems"); + cy.findByText("Pick up where you left off"); + + cy.findByText("Orders in a dashboard").click(); + cy.wait("@getDashboard"); + cy.findByText("Orders"); + }); + + it.skip("should display popular items for a new user", () => { + restore("default"); + cy.signInAsNormalUser(); + + cy.visit("/"); + cy.wait("@getPopularItems"); + cy.findByText("Here are some popular items"); + + cy.findByText("Orders in a dashboard").click(); + cy.wait("@getDashboard"); + cy.findByText("Orders"); }); }); + +const getXrayCandidates = () => [ + { + id: "1/public", + schema: "public", + tables: [{ title: "Orders", url: "/auto/dashboard/table/1" }], + }, + { + id: "1/private", + schema: "private", + tables: [{ title: "People", url: "/auto/dashboard/table/2" }], + }, +]; diff --git a/frontend/test/snapshot-creators/default.cy.snap.js b/frontend/test/snapshot-creators/default.cy.snap.js index 46d3317acb0..0f890f900f0 100644 --- a/frontend/test/snapshot-creators/default.cy.snap.js +++ b/frontend/test/snapshot-creators/default.cy.snap.js @@ -30,6 +30,7 @@ describe("snapshots", () => { snapshot("blank"); setup(); updateSettings(); + snapshot("setup"); addUsersAndGroups(); createCollections(); withSampleDatabase(SAMPLE_DATABASE => { -- GitLab