Skip to content

Commit

Permalink
WIP - new recents format
Browse files Browse the repository at this point in the history
  • Loading branch information
iethree committed May 7, 2024
1 parent e2d7648 commit 934e2ea
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 72 deletions.
43 changes: 35 additions & 8 deletions frontend/src/metabase-types/api/activity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { CardDisplayType } from "./card";
import type { CollectionId } from "./collection";
import type { DatabaseId } from "./database";
import type { UserId } from "./user";
import type { DatabaseId, 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;
Expand All @@ -21,17 +22,43 @@ export interface ActivityModelObject {
db_id?: DatabaseId;
}

export interface RecentItem {
cnt: number;
max_ts: string;
user_id: UserId;
export interface BaseRecentItem {
id: number;
name: string;
model: ActivityModel;
model_id: ActivityModelId;
model_object: ActivityModelObject;
timestamp: string;
}

export interface RecentTableItem extends BaseRecentItem {
model: "table";
display_name: string;
database: {
id: number;
name: string;
initial_sync_status: InitialSyncStatus;
};
}

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 PopularItem {
model: ActivityModel;
model_id: ActivityModelId;
model_object: ActivityModelObject;
}

export interface RecentItemsResponse {
"recent-views": RecentItem[];
}
10 changes: 8 additions & 2 deletions frontend/src/metabase/api/activity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { PopularItem, RecentItem } from "metabase-types/api";
import type {
PopularItem,
RecentItemsResponse,
RecentItem,
} from "metabase-types/api";

import { Api } from "./api";
import { provideActivityItemListTags } from "./tags";
Expand All @@ -10,7 +14,9 @@ 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: () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getName } from "metabase/lib/name";
import { useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { getUser } from "metabase/selectors/user";
import type { RecentItem } from "metabase-types/api";

import { isWithinWeeks } from "../../utils";
import { HomeCaption } from "../HomeCaption";
Expand All @@ -33,14 +34,11 @@ export const HomeRecentSection = () => {
<div>
<HomeCaption>{t`Pick up where you left off`}</HomeCaption>
<SectionBody>
{recentItems.map((item, index) => (
{resultsFilter(recentItems).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 All @@ -49,3 +47,7 @@ export const HomeRecentSection = () => {
</div>
);
};

export const resultsFilter = (results: RecentItem[]): RecentItem[] => {
return results.filter(item => item.model !== "collection").slice(0, 5);
};
2 changes: 1 addition & 1 deletion frontend/src/metabase/lib/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {

export type ObjectWithModel = {
model: SearchModel;
authority_level?: string;
authority_level?: string | null;
display?: CardDisplayType;
type?: Collection["type"];
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase/lib/urls/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type DashboardUrlBuilderOpts = {
};

export function dashboard(
dashboard: Dashboard,
dashboard: Pick<Dashboard, "name" | "id">,
{ addCardWithId, editMode }: DashboardUrlBuilderOpts = {},
) {
const options = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/metabase/lib/urls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./collections";
export * from "./dashboards";
export * from "./indexed-entities";
export * from "./metabot";
export * from "./modelToUrl";
export * from "./misc";
export * from "./models";
export * from "./pulses";
Expand Down
34 changes: 0 additions & 34 deletions frontend/src/metabase/lib/urls/misc.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,6 @@
import { dashboard } from "./dashboards";
import { model } from "./models";
import { pulse } from "./pulses";
import { question, tableRowsQuery } from "./questions";

export const exportFormats = ["csv", "xlsx", "json"];
export const exportFormatPng = "png";

export function accountSettings() {
return "/account/profile";
}

function prepareModel(item) {
if (item.model_object) {
return item.model_object;
}
return {
id: item.model_id,
...item.details,
};
}

export function modelToUrl(item) {
const modelData = prepareModel(item);

switch (item.model) {
case "card":
return question(modelData);
case "dataset":
return model(modelData);
case "dashboard":
return dashboard(modelData);
case "pulse":
return pulse(modelData.id);
case "table":
return tableRowsQuery(modelData.db_id, modelData.id);
default:
return null;
}
}
29 changes: 29 additions & 0 deletions frontend/src/metabase/lib/urls/modelToUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { RecentItem } from "metabase-types/api";

import { collection } from "./collections";
import { dashboard } from "./dashboards";
import { model } from "./models";
import { question, tableRowsQuery } from "./questions";

export function modelToUrl(item: RecentItem) {
switch (item.model) {
case "card":
return question({
...item,
model: "card", // somehow typescript is not smart enough to infer this
});
case "dataset":
return model(item);
case "dashboard":
return dashboard(item);
case "table":
if (item?.database) {
return tableRowsQuery(item?.database?.id, item.id);
}
return null;
case "collection":
return collection(item);
default:
return null;
}
}
87 changes: 87 additions & 0 deletions frontend/src/metabase/lib/urls/modelToUrl.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { modelToUrl } from "./modelToUrl";
describe("urls > modelToUrl", () => {
it("should return null for unknown model", () => {
expect(
modelToUrl({
// @ts-expect-error - testing the error case
model: "pikachu",
}),
).toBeNull();
});

it("should return a question URL for a card", () => {
expect(
modelToUrl({
model: "card",
name: "My Cool Question",
id: 101,
parent_collection: {
id: 1,
name: "My Cool Collection",
},
timestamp: "2021-01-01T00:00:00.000Z",
}),
).toBe("/question/101-my-cool-question");
});

it("should return a model URL for a dataset", () => {
expect(
modelToUrl({
model: "dataset",
name: "My Cool Dataset",
parent_collection: {
id: 1,
name: "My Cool Collection",
},
id: 101,
timestamp: "2021-01-01T00:00:00.000Z",
}),
).toBe("/model/101-my-cool-dataset");
});

it("should return a dashboard URL for a dashboard", () => {
expect(
modelToUrl({
model: "dashboard",
name: "My Cool Dashboard",
parent_collection: {
id: 1,
name: "My Cool Collection",
},
id: 101,
timestamp: "2021-01-01T00:00:00.000Z",
}),
).toBe("/dashboard/101-my-cool-dashboard");
});

it("should return a collection URL for a collection", () => {
expect(
modelToUrl({
model: "collection",
name: "My Cool Collection",
id: 1,
parent_collection: {
id: 1,
name: "My Cool Collection",
},
timestamp: "2021-01-01T00:00:00.000Z",
}),
).toBe("/collection/1-my-cool-collection");
});

it("should return a table URL for a table", () => {
expect(
modelToUrl({
model: "table",
name: "MY_COOL_TABLE",
display_name: "My Cool Table",
id: 33,
database: {

Check failure on line 79 in frontend/src/metabase/lib/urls/modelToUrl.unit.spec.ts

View workflow job for this annotation

GitHub Actions / fe-type-check

Property 'initial_sync_status' is missing in type '{ id: number; name: string; }' but required in type '{ id: number; name: string; initial_sync_status: InitialSyncStatus; }'.
id: 22,
name: "My Cool Database",
},
timestamp: "2021-01-01T00:00:00.000Z",
}),
).toBe("/question/#?db=22&table=33");
});
});
2 changes: 1 addition & 1 deletion frontend/src/metabase/lib/urls/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type QuestionUrlBuilderParams = {
};

export function question(
card: Card | null,
card: Pick<Card, "id" | "name" | "model" | "type" | "card_id"> | null,
{
mode = "view",
hash = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { SearchResultLink } from "metabase/search/components/SearchResultLink";
import { Group, Loader, Stack, Title } from "metabase/ui";
import type { RecentItem } from "metabase-types/api";

import { getItemUrl, isItemActive } from "./util";
import { getItemUrl, isItemActive, resultsFilter } from "./util";

type RecentsListContentProps = {
isLoading: boolean;
Expand Down Expand Up @@ -67,7 +67,7 @@ export const RecentsListContent = ({
>
<Title order={4} px="sm">{t`Recently viewed`}</Title>
<Stack spacing={0}>
{results.map((item, index) => {
{resultsFilter(results).map((item, index) => {
const isActive = isItemActive(item);

return (
Expand All @@ -89,7 +89,7 @@ export const RecentsListContent = ({
truncate
href={onClick ? undefined : getItemUrl(item)}
>
{getName(item.model_object)}
{getName(item)}
</ResultTitle>
<PLUGIN_MODERATION.ModerationStatusIcon
status={getModeratedStatus(item)}
Expand All @@ -114,17 +114,20 @@ export const RecentsListContent = ({
);
};

const getItemKey = ({ model, model_id }: RecentItem) => {
return `${model}:${model_id}`;
const getItemKey = ({ model, id }: RecentItem) => {
return `${model}:${id}`;
};

const getModeratedStatus = ({ model_object }: RecentItem) => {
return model_object.moderated_status;
const getModeratedStatus = (item: RecentItem) => {
return item.model !== "table" && item.moderated_status;
};

const isItemLoading = ({ model, model_object }: RecentItem) => {
if (model !== "table") {
const isItemLoading = (item: RecentItem) => {
if (item.model !== "table") {
return false;
}
return !isSyncCompleted(model_object);
if (!item.database) {
return false;
}
return !isSyncCompleted(item.database);
};
10 changes: 7 additions & 3 deletions frontend/src/metabase/nav/components/search/RecentsList/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { isSyncCompleted } from "metabase/lib/syncing";
import * as Urls from "metabase/lib/urls";
import type { RecentItem } from "metabase-types/api";

export const isItemActive = ({ model, model_object }: RecentItem) => {
if (model !== "table") {
export const isItemActive = (item: RecentItem) => {
if (item.model !== "table") {
return true;
}
return isSyncCompleted(model_object);
return isSyncCompleted(item);
};

export const getItemUrl = (item: RecentItem) => {
const url = isItemActive(item) && Urls.modelToUrl(item);
return url || undefined;
};

export const resultsFilter = (results: RecentItem[]): RecentItem[] => {
return results.filter(item => item.model !== "collection").slice(0, 5);
};
Loading

0 comments on commit 934e2ea

Please sign in to comment.