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