Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Recents + Popular API Format - Frontend #42305

Merged
merged 12 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions frontend/src/metabase-types/api/activity.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
import type { CollectionId } from "./collection";
import type { DatabaseId } from "./database";
import type { UserId } from "./user";
import type { CardDisplayType } from "./card";
import type { InitialSyncStatus } from "./database";

export const ACTIVITY_MODELS = [
"table",
"card",
"dataset",
"dashboard",
"collection",
] as const;
export type ActivityModel = typeof ACTIVITY_MODELS[number];
export type ActivityModelId = number;

export interface ActivityModelObject {
export interface BaseRecentItem {
id: number;
name: string;
display_name?: string;
moderated_status?: string;
collection_id?: CollectionId | null;
collection_name?: string;
database_name?: string;
db_id?: DatabaseId;
model: ActivityModel;
description?: string | null;
timestamp: string;
}

export interface RecentItem {
cnt: number;
max_ts: string;
user_id: UserId;
model: ActivityModel;
model_id: ActivityModelId;
model_object: ActivityModelObject;
export interface RecentTableItem extends BaseRecentItem {
model: "table";
display_name: string;
database: {
id: number;
name: string;
initial_sync_status: InitialSyncStatus;
};
}

export interface PopularItem {
model: ActivityModel;
model_id: ActivityModelId;
model_object: ActivityModelObject;
export interface RecentCollectionItem extends BaseRecentItem {
model: "collection" | "dashboard" | "dataset" | "card";
parent_collection: {
id: number | null;
name: string;
authority_level?: "official" | null;
};
authority_level?: "official" | null; // for collections
moderated_status?: "verified" | null; // for models
display?: CardDisplayType; // for questions
}

export type RecentItem = RecentTableItem | RecentCollectionItem;

export interface RecentItemsResponse {
recent_views: RecentItem[];
}

export type PopularItem = RecentItem;

export interface PopularItemsResponse {
popular_items: PopularItem[];
}
2 changes: 1 addition & 1 deletion frontend/src/metabase-types/api/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export type UnrestrictedLinkEntity = {
model: SearchModel;
name: string;
display_name?: string;
description?: string;
description?: string | null;
display?: CardDisplayType;
};

Expand Down
53 changes: 32 additions & 21 deletions frontend/src/metabase-types/api/mocks/activity.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import type {
ActivityModelObject,
PopularItem,
RecentItem,
RecentTableItem,
RecentCollectionItem,
} from "metabase-types/api";

export const createMockModelObject = (
opts?: Partial<ActivityModelObject>,
): ActivityModelObject => ({
name: "Orders",
...opts,
});

export const createMockRecentItem = (
opts?: Partial<RecentItem>,
export const createMockRecentTableItem = (
opts?: Partial<RecentTableItem>,
): RecentItem => ({
id: 1,
model: "table",
model_id: 1,
model_object: createMockModelObject(),
cnt: 1,
max_ts: "2021-03-01T00:00:00.000Z",
user_id: 1,
name: "my_cool_table",
display_name: "My Cool Table",
timestamp: "2021-03-01T00:00:00.000Z",
database: {
id: 1,
name: "My Cool Collection",
initial_sync_status: "complete",
},
...opts,
});

export const createMockPopularItem = (
opts?: Partial<PopularItem>,
): PopularItem => ({
model: "table",
model_id: 1,
model_object: createMockModelObject(),
export const createMockRecentCollectionItem = (
opts?: Partial<RecentCollectionItem>,
): RecentItem => ({
id: 1,
model: "card",
name: "My Cool Question",
timestamp: "2021-03-01T00:00:00.000Z",
parent_collection: {
id: 1,
name: "My Cool Collection",
},
...opts,
});

export const createMockPopularTableItem = (
opts?: Partial<RecentTableItem>,
): PopularItem => createMockRecentTableItem(opts);

export const createMockPopularCollectionItem = (
opts?: Partial<RecentCollectionItem>,
): PopularItem => createMockRecentCollectionItem(opts);
15 changes: 12 additions & 3 deletions frontend/src/metabase/api/activity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { PopularItem, RecentItem } from "metabase-types/api";
import type {
RecentItem,
PopularItem,
RecentItemsResponse,
PopularItemsResponse,
} from "metabase-types/api";

import { Api } from "./api";
import { provideActivityItemListTags } from "./tags";
Expand All @@ -10,14 +15,18 @@ export const activityApi = Api.injectEndpoints({
method: "GET",
url: "/api/activity/recent_views",
}),
providesTags: (items = []) => provideActivityItemListTags(items),
transformResponse: (response: RecentItemsResponse) =>
response?.recent_views,
providesTags: items => provideActivityItemListTags(items ?? []),
}),
listPopularItems: builder.query<PopularItem[], void>({
query: () => ({
method: "GET",
url: "/api/activity/popular_items",
}),
providesTags: (items = []) => provideActivityItemListTags(items),
transformResponse: (response: PopularItemsResponse) =>
response?.popular_items,
providesTags: items => provideActivityItemListTags(items ?? []),
}),
}),
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase/api/tags/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function provideActivityItemListTags(
export function provideActivityItemTags(
item: RecentItem | PopularItem,
): TagDescription<TagType>[] {
return [idTag(TAG_TYPE_MAPPING[item.model], item.model_id)];
return [idTag(TAG_TYPE_MAPPING[item.model], item.id)];
}

export function provideAlertListTags(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMemo } from "react";

import {
useListRecentItemsQuery,
useListPopularItemsQuery,
Expand All @@ -14,20 +16,25 @@ import { getIsXrayEnabled } from "../../selectors";
import { isWithinWeeks } from "../../utils";
import { EmbedHomepage } from "../EmbedHomepage";
import { HomePopularSection } from "../HomePopularSection";
import { HomeRecentSection } from "../HomeRecentSection";
import { HomeRecentSection, recentsFilter } from "../HomeRecentSection";
import { HomeXraySection } from "../HomeXraySection";

export const HomeContent = (): JSX.Element | null => {
const user = useSelector(getUser);
const embeddingHomepage = useSetting("embedding-homepage");
const isXrayEnabled = useSelector(getIsXrayEnabled);
const { data: databases, error: databasesError } = useDatabaseListQuery();
const { data: recentItems, error: recentItemsError } =
const { data: recentItemsRaw, error: recentItemsError } =
useListRecentItemsQuery(undefined, { refetchOnMountOrArgChange: true });
const { data: popularItems, error: popularItemsError } =
useListPopularItemsQuery(undefined, { refetchOnMountOrArgChange: true });
const error = databasesError || recentItemsError || popularItemsError;

const recentItems = useMemo(
() => (recentItemsRaw && recentsFilter(recentItemsRaw)) ?? [],
[recentItemsRaw],
);

if (error) {
return <LoadingAndErrorWrapper error={error} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import type {
} from "metabase-types/api";
import {
createMockDatabase,
createMockPopularItem,
createMockRecentItem,
createMockPopularTableItem,
createMockRecentTableItem,
createMockUser,
} from "metabase-types/api/mocks";
import {
Expand Down Expand Up @@ -86,8 +86,8 @@ describe("HomeContent", () => {
first_login: "2020-01-05T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [createMockRecentItem()],
popularItems: [createMockPopularItem()],
recentItems: [createMockRecentTableItem()],
popularItems: [createMockPopularTableItem()],
});

expect(
Expand All @@ -103,7 +103,7 @@ describe("HomeContent", () => {
first_login: "2020-01-05T00:00:00Z",
}),
databases: [createMockDatabase()],
popularItems: [createMockPopularItem()],
popularItems: [createMockPopularTableItem()],
});

expect(
Expand All @@ -119,7 +119,7 @@ describe("HomeContent", () => {
first_login: "2020-01-01T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [createMockRecentItem()],
recentItems: [createMockRecentTableItem()],
});

expect(screen.getByText("Pick up where you left off")).toBeInTheDocument();
Expand All @@ -146,7 +146,7 @@ describe("HomeContent", () => {
first_login: "2020-01-10T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [createMockRecentItem()],
recentItems: [createMockRecentTableItem()],
});

expect(screen.getByText(/Here are some explorations/)).toBeInTheDocument();
Expand All @@ -160,7 +160,7 @@ describe("HomeContent", () => {
first_login: "2020-01-10T00:00:00Z",
}),
databases: [createMockDatabase()],
recentItems: [createMockRecentItem()],
recentItems: [createMockRecentTableItem()],
isXrayEnabled: false,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,8 @@ export const HomePopularSection = (): JSX.Element => {
{popularItems.map((item, index) => (
<HomeModelCard
key={index}
title={getName(item.model_object)}
icon={getIcon(
{ ...item.model_object, model: item.model },
{ variant: "secondary" },
)}
title={getName(item)}
icon={getIcon(item, { variant: "secondary" })}
url={Urls.modelToUrl(item) ?? ""}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@ import { screen } from "@testing-library/react";
import { setupPopularItemsEndpoints } from "__support__/server-mocks";
import { renderWithProviders, waitForLoaderToBeRemoved } from "__support__/ui";
import type { PopularItem } from "metabase-types/api";
import { createMockPopularItem } from "metabase-types/api/mocks";
import { createMockPopularCollectionItem } from "metabase-types/api/mocks";

import { HomePopularSection } from "./HomePopularSection";

interface SetupOpts {
popularItems: PopularItem[];
}

const samplePopularItems = [
createMockPopularCollectionItem({
model: "dashboard",
name: "Metrics",
}),
createMockPopularCollectionItem({
model: "dashboard",
name: "Revenue",
}),
createMockPopularCollectionItem({
model: "card",
name: "Orders",
}),
];
const setup = async ({ popularItems }: SetupOpts) => {
setupPopularItemsEndpoints(popularItems);
renderWithProviders(<HomePopularSection />);
Expand All @@ -20,41 +34,15 @@ const setup = async ({ popularItems }: SetupOpts) => {
describe("HomePopularSection", () => {
it("should render a list of items of the same type", async () => {
await setup({
popularItems: [
createMockPopularItem({
model: "dashboard",
model_object: {
name: "Metrics",
},
}),
createMockPopularItem({
model: "dashboard",
model_object: {
name: "Revenue",
},
}),
],
popularItems: samplePopularItems.slice(0, 2),
});

expect(screen.getByText(/popular dashboards/)).toBeInTheDocument();
});

it("should render a list of items of different types", async () => {
await setup({
popularItems: [
createMockPopularItem({
model: "dashboard",
model_object: {
name: "Metrics",
},
}),
createMockPopularItem({
model: "card",
model_object: {
name: "Revenue",
},
}),
],
popularItems: samplePopularItems,
});

expect(screen.getByText(/popular items/)).toBeInTheDocument();
Expand Down
Loading
Loading