Skip to content

Commit

Permalink
New Recents + Popular API Format - Frontend (#42305)
Browse files Browse the repository at this point in the history
* fix tests

* new API payload format for recents and populars

* update url generation

* update recents components

* update tests

* only show 5 recents in command palette

* remove unused types

* update tests

* obey the linter

* type updates

* fix unit test

* change where we filter recents

---------

Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
  • Loading branch information
iethree and escherize committed May 10, 2024
1 parent 2c29491 commit f878b56
Show file tree
Hide file tree
Showing 27 changed files with 362 additions and 245 deletions.
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

0 comments on commit f878b56

Please sign in to comment.