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

Add popular section to the new homepage (#21429)

parent b5bc0c05
No related branches found
No related tags found
No related merge requests found
Showing
with 308 additions and 66 deletions
export type ModelType = "table" | "card" | "dataset" | "dashboard";
export interface ModelObject {
name: string;
}
export interface RecentView {
model: string;
model_object: RecentModelObject;
model: ModelType;
model_object: ModelObject;
}
export interface RecentModelObject {
name: string;
export interface PopularView {
model: ModelType;
model_object: ModelObject;
}
import { RecentModelObject, RecentView } from "metabase-types/api";
import { ModelObject, PopularView, RecentView } from "metabase-types/api";
export const createMockModelObject = (
opts?: Partial<ModelObject>,
): ModelObject => ({
name: "Orders",
...opts,
});
export const createMockRecentView = (
opts?: Partial<RecentView>,
): RecentView => ({
model: "table",
model_object: createMockRecentModelObject(),
model_object: createMockModelObject(),
...opts,
});
export const createMockRecentModelObject = (
opts?: Partial<RecentModelObject>,
): RecentModelObject => ({
name: "Orders",
export const createMockPopularView = (
opts?: Partial<PopularView>,
): PopularView => ({
model: "table",
model_object: createMockModelObject(),
...opts,
});
import { User } from "metabase-types/api";
export const createMockUser = ({
id = 1,
first_name = "Testy",
last_name = "Tableton",
common_name = `${first_name} ${last_name}`,
email = "user@metabase.test",
google_auth = false,
is_active = true,
is_qbnewb = false,
is_superuser = false,
has_invited_second_user = false,
personal_collection_id = 1,
date_joined = new Date().toISOString(),
last_login = new Date().toISOString(),
can_access_data_model = false,
can_access_database_management = false,
}: Partial<User> = {}): User => ({
id,
common_name,
first_name,
last_name,
email,
google_auth,
is_active,
is_qbnewb,
is_superuser,
has_invited_second_user,
personal_collection_id,
date_joined,
last_login,
can_access_data_model,
can_access_database_management,
export const createMockUser = (opts?: Partial<User>): User => ({
id: 1,
first_name: "Testy",
last_name: "Tableton",
common_name: `Testy Tableton`,
email: "user@metabase.test",
google_auth: false,
is_active: true,
is_qbnewb: false,
is_superuser: false,
is_installer: false,
has_invited_second_user: false,
has_question_and_dashboard: false,
personal_collection_id: 1,
date_joined: new Date().toISOString(),
last_login: new Date().toISOString(),
can_access_data_model: false,
can_access_database_management: false,
...opts,
});
export interface User {
id: number;
common_name: string;
first_name: string;
last_name: string;
common_name: string;
email: string;
google_auth: boolean;
is_active: boolean;
is_qbnewb: boolean;
is_superuser: boolean;
is_installer: boolean;
has_invited_second_user: boolean;
has_question_and_dashboard: boolean;
date_joined: string;
last_login: string;
has_invited_second_user: boolean;
personal_collection_id: number;
can_access_data_model: boolean;
can_access_database_management: boolean;
......
......@@ -24,5 +24,6 @@ export { default as users } from "./users";
export { default as groups } from "./groups";
export { default as search } from "./search";
export { default as recents } from "./recents";
export { default as popularViews } from "./popular-views";
export { default as recentViews } from "./recent-views";
export { default as snippets } from "./snippets";
import { createEntity } from "metabase/lib/entities";
import { entityTypeForObject } from "metabase/lib/schema";
import { PopularViewsSchema } from "metabase/schema";
export const getEntity = item => {
const entities = require("metabase/entities");
return entities[entityTypeForObject(item)];
};
export const getName = item => {
return item.model_object.display_name || item.model_object.name;
};
export const getIcon = item => {
const entity = getEntity(item);
return entity.objectSelectors.getIcon(item.model_object);
};
const PopularViews = createEntity({
name: "popularViews",
nameOne: "popularView",
path: "/api/activity/popular_views",
schema: PopularViewsSchema,
wrapEntity(item, dispatch = null) {
const entity = getEntity(item);
return entity.wrapEntity(item, dispatch);
},
objectSelectors: {
getName,
getIcon,
},
});
export default PopularViews;
import { createEntity } from "metabase/lib/entities";
import { entityTypeForObject } from "metabase/lib/schema";
import { RecentsSchema } from "metabase/schema";
import { RecentViewsSchema } 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 Recents = createEntity({
name: "recents",
nameOne: "recent",
const RecentViews = createEntity({
name: "recentViews",
nameOne: "recentView",
path: "/api/activity/recent_views",
schema: RecentsSchema,
schema: RecentViewsSchema,
wrapEntity(item, dispatch = null) {
const entity = getEntity(item);
......@@ -33,4 +33,4 @@ const Recents = createEntity({
},
});
export default Recents;
export default RecentViews;
......@@ -3,6 +3,8 @@ import { alpha, color } from "metabase/lib/colors";
import Link from "metabase/core/components/Link";
export const CardRoot = styled(Link)`
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid ${color("border")};
border-radius: 0.5rem;
......
......@@ -4,6 +4,7 @@ import { CardRoot } from "./HomeCard.styled";
export interface HomeCardProps {
className?: string;
url?: string;
external?: boolean;
children?: ReactNode;
}
......
import React from "react";
import moment from "moment";
import { parseTimestamp } from "metabase/lib/time";
import { isSyncCompleted } from "metabase/lib/syncing";
import { Database, RecentView } from "metabase-types/api";
import RecentSection from "../../containers/RecentSection";
import XraySection from "../../containers/XraySection";
import { Database, RecentView, User } from "metabase-types/api";
import HomePopularSection from "../../containers/HomePopularSection";
import HomeRecentSection from "../../containers/HomeRecentSection";
import HomeXraySection from "../../containers/HomeXraySection";
export interface HomeContentProps {
user: User;
databases: Database[];
recents: RecentView[];
recentViews: RecentView[];
}
const HomeContent = ({
databases,
recents,
}: HomeContentProps): JSX.Element | null => {
if (recents.length) {
return <RecentSection />;
} else if (databases.some(isSyncCompleted)) {
return <XraySection />;
} else {
return null;
const HomeContent = (props: HomeContentProps): JSX.Element | null => {
if (isPopularSection(props)) {
return <HomePopularSection />;
}
if (isRecentSection(props)) {
return <HomeRecentSection />;
}
if (isXraySection(props)) {
return <HomeXraySection />;
}
return null;
};
const isPopularSection = ({ user, recentViews }: HomeContentProps) => {
return (
!user.is_installer &&
user.has_question_and_dashboard &&
(isWithinWeek(user.date_joined) || !recentViews.length)
);
};
const isRecentSection = ({ recentViews }: HomeContentProps) => {
return recentViews.length > 0;
};
const isXraySection = ({ databases }: HomeContentProps) => {
return databases.some(isSyncCompleted);
};
const isWithinWeek = (timestamp: string) => {
const date = parseTimestamp(timestamp);
const today = moment();
const weekAgo = today.clone().subtract(1, "week");
return date.isBetween(weekAgo, today);
};
export default HomeContent;
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import ExternalLink from "metabase/core/components/ExternalLink";
export const CardRoot = styled(ExternalLink)`
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid ${color("focus")};
border-radius: 0.5rem;
`;
export const CardIcon = styled(Icon)`
display: block;
flex: 0 0 auto;
color: ${color("text-dark")};
width: 1rem;
height: 1rem;
`;
export const CardTitle = styled.div`
color: ${color("text-dark")};
font-size: 1rem;
font-weight: bold;
margin-left: 1rem;
`;
import React from "react";
import { t } from "ttag";
import { CardIcon, CardRoot, CardTitle } from "./HomeHelpCard.styled";
const HomeHelpCard = (): JSX.Element => {
return (
<CardRoot href="https://www.metabase.com/learn/">
<CardIcon name="reference" />
<CardTitle>{t`Metabase tips`}</CardTitle>
</CardRoot>
);
};
export default HomeHelpCard;
export { default } from "./HomeHelpCard";
......@@ -23,7 +23,7 @@ const gradientStyles = css`
`;
export const LayoutRoot = styled.div<LayoutProps>`
height: 100%;
min-height: 100%;
padding: 4rem 7rem;
${props => (props.showScene ? sceneStyles : gradientStyles)};
`;
......
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import Ellipsified from "metabase/components/Ellipsified";
export const CardIcon = styled(Icon)`
display: block;
flex: 0 0 auto;
color: ${color("brand")};
width: 1rem;
height: 1rem;
`;
export const CardTitle = styled(Ellipsified)`
color: ${color("text-dark")};
font-size: 1rem;
font-weight: bold;
margin-left: 1rem;
`;
import React from "react";
import HomeCard from "../HomeCard";
import { CardIcon, CardTitle } from "./HomeModelCard.styled";
export interface HomeModelCardProps {
title: string;
icon: HomeModelIconProps;
url: string;
}
export interface HomeModelIconProps {
name: string;
}
const HomeModelCard = ({
title,
icon,
url,
}: HomeModelCardProps): JSX.Element => {
return (
<HomeCard url={url}>
<CardIcon {...icon} />
<CardTitle>{title}</CardTitle>
</HomeCard>
);
};
export default HomeModelCard;
export { default } from "./HomeModelCard";
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;
gap: 1.5rem;
flex-wrap: wrap;
`;
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 HomeHelpCard from "../HomeHelpCard";
import HomeModelCard from "../HomeModelCard";
import { SectionBody, SectionTitle } from "./HomePopularSection.styled";
export interface HomePopularSectionProps {
popularViews: PopularView[];
}
const HomePopularSection = ({
popularViews,
}: HomePopularSectionProps): JSX.Element => {
return (
<div>
<SectionTitle>{getTitle(popularViews)}</SectionTitle>
<SectionBody>
{popularViews.map((item, index) => (
<HomeModelCard
key={index}
title={getName(item)}
icon={getIcon(item)}
url={Urls.modelToUrl(item) ?? ""}
/>
))}
<HomeHelpCard />
</SectionBody>
</div>
);
};
const getTitle = (popularViews: PopularView[]) => {
const models = _.uniq(popularViews.map(item => item.model));
if (models.length !== 1) {
return t`Here is some popular stuff`;
}
switch (models[0]) {
case "table":
return t`Here are some popular tables`;
case "card":
return t`Here are some popular questions`;
case "dataset":
return t`Here are some popular models`;
case "dashboard":
return t`Here are some popular dashboards`;
default:
return t`Here is some popular stuff`;
}
};
export default HomePopularSection;
export { default } from "./HomePopularSection";
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