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 40dc1d781..49b7d8f9e 100644 --- a/src/hooks/useRecentlyViewed.ts +++ b/src/hooks/useRecentlyViewed.ts @@ -3,7 +3,7 @@ import { useCallback, useSyncExternalStore } 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..dc3eee67a --- /dev/null +++ b/src/routes/Dashboard/DashboardHomeView.tsx @@ -0,0 +1,231 @@ +// TODO: Remove fake announcement data before shipping +if (!window.__TANGLE_ANNOUNCEMENTS__) { + window.__TANGLE_ANNOUNCEMENTS__ = [ + { + id: "dashboard-beta-welcome", + title: "Welcome to the new Dashboard (Beta)", + body: "We're rolling out new features — favorites, recently viewed, and more. Expect changes as we iterate.", + variant: "info", + dismissible: true, + }, + ]; +} + +import { Link } from "@tanstack/react-router"; +import { GitBranch, Play } from "lucide-react"; + +import { RunSection } from "@/components/Home/RunSection/RunSection"; +import { AnnouncementBanners } from "@/components/shared/AnnouncementBanners"; +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"; + +const PREVIEW_COUNT = 5; + +function formatRelativeTime(viewedAt: number): string { + const diff = Date.now() - viewedAt; + const minutes = Math.floor(diff / 60_000); + if (minutes < 1) return "just now"; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +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 "/"; +} + +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" ? ( + + ) : ( + + )} + {type === "pipeline" ? "Pipeline" : type === "run" ? "Run" : "Component"} + +); + +const SectionHeader = ({ + title, + viewAllTo, + viewAllLabel = "View all", +}: { + title: string; + viewAllTo: string; + viewAllLabel?: string; +}) => ( + + + {title} + + + {viewAllLabel} → + + +); + +// ─── Favorites ───────────────────────────────────────────────────────────────── + +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} + + + + )) + )} +
+
+ ); +}; + +// ─── Recently Viewed ─────────────────────────────────────────────────────────── + +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(item.viewedAt)} + + + )) + )} +
+
+ ); +}; + +// ─── My Dashboard ────────────────────────────────────────────────────────────── + +export function DashboardHomeView() { + return ( + + + + {/* Favorites + Recently Viewed + (future) side by side */} +
+ + +
+
+ + {/* My Runs — full table with created_by:me filter */} + + + {/* + 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 09ed2b128..edc41dbfd 100644 --- a/src/routes/Dashboard/DashboardLayout.tsx +++ b/src/routes/Dashboard/DashboardLayout.tsx @@ -20,11 +20,18 @@ interface SidebarItem { to: string; label: string; icon: IconName; + exact?: boolean; } const SIDEBAR_ITEMS: SidebarItem[] = [ - { to: "/dashboard/runs", label: "Runs", icon: "Play" }, - { to: "/dashboard/pipelines", label: "Pipelines", icon: "GitBranch" }, + { + to: "/dashboard", + label: "My Dashboard", + icon: "LayoutDashboard", + exact: true, + }, + { to: "/dashboard/pipelines", label: "My Pipelines", icon: "GitBranch" }, + { to: "/dashboard/runs", label: "All Runs", icon: "Play" }, { to: "/dashboard/components", label: "Components", icon: "Package" }, { to: "/dashboard/favorites", label: "Favorites", icon: "Star" }, { to: "/dashboard/recently-viewed", label: "Recently Viewed", icon: "Clock" }, @@ -71,6 +78,7 @@ export function DashboardLayout() { to={item.to} className="w-full" activeProps={{ className: "is-active" }} + activeOptions={item.exact ? { exact: true } : undefined} > {({ isActive }) => (
diff --git a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx index ece643f10..f3a52141f 100644 --- a/src/routes/Dashboard/DashboardRecentlyViewedView.tsx +++ b/src/routes/Dashboard/DashboardRecentlyViewedView.tsx @@ -1,6 +1,8 @@ import { Link } from "@tanstack/react-router"; -import { GitBranch, Play } from "lucide-react"; +import { ChevronLeft, ChevronRight, GitBranch, Play } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Paragraph, Text } from "@/components/ui/typography"; import { @@ -9,6 +11,8 @@ import { } from "@/hooks/useRecentlyViewed"; import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router"; +const PAGE_SIZE = 20; + function getRecentlyViewedUrl(item: RecentlyViewedItem): string { if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`; if (item.type === "run") return `${RUNS_BASE_PATH}/${item.id}`; @@ -70,6 +74,14 @@ const RecentlyViewedCard = ({ item }: { item: RecentlyViewedItem }) => { 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 ( @@ -82,11 +94,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