Skip to content
Snippets Groups Projects
Unverified Commit 3051967c authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Migrate home to redux toolkit and unify jest tests (#31213)

parent 3438e466
No related branches found
No related tags found
No related merge requests found
Showing
with 169 additions and 204 deletions
import { DashboardId } from "./dashboard";
export type UserId = number;
export type UserAttribute = string;
......@@ -27,7 +29,7 @@ export interface User extends BaseUser {
has_question_and_dashboard: boolean;
personal_collection_id: number;
custom_homepage: {
dashboard_id: number;
dashboard_id: DashboardId;
} | null;
}
......
import { useDeepCompareEffect } from "react-use";
import { useDeepCompareEffect, usePrevious } from "react-use";
import type { Action } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { State } from "metabase-types/store";
......@@ -50,17 +50,19 @@ export const useEntityListQuery = <TItem, TQuery = never>(
): UseEntityListQueryResult<TItem> => {
const options = { entityQuery };
const data = useSelector(state => getList(state, options));
const error = useSelector(state => getError(state, options));
const isLoading = useSelector(state => getLoading(state, options));
const isLoaded = useSelector(state => getLoaded(state, options));
const error = useSelector(state => getError(state, options));
const isLoadedPreviously = usePrevious(isLoaded);
const isInvalidated = !isLoaded && isLoadedPreviously;
const dispatch = useDispatch();
useDeepCompareEffect(() => {
if (enabled && !isLoaded) {
if (enabled || (enabled && isInvalidated)) {
const action = dispatch(fetchList(entityQuery, { reload }));
Promise.resolve(action).catch(() => undefined);
}
}, [dispatch, fetchList, entityQuery, reload, enabled, isLoaded]);
}, [dispatch, fetchList, entityQuery, reload, enabled, isInvalidated]);
return { data, isLoading, error };
};
......@@ -23,13 +23,13 @@ export const CustomHomePageModal = ({
isOpen,
onClose,
}: CustomHomePageModalProps) => {
const [dashboard, setDashboard] = useState<DashboardId>();
const [dashboardId, setDashboardId] = useState<DashboardId>();
const dispatch = useDispatch();
const handleSave = async () => {
await dispatch(
updateSettings({
[CUSTOM_HOMEPAGE_DASHBOARD_SETTING_KEY]: dashboard,
[CUSTOM_HOMEPAGE_DASHBOARD_SETTING_KEY]: dashboardId,
[CUSTOM_HOMEPAGE_SETTING_KEY]: true,
}),
);
......@@ -37,14 +37,14 @@ export const CustomHomePageModal = ({
};
const handleChange = useCallback(
(value: number | null | undefined | string) => {
(value: DashboardId | null | undefined) => {
if (value) {
setDashboard(value);
setDashboardId(value);
} else {
setDashboard(undefined);
setDashboardId(undefined);
}
},
[setDashboard],
[setDashboardId],
);
return (
......@@ -63,7 +63,7 @@ export const CustomHomePageModal = ({
>
<p>{t`Pick one of your dashboards to serve as homepage. Users without dashboard access will be directed to the default homepage. You can update or reset this anytime in Admin Settings > Settings > General`}</p>
<DashboardSelector
value={dashboard}
value={dashboardId}
onChange={handleChange}
collectionFilter={(collection: Collection) =>
collection.personal_owner_id === null || collection.id === "root"
......
export * from "./CustomHomePageModal";
import { ReactNode } from "react";
import { CaptionRoot } from "./HomeCaption.styled";
export interface HomeCaptionProps {
interface HomeCaptionProps {
primary?: boolean;
children?: ReactNode;
}
const HomeCaption = ({ primary, children }: HomeCaptionProps): JSX.Element => {
export const HomeCaption = ({
primary,
children,
}: HomeCaptionProps): JSX.Element => {
return <CaptionRoot primary={primary}>{children}</CaptionRoot>;
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default HomeCaption;
import { render, screen } from "@testing-library/react";
import HomeCaption from "./HomeCaption";
import { render, screen } from "__support__/ui";
import { HomeCaption } from "./HomeCaption";
const setup = () => {
render(<HomeCaption>Title</HomeCaption>);
};
describe("HomeCaption", () => {
it("should render correctly", () => {
render(<HomeCaption>Title</HomeCaption>);
setup();
expect(screen.getByText("Title")).toBeInTheDocument();
});
});
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./HomeCaption";
export * from "./HomeCaption";
import { ReactNode } from "react";
import { CardRoot } from "./HomeCard.styled";
export interface HomeCardProps {
interface HomeCardProps {
className?: string;
url?: string;
external?: boolean;
children?: ReactNode;
}
const HomeCard = ({
export const HomeCard = ({
className,
url = "",
children,
......@@ -19,6 +19,3 @@ const HomeCard = ({
</CardRoot>
);
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default HomeCard;
import { render, screen } from "@testing-library/react";
import HomeCard from "./HomeCard";
import { render, screen } from "__support__/ui";
import { HomeCard } from "./HomeCard";
const setup = () => {
render(<HomeCard>A look at table</HomeCard>);
};
describe("HomeCard", () => {
it("should render correctly", () => {
render(<HomeCard>A look at table</HomeCard>);
setup();
expect(screen.getByText("A look at table")).toBeInTheDocument();
});
});
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./HomeCard";
export * from "./HomeCard";
import { useSelector } from "metabase/lib/redux";
import { isSyncCompleted } from "metabase/lib/syncing";
import { getUser } from "metabase/selectors/user";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import {
useDatabaseListQuery,
usePopularItemListQuery,
useRecentItemListQuery,
} from "metabase/common/hooks";
import { PopularItem, RecentItem, User } from "metabase-types/api";
import Database from "metabase-lib/metadata/Database";
import HomePopularSection from "../HomePopularSection";
import HomeRecentSection from "../HomeRecentSection";
import HomeXraySection from "../../containers/HomeXraySection";
import { HomePopularSection } from "../HomePopularSection";
import { HomeRecentSection } from "../HomeRecentSection";
import { HomeXraySection } from "../HomeXraySection";
import { getIsXrayEnabled } from "../../selectors";
import { isWithinWeeks } from "../../utils";
export interface HomeContentProps {
user: User;
databases?: Database[];
recentItems?: RecentItem[];
popularItems?: PopularItem[];
isXrayEnabled: boolean;
}
export const HomeContent = (): JSX.Element | null => {
const user = useSelector(getUser);
const isXrayEnabled = useSelector(getIsXrayEnabled);
const { data: databases } = useDatabaseListQuery();
const { data: recentItems } = useRecentItemListQuery({ reload: true });
const { data: popularItems } = usePopularItemListQuery({ reload: true });
const HomeContent = (props: HomeContentProps): JSX.Element | null => {
if (isLoading(props)) {
if (!user || isLoading(user, databases, recentItems, popularItems)) {
return <LoadingAndErrorWrapper loading />;
}
if (isPopularSection(props)) {
if (isPopularSection(user, recentItems, popularItems)) {
return <HomePopularSection />;
}
if (isRecentSection(props)) {
if (isRecentSection(user, recentItems)) {
return <HomeRecentSection />;
}
if (isXraySection(props)) {
if (isXraySection(databases, isXrayEnabled)) {
return <HomeXraySection />;
}
return null;
};
const isLoading = ({
user,
databases,
recentItems,
popularItems,
}: HomeContentProps): boolean => {
const isLoading = (
user: User,
databases: Database[] | undefined,
recentItems: RecentItem[] | undefined,
popularItems: PopularItem[] | undefined,
): boolean => {
if (!user.has_question_and_dashboard) {
return databases == null;
} else if (user.is_installer || !isWithinWeeks(user.first_login, 1)) {
......@@ -50,11 +56,11 @@ const isLoading = ({
}
};
const isPopularSection = ({
user,
recentItems = [],
popularItems = [],
}: HomeContentProps): boolean => {
const isPopularSection = (
user: User,
recentItems: RecentItem[] = [],
popularItems: PopularItem[] = [],
): boolean => {
return (
!user.is_installer &&
user.has_question_and_dashboard &&
......@@ -63,19 +69,16 @@ const isPopularSection = ({
);
};
const isRecentSection = ({
user,
recentItems = [],
}: HomeContentProps): boolean => {
const isRecentSection = (
user: User,
recentItems: RecentItem[] = [],
): boolean => {
return user.has_question_and_dashboard && recentItems.length > 0;
};
const isXraySection = ({
databases = [],
isXrayEnabled,
}: HomeContentProps): boolean => {
const isXraySection = (
databases: Database[] = [],
isXrayEnabled: boolean,
): boolean => {
return databases.some(isSyncCompleted) && isXrayEnabled;
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default HomeContent;
import { checkNotNull } from "metabase/core/utils/types";
import { getMetadata } from "metabase/selectors/metadata";
import { Database, PopularItem, RecentItem, User } from "metabase-types/api";
import {
createMockDatabase,
......@@ -7,19 +5,22 @@ import {
createMockRecentItem,
createMockUser,
} from "metabase-types/api/mocks";
import { createMockState } from "metabase-types/store/mocks";
import { createMockEntitiesState } from "__support__/store";
import { renderWithProviders, screen } from "__support__/ui";
import HomeContent from "./HomeContent";
const PopularSectionMock = () => <div>PopularSection</div>;
jest.mock("../HomePopularSection", () => PopularSectionMock);
const RecentSectionMock = () => <div>RecentSection</div>;
jest.mock("../HomeRecentSection", () => RecentSectionMock);
const XraySectionMock = () => <div>XraySection</div>;
jest.mock("../../containers/HomeXraySection", () => XraySectionMock);
import {
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
import {
renderWithProviders,
screen,
waitForElementToBeRemoved,
} from "__support__/ui";
import {
setupDatabaseCandidatesEndpoint,
setupDatabasesEndpoints,
setupPopularItemsEndpoints,
setupRecentViewsEndpoints,
} from "__support__/server-mocks";
import { HomeContent } from "./HomeContent";
interface SetupOpts {
user: User;
......@@ -29,35 +30,33 @@ interface SetupOpts {
isXrayEnabled?: boolean;
}
const setup = ({
const setup = async ({
user,
databases,
recentItems,
popularItems,
databases = [],
recentItems = [],
popularItems = [],
isXrayEnabled = true,
}: SetupOpts) => {
const state = createMockState({
entities: createMockEntitiesState({ databases }),
currentUser: user,
settings: createMockSettingsState({
"enable-xrays": isXrayEnabled,
}),
});
const metadata = getMetadata(state);
renderWithProviders(
<HomeContent
user={user}
databases={databases?.map(({ id }) =>
checkNotNull(metadata.database(id)),
)}
recentItems={recentItems}
popularItems={popularItems}
isXrayEnabled={isXrayEnabled}
/>,
{ storeInitialState: state },
);
setupDatabasesEndpoints(databases);
setupRecentViewsEndpoints(recentItems);
setupPopularItemsEndpoints(popularItems);
databases.forEach(({ id }) => setupDatabaseCandidatesEndpoint(id, []));
renderWithProviders(<HomeContent />, { storeInitialState: state });
await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));
};
describe("HomeContent", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
jest.setSystemTime(new Date(2020, 0, 10));
});
......@@ -65,8 +64,8 @@ describe("HomeContent", () => {
jest.useRealTimers();
});
it("should render popular items for a new user", () => {
setup({
it("should render popular items for a new user", async () => {
await setup({
user: createMockUser({
is_installer: false,
has_question_and_dashboard: true,
......@@ -77,26 +76,29 @@ describe("HomeContent", () => {
popularItems: [createMockPopularItem()],
});
expect(screen.getByText("PopularSection")).toBeInTheDocument();
expect(
screen.getByText("Here are some popular tables"),
).toBeInTheDocument();
});
it("should render popular items for a user without recent items", () => {
setup({
it("should render popular items for a user without recent items", async () => {
await setup({
user: createMockUser({
is_installer: false,
has_question_and_dashboard: true,
first_login: "2020-01-05T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [],
popularItems: [createMockPopularItem()],
});
expect(screen.getByText("PopularSection")).toBeInTheDocument();
expect(
screen.getByText("Here are some popular tables"),
).toBeInTheDocument();
});
it("should render recent items for an existing user", () => {
setup({
it("should render recent items for an existing user", async () => {
await setup({
user: createMockUser({
is_installer: false,
has_question_and_dashboard: true,
......@@ -106,25 +108,24 @@ describe("HomeContent", () => {
recentItems: [createMockRecentItem()],
});
expect(screen.getByText("RecentSection")).toBeInTheDocument();
expect(screen.getByText("Pick up where you left off")).toBeInTheDocument();
});
it("should render x-rays for an installer after the setup", () => {
setup({
it("should render x-rays for an installer after the setup", async () => {
await setup({
user: createMockUser({
is_installer: true,
has_question_and_dashboard: false,
first_login: "2020-01-10T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [],
});
expect(screen.getByText("XraySection")).toBeInTheDocument();
expect(screen.getByText(/Here are some explorations/)).toBeInTheDocument();
});
it("should render x-rays for the installer when there is no question and dashboard", () => {
setup({
it("should render x-rays for the installer when there is no question and dashboard", async () => {
await setup({
user: createMockUser({
is_installer: true,
has_question_and_dashboard: false,
......@@ -134,11 +135,11 @@ describe("HomeContent", () => {
recentItems: [createMockRecentItem()],
});
expect(screen.getByText("XraySection")).toBeInTheDocument();
expect(screen.getByText(/Here are some explorations/)).toBeInTheDocument();
});
it("should not render x-rays for the installer when there is no question and dashboard if the x-rays feature is disabled", () => {
setup({
it("should not render x-rays for the installer when there is no question and dashboard if the x-rays feature is disabled", async () => {
await setup({
user: createMockUser({
is_installer: true,
has_question_and_dashboard: false,
......@@ -149,33 +150,22 @@ describe("HomeContent", () => {
isXrayEnabled: false,
});
expect(screen.queryByText("XraySection")).not.toBeInTheDocument();
});
it("should render nothing if there are no databases", () => {
setup({
user: createMockUser({
is_installer: true,
has_question_and_dashboard: false,
first_login: "2020-01-10T00:00:00Z",
}),
databases: [],
recentItems: [],
});
expect(screen.queryByText("XraySection")).not.toBeInTheDocument();
expect(
screen.queryByText(/Here are some explorations/),
).not.toBeInTheDocument();
});
it("should render loading state if there is not enough data to choose a section", () => {
setup({
it("should render nothing if there are no databases", async () => {
await setup({
user: createMockUser({
is_installer: true,
has_question_and_dashboard: false,
first_login: "2020-01-10T00:00:00Z",
}),
databases: undefined,
});
expect(screen.getByText("Loading...")).toBeInTheDocument();
expect(
screen.queryByText(/Here are some explorations/),
).not.toBeInTheDocument();
});
});
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./HomeContent";
export * from "./HomeContent";
import { useMemo } from "react";
import { connect } from "react-redux";
import { t } from "ttag";
import _ from "underscore";
import { useSelector } from "metabase/lib/redux";
import Tooltip from "metabase/core/components/Tooltip";
import { getUser } from "metabase/selectors/user";
import { getSetting } from "metabase/selectors/settings";
import { User } from "metabase-types/api";
import { State } from "metabase-types/store";
import { getHasMetabotLogo } from "../../selectors";
import {
GreetingLogo,
GreetingMessage,
GreetingRoot,
} from "./HomeGreeting.styled";
interface StateProps {
user: User | null;
showLogo?: boolean;
}
type HomeGreetingProps = StateProps;
const mapStateToProps = (state: State): StateProps => ({
user: getUser(state),
showLogo: getSetting(state, "show-metabot"),
});
const HomeGreeting = ({ user, showLogo }: HomeGreetingProps): JSX.Element => {
export const HomeGreeting = (): JSX.Element => {
const user = useSelector(getUser);
const showLogo = useSelector(getHasMetabotLogo);
const name = user?.first_name;
const message = useMemo(() => getMessage(name), [name]);
......@@ -56,6 +44,3 @@ const getMessage = (name: string | null | undefined): string => {
return _.sample(options) ?? "";
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default connect(mapStateToProps)(HomeGreeting);
......@@ -5,7 +5,7 @@ import {
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
import HomeGreeting from "./HomeGreeting";
import { HomeGreeting } from "./HomeGreeting";
interface SetupOpts {
currentUser?: User;
......
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./HomeGreeting";
export * from "./HomeGreeting";
......@@ -2,7 +2,7 @@ import { t } from "ttag";
import MetabaseSettings from "metabase/lib/settings";
import { CardIcon, CardRoot, CardTitle } from "./HomeHelpCard.styled";
const HomeHelpCard = (): JSX.Element => {
export const HomeHelpCard = (): JSX.Element => {
return (
<CardRoot href={MetabaseSettings.learnUrl()}>
<CardIcon name="reference" />
......@@ -10,6 +10,3 @@ const HomeHelpCard = (): JSX.Element => {
</CardRoot>
);
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default HomeHelpCard;
import { render, screen } from "@testing-library/react";
import HomeHelpCard from "./HomeHelpCard";
import { render, screen } from "__support__/ui";
import { HomeHelpCard } from "./HomeHelpCard";
const setup = () => {
render(<HomeHelpCard />);
};
describe("HomeHelpCard", () => {
it("should render correctly", () => {
render(<HomeHelpCard />);
setup();
expect(screen.getByText("Metabase tips")).toBeInTheDocument();
});
});
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./HomeHelpCard";
export * from "./HomeHelpCard";
import { ReactNode, useState } from "react";
import { connect } from "react-redux";
import { getSetting } from "metabase/selectors/settings";
import { useSelector } from "metabase/lib/redux";
import { getUserIsAdmin } from "metabase/selectors/user";
import MetabotWidget from "metabase/metabot/components/MetabotWidget";
import { State } from "metabase-types/store";
import Tooltip from "metabase/core/components/Tooltip/Tooltip";
import HomeGreeting from "../HomeGreeting";
import { CustomHomePageModal } from "../Modals/CustomHomePageModal/CustomHomePageModal";
import { HomeGreeting } from "../HomeGreeting";
import { getHasIllustration } from "../../selectors";
import { CustomHomePageModal } from "../CustomHomePageModal";
import {
LayoutBody,
LayoutEditButton,
......@@ -14,30 +13,18 @@ import {
LayoutRoot,
} from "./HomeLayout.styled";
interface OwnProps {
hasMetabot?: boolean;
interface HomeLayoutProps {
hasMetabot: boolean;
children?: ReactNode;
}
interface StateProps {
hasIllustration?: boolean;
isAdmin?: boolean;
}
type HomeLayoutProps = OwnProps & StateProps;
const mapStateToProps = (state: State) => ({
hasIllustration: getSetting(state, "show-lighthouse-illustration"),
isAdmin: getUserIsAdmin(state),
});
const HomeLayout = ({
export const HomeLayout = ({
hasMetabot,
hasIllustration,
children,
isAdmin,
}: HomeLayoutProps): JSX.Element => {
const [showModal, setShowModal] = useState(false);
const isAdmin = useSelector(getUserIsAdmin);
const hasIllustration = useSelector(getHasIllustration);
return (
<LayoutRoot>
......@@ -62,6 +49,3 @@ const HomeLayout = ({
</LayoutRoot>
);
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default connect(mapStateToProps)(HomeLayout);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment