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/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} - - { - e.preventDefault(); - e.stopPropagation(); - onRemove(item.type, item.id); - }} - className="shrink-0 opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground" - aria-label="Remove from favorites" - > - - - -); - -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 && ( - { - setPage(0); - setQuery(""); - }} - className="absolute right-2 top-1/2 -translate-y-1/2 size-6 text-muted-foreground hover:text-foreground" - aria-label="Clear search" - > - - - )} - - - - {paginated.map((item) => ( - - ))} - - - {totalPages > 1 && ( - - setPage(safePage - 1)} - > - - - - {safePage + 1} / {totalPages} - - = totalPages - 1} - onClick={() => setPage(safePage + 1)} - > - - - - )} - - )} - - ); -}; 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/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 */} = { + pipeline: { + className: "bg-violet-100 text-violet-700", + icon: "GitBranch", + label: "Pipeline", + }, + run: { + className: "bg-emerald-100 text-emerald-700", + icon: "Play", + label: "Run", + }, + component: { + className: "bg-blue-100 text-blue-700", + icon: "Package", + label: "Component", + }, +}; + +const TypePill = ({ type }: { type: ItemType }) => { + const config = TYPE_CONFIG[type]; + return ( + + + {config.label} + + ); +}; + +interface SectionHeaderProps { + title: string; + viewAllTo: string; + viewAllLabel?: string; +} + +const SectionHeader = ({ + title, + viewAllTo, + viewAllLabel = "View all", +}: SectionHeaderProps) => ( + + + {title} + + + {viewAllLabel} → + + +); + +const FavoritePreviewRow = ({ + item, + onRemove, +}: { + item: FavoriteItem; + onRemove: () => void; +}) => ( + + + + + + {item.name} + + + {item.name} + + { + e.preventDefault(); + e.stopPropagation(); + onRemove(); + }} + className="shrink-0 size-5 opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground" + aria-label="Remove from favorites" + > + + + +); + +const RecentlyViewedPreviewRow = ({ item }: { item: RecentlyViewedItem }) => ( + + + + + + {item.name} + + + {item.name} + + + {formatRelativeTime(new Date(item.viewedAt))} + + +); + +const FavoritesPreview = () => { + const { favorites, removeFavorite } = useFavorites(); + const preview = favorites.slice(0, PREVIEW_COUNT); + + return ( + + + + {preview.length === 0 ? ( + + + No favorites yet. Star a pipeline or run to pin it here. + + + ) : ( + preview.map((item) => ( + removeFavorite(item.type, item.id)} + /> + )) + )} + + + ); +}; + +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) => ( + + )) + )} + + + ); +}; + +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 }) => ( { 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} + + ); }; 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 +84,39 @@ export function DashboardRecentlyViewedView() { Nothing viewed yet. Open a pipeline or run to see it here. ) : ( - - {recentlyViewed.map((item) => ( - - ))} - + + + {paginated.map((item) => ( + + ))} + + + {totalPages > 1 && ( + + setPage(safePage - 1)} + > + + + + {safePage + 1} / {totalPages} + + = totalPages - 1} + onClick={() => setPage(safePage + 1)} + > + + + + )} + )} ); 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