From 3c6429b8e54402a6141c0973693bee007a96e01b Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Wed, 25 Mar 2026 16:14:40 -0400 Subject: [PATCH 1/3] Adds new dashboard view --- knip.json | 3 +- src/components/Home/RunSection/RunSection.tsx | 18 +- src/hooks/useRecentlyViewed.ts | 2 +- src/routes/Dashboard/DashboardHomeView.tsx | 200 ++++++++++++++++++ src/routes/Dashboard/DashboardLayout.tsx | 7 +- .../Dashboard/DashboardRecentlyViewedView.tsx | 50 ++++- src/routes/router.ts | 5 +- 7 files changed, 269 insertions(+), 16 deletions(-) create mode 100644 src/routes/Dashboard/DashboardHomeView.tsx diff --git a/knip.json b/knip.json index 4353923aa..97a0043f4 100644 --- a/knip.json +++ b/knip.json @@ -5,8 +5,7 @@ "src/api/**", "src/components/ui/**", "src/config/announcements.ts", - "src/components/shared/BetaFeatureWrapper/BetaFeatureWrapper.tsx", - "src/components/Home/FavoritesSection/FavoritesSection.tsx" + "src/components/shared/BetaFeatureWrapper/BetaFeatureWrapper.tsx" ], "ignoreDependencies": [ "@radix-ui/react-accordion", diff --git a/src/components/Home/RunSection/RunSection.tsx b/src/components/Home/RunSection/RunSection.tsx index 0019a68d0..4ffa2ed16 100644 --- a/src/components/Home/RunSection/RunSection.tsx +++ b/src/components/Home/RunSection/RunSection.tsx @@ -43,9 +43,18 @@ interface RunSectionProps { onEmptyList?: () => void; /** When true, hides the built-in filter UI (used when new filter bar is enabled) */ hideFilters?: boolean; + /** When provided, overrides the URL filter param (e.g. "created_by:me") */ + forcedFilter?: string; + /** When provided, limits the number of rows shown (pagination still works per backend page) */ + maxItems?: number; } -export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => { +export const RunSection = ({ + onEmptyList, + hideFilters, + forcedFilter, + maxItems, +}: RunSectionProps) => { const { backendUrl, configured, available, ready } = useBackend(); const navigate = useNavigate(); const { pathname } = useLocation(); @@ -54,7 +63,7 @@ export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => { const dataVersion = useRef(0); // Supports both JSON (new) and key:value (legacy) URL formats - const filters = parseFilterParam(search.filter); + const filters = parseFilterParam(forcedFilter ?? search.filter); const createdByValue = filters.created_by; const apiFilterQuery = filtersToFilterQuery(filters); @@ -281,7 +290,10 @@ export const RunSection = ({ onEmptyList, hideFilters }: RunSectionProps) => { - {data.pipeline_runs?.map((run) => ( + {(maxItems + ? data.pipeline_runs?.slice(0, maxItems) + : data.pipeline_runs + )?.map((run) => ( ))} diff --git a/src/hooks/useRecentlyViewed.ts b/src/hooks/useRecentlyViewed.ts index a149dfbcd..64c5024ed 100644 --- a/src/hooks/useRecentlyViewed.ts +++ b/src/hooks/useRecentlyViewed.ts @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { getStorage } from "@/utils/typedStorage"; const RECENTLY_VIEWED_KEY = "Home/recently_viewed"; -const MAX_ITEMS = 10; +const MAX_ITEMS = 100; type RecentlyViewedType = "pipeline" | "run" | "component"; diff --git a/src/routes/Dashboard/DashboardHomeView.tsx b/src/routes/Dashboard/DashboardHomeView.tsx new file mode 100644 index 000000000..f633285a0 --- /dev/null +++ b/src/routes/Dashboard/DashboardHomeView.tsx @@ -0,0 +1,200 @@ +import { Link } from "@tanstack/react-router"; + +import { RunSection } from "@/components/Home/RunSection/RunSection"; +import { AnnouncementBanners } from "@/components/shared/AnnouncementBanners"; +import { Button } from "@/components/ui/button"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Paragraph, Text } from "@/components/ui/typography"; +import { type FavoriteItem, useFavorites } from "@/hooks/useFavorites"; +import { + type RecentlyViewedItem, + useRecentlyViewed, +} from "@/hooks/useRecentlyViewed"; +import { APP_ROUTES, EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router"; +import { formatRelativeTime } from "@/utils/date"; + +const PREVIEW_COUNT = 5; + +function getRecentlyViewedUrl(item: RecentlyViewedItem): string { + if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`; + if (item.type === "run") return `${RUNS_BASE_PATH}/${item.id}`; + return APP_ROUTES.DASHBOARD_COMPONENTS; +} + +function getFavoriteUrl(item: FavoriteItem): string { + if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`; + return `${RUNS_BASE_PATH}/${item.id}`; +} + +const TypePill = ({ type }: { type: "pipeline" | "run" | "component" }) => ( + + + {type === "pipeline" ? "Pipeline" : type === "run" ? "Run" : "Component"} + +); + +interface SectionHeaderProps { + title: string; + viewAllTo: string; + viewAllLabel?: string; +} + +const SectionHeader = ({ + title, + viewAllTo, + viewAllLabel = "View all", +}: SectionHeaderProps) => ( + + + {title} + + + {viewAllLabel} → + + +); + +const FavoritesPreview = () => { + const { favorites, removeFavorite } = useFavorites(); + const preview = favorites.slice(-PREVIEW_COUNT).reverse(); + + return ( + + +
+ {preview.length === 0 ? ( +
+ + No favorites yet. Star a pipeline or run to pin it here. + +
+ ) : ( + preview.map((item, i) => ( + + + + + + {item.name} + + + {item.name} + + + + )) + )} +
+
+ ); +}; + +const RecentlyViewedPreview = () => { + const { recentlyViewed } = useRecentlyViewed(); + const preview = recentlyViewed.slice(0, PREVIEW_COUNT); + + return ( + + +
+ {preview.length === 0 ? ( +
+ + Nothing viewed yet. Open a pipeline or run to see it here. + +
+ ) : ( + preview.map((item, i) => ( + + + + + + {item.name} + + + {item.name} + + + {formatRelativeTime(new Date(item.viewedAt))} + + + )) + )} +
+
+ ); +}; + +export function DashboardHomeView() { + return ( + + + +
+ + +
+
+ + + + {/* + Fetching 10 records because the API does not yet support a custom page_size. + Once TangleML/tangle#188 lands, reduce this to match the visible row count. + Tracked in TangleML/tangle-ui#2016. + */} + + + + ); +} diff --git a/src/routes/Dashboard/DashboardLayout.tsx b/src/routes/Dashboard/DashboardLayout.tsx index abe4a345a..bfb235d63 100644 --- a/src/routes/Dashboard/DashboardLayout.tsx +++ b/src/routes/Dashboard/DashboardLayout.tsx @@ -22,11 +22,13 @@ interface SidebarItem { to: string; label: string; icon: IconName; + exact?: boolean; } const SIDEBAR_ITEMS: SidebarItem[] = [ - { to: "/runs", label: "Runs", icon: "Play" }, - { to: "/pipelines", label: "Pipelines", icon: "GitBranch" }, + { to: "/", label: "My Dashboard", icon: "LayoutDashboard", exact: true }, + { to: "/pipelines", label: "My Pipelines", icon: "GitBranch" }, + { to: "/runs", label: "All Runs", icon: "Play" }, { to: "/components", label: "Components", icon: "Package" }, { to: "/favorites", label: "Favorites", icon: "Star" }, { to: "/recently-viewed", label: "Recently Viewed", icon: "Clock" }, @@ -67,6 +69,7 @@ export function DashboardLayout() { to={item.to} className="w-full" activeProps={{ className: "is-active" }} + activeOptions={item.exact ? { exact: true } : undefined} > {({ isActive }) => ( { export function DashboardRecentlyViewedView() { const { recentlyViewed } = useRecentlyViewed(); + const [page, setPage] = useState(0); + + const totalPages = Math.ceil(recentlyViewed.length / PAGE_SIZE); + const safePage = Math.min(page, Math.max(0, totalPages - 1)); + const paginated = recentlyViewed.slice( + safePage * PAGE_SIZE, + (safePage + 1) * PAGE_SIZE, + ); return ( @@ -70,11 +82,39 @@ export function DashboardRecentlyViewedView() { Nothing viewed yet. Open a pipeline or run to see it here. ) : ( -
- {recentlyViewed.map((item) => ( - - ))} -
+ +
+ {paginated.map((item) => ( + + ))} +
+ + {totalPages > 1 && ( + + + + {safePage + 1} / {totalPages} + + + + )} +
)}
); diff --git a/src/routes/router.ts b/src/routes/router.ts index 25cd61133..d7ac791c2 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -18,6 +18,7 @@ import { BASE_URL, IS_GITHUB_PAGES } from "@/utils/constants"; import RootLayout from "../components/layout/RootLayout"; import { DashboardFavoritesView } from "./Dashboard/DashboardFavoritesView"; +import { DashboardHomeView } from "./Dashboard/DashboardHomeView"; import { DashboardLayout } from "./Dashboard/DashboardLayout"; import { DashboardPipelinesView } from "./Dashboard/DashboardPipelinesView"; import { DashboardRecentlyViewedView } from "./Dashboard/DashboardRecentlyViewedView"; @@ -103,9 +104,7 @@ const dashboardRoute = createRoute({ const dashboardIndexRoute = createRoute({ getParentRoute: () => dashboardRoute, path: "/", - beforeLoad: () => { - throw redirect({ to: APP_ROUTES.DASHBOARD_RUNS }); - }, + component: DashboardHomeView, }); // Placeholder component — replaced in subsequent PRs From 8e2d01b241dd9e4a9067c8badf1fd845bbea7393 Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Thu, 9 Apr 2026 16:28:36 -0400 Subject: [PATCH 2/3] address pr feedback --- .../Dashboard/DashboardRecentlyViewedView.tsx | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx index c187e7353..fe75c407d 100644 --- a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx +++ b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx @@ -24,38 +24,40 @@ const RecentlyViewedCard = ({ item }: { item: RecentlyViewedItem }) => { const isPipeline = item.type === "pipeline"; return ( - - {/* Type pill + timestamp */} - - - - - {isPipeline ? "Pipeline" : "Run"} - + + + {/* Type pill + timestamp */} + + + + + {isPipeline ? "Pipeline" : "Run"} + + + + {formatRelativeTime(new Date(item.viewedAt))} + - - {formatRelativeTime(new Date(item.viewedAt))} - - - {/* Name */} - - {item.name} - + {/* Name */} + + {item.name} + - {/* ID */} - - {item.id} - + {/* ID */} + + {item.id} + + ); }; From e073068a7a9b5f6d113d50fb295922c6ed1d59a1 Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Thu, 9 Apr 2026 16:37:38 -0400 Subject: [PATCH 3/3] address pr feedback --- .../FavoritesSection/FavoritesSection.tsx | 168 ----------------- .../Dashboard/DashboardFavoritesView.tsx | 4 +- src/routes/Dashboard/DashboardHomeView.tsx | 173 +++++++++++------- .../Dashboard/DashboardRecentlyViewedView.tsx | 2 +- 4 files changed, 110 insertions(+), 237 deletions(-) delete mode 100644 src/components/Home/FavoritesSection/FavoritesSection.tsx diff --git a/src/components/Home/FavoritesSection/FavoritesSection.tsx b/src/components/Home/FavoritesSection/FavoritesSection.tsx deleted file mode 100644 index 9493f2a8a..000000000 --- a/src/components/Home/FavoritesSection/FavoritesSection.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { Input } from "@/components/ui/input"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Heading, Paragraph, Text } from "@/components/ui/typography"; -import { - type FavoriteItem, - type FavoriteType, - useFavorites, -} from "@/hooks/useFavorites"; -import { cn } from "@/lib/utils"; -import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router"; - -const PAGE_SIZE = 10; - -function getFavoriteUrl(item: FavoriteItem): string { - if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`; - return `${RUNS_BASE_PATH}/${item.id}`; -} - -const FavoriteChip = ({ - item, - onRemove, -}: { - item: FavoriteItem; - onRemove: (type: FavoriteType, id: string) => void; -}) => ( - - - - {item.name} - - - -); - -export const FavoritesSection = () => { - const { favorites, removeFavorite } = useFavorites(); - const [page, setPage] = useState(0); - const [query, setQuery] = useState(""); - - const normalizedQuery = query.trim().toLowerCase(); - const filtered = normalizedQuery - ? favorites.filter( - (favorite) => - favorite.id.toLowerCase().includes(normalizedQuery) || - favorite.name.toLowerCase().includes(normalizedQuery), - ) - : favorites; - - const totalPages = Math.ceil(filtered.length / PAGE_SIZE); - const safePage = Math.min(page, Math.max(0, totalPages - 1)); - const paginated = filtered.slice( - safePage * PAGE_SIZE, - (safePage + 1) * PAGE_SIZE, - ); - - return ( - - - - Favorites - - - {favorites.length === 0 ? ( - - No favorites yet. Star a pipeline or run to pin it here. - - ) : ( - -
- - { - setPage(0); - setQuery(e.target.value); - }} - className="pl-9 pr-8 w-full" - /> - {query && ( - - )} -
- - - {paginated.map((item) => ( - - ))} - - - {totalPages > 1 && ( - - - - {safePage + 1} / {totalPages} - - - - )} -
- )} -
- ); -}; diff --git a/src/routes/Dashboard/DashboardFavoritesView.tsx b/src/routes/Dashboard/DashboardFavoritesView.tsx index cf808afde..6476015de 100644 --- a/src/routes/Dashboard/DashboardFavoritesView.tsx +++ b/src/routes/Dashboard/DashboardFavoritesView.tsx @@ -10,7 +10,7 @@ import { type FavoriteItem, useFavorites } from "@/hooks/useFavorites"; import { cn } from "@/lib/utils"; import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router"; -const PAGE_SIZE = 16; +const PAGE_SIZE = 20; function getFavoriteUrl(item: FavoriteItem): string { if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`; @@ -25,7 +25,7 @@ const FavoriteCard = ({ item }: { item: FavoriteItem }) => { return ( {/* Remove button */} + +); + +const RecentlyViewedPreviewRow = ({ item }: { item: RecentlyViewedItem }) => ( + + + + + + {item.name} + + + {item.name} + + + {formatRelativeTime(new Date(item.viewedAt))} + + +); + const FavoritesPreview = () => { const { favorites, removeFavorite } = useFavorites(); - const preview = favorites.slice(-PREVIEW_COUNT).reverse(); + const preview = favorites.slice(0, PREVIEW_COUNT); return ( @@ -79,7 +161,7 @@ const FavoritesPreview = () => { title="Favorites" viewAllTo={APP_ROUTES.DASHBOARD_FAVORITES} /> -
+
{preview.length === 0 ? (
@@ -87,37 +169,12 @@ const FavoritesPreview = () => {
) : ( - preview.map((item, i) => ( - ( + - - - - - {item.name} - - - {item.name} - - - + item={item} + onRemove={() => removeFavorite(item.type, item.id)} + /> )) )}
@@ -135,7 +192,7 @@ const RecentlyViewedPreview = () => { title="Recently Viewed" viewAllTo={APP_ROUTES.DASHBOARD_RECENTLY_VIEWED} /> -
+
{preview.length === 0 ? (
@@ -143,27 +200,11 @@ const RecentlyViewedPreview = () => {
) : ( - preview.map((item, i) => ( - ( + - - - - - {item.name} - - - {item.name} - - - {formatRelativeTime(new Date(item.viewedAt))} - - + item={item} + /> )) )}
diff --git a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx index fe75c407d..4adedbdfa 100644 --- a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx +++ b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx @@ -27,7 +27,7 @@ const RecentlyViewedCard = ({ item }: { item: RecentlyViewedItem }) => { {/* Type pill + timestamp */}