From 9f556af0ad2595b41942ff51117caa94cfd22871 Mon Sep 17 00:00:00 2001 From: Morgan Wowk Date: Fri, 9 Jan 2026 13:33:37 -0800 Subject: [PATCH] feat: Improve visual contrast between Pipelines and Runs **Changes:** * Removes grid when viewing a read-only run * Adds a background color to the Runs view that is different from the Pipelines view * Adds breadcrumbs that intelligently detects if a pipeline exists locally and gives the user the capability of navigating to the pipeline that triggered the run --- .../PipelineRun/PipelineRunPage.tsx | 11 +- .../components/PipelineRunBreadcrumbs.tsx | 195 ++++++++++++++++++ src/components/layout/AppMenu.tsx | 20 +- 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/components/PipelineRun/components/PipelineRunBreadcrumbs.tsx diff --git a/src/components/PipelineRun/PipelineRunPage.tsx b/src/components/PipelineRun/PipelineRunPage.tsx index 0a7fc30ec..fc75b951b 100644 --- a/src/components/PipelineRun/PipelineRunPage.tsx +++ b/src/components/PipelineRun/PipelineRunPage.tsx @@ -1,4 +1,4 @@ -import { Background, MiniMap, type ReactFlowProps } from "@xyflow/react"; +import { MiniMap, type ReactFlowProps } from "@xyflow/react"; import { useCallback, useState } from "react"; import { FlowCanvas, FlowControls } from "@/components/shared/ReactFlow"; @@ -34,8 +34,12 @@ const PipelineRunPage = () => { }> - - + + { updateConfig={updateFlowConfig} showInteractive={false} /> - diff --git a/src/components/PipelineRun/components/PipelineRunBreadcrumbs.tsx b/src/components/PipelineRun/components/PipelineRunBreadcrumbs.tsx new file mode 100644 index 000000000..821450c95 --- /dev/null +++ b/src/components/PipelineRun/components/PipelineRunBreadcrumbs.tsx @@ -0,0 +1,195 @@ +import { useNavigate, useParams } from "@tanstack/react-router"; +import { Check, ChevronRight, Copy, Network } from "lucide-react"; +import { type MouseEvent, useCallback, useEffect, useState } from "react"; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useComponentSpec } from "@/providers/ComponentSpecProvider"; +import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; +import { loadPipelineByName } from "@/services/pipelineService"; +import { copyToClipboard } from "@/utils/string"; + +interface PipelineRunBreadcrumbsProps { + variant?: "overlay" | "topbar"; +} + +export const PipelineRunBreadcrumbs = ({ + variant = "overlay", +}: PipelineRunBreadcrumbsProps) => { + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const { componentSpec } = useComponentSpec(); + const executionData = useExecutionDataOptional(); + + // Get run ID from execution data OR from URL params (fallback for when provider isn't loaded yet) + const runIdFromParams = + "id" in params && typeof params.id === "string" ? params.id : undefined; + const runId = executionData?.runId || runIdFromParams; + const metadata = executionData?.metadata; + const [isCopied, setIsCopied] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [pipelineExistsLocally, setPipelineExistsLocally] = useState< + boolean | null + >(null); + + const pipelineName = componentSpec?.name || metadata?.pipeline_name; + + // Check if the pipeline exists in local storage + useEffect(() => { + const checkPipelineExists = async () => { + if (!pipelineName) { + setPipelineExistsLocally(null); + return; + } + + try { + const result = await loadPipelineByName(pipelineName); + setPipelineExistsLocally(!!result.experiment); + } catch (error) { + console.error("Error checking pipeline existence:", error); + setPipelineExistsLocally(false); + } + }; + + checkPipelineExists(); + }, [pipelineName]); + + const handleNavigateToPipeline = useCallback(() => { + if (pipelineName && pipelineExistsLocally) { + navigate({ to: `/editor/${encodeURIComponent(pipelineName)}` }); + } + }, [pipelineName, pipelineExistsLocally, navigate]); + + const handleCopyRunId = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + if (runId) { + copyToClipboard(runId); + setIsCopied(true); + } + }, + [runId], + ); + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => { + setIsCopied(false); + }, 1500); + return () => clearTimeout(timer); + } + }, [isCopied]); + + if (!pipelineName) { + return null; + } + + // Styles for topbar variant (white text on dark background) + const isTopbar = variant === "topbar"; + const textColorClass = isTopbar ? "text-white" : "text-foreground"; + const mutedTextClass = isTopbar ? "text-gray-300" : "text-muted-foreground"; + const buttonVariantClass = isTopbar + ? "h-7 px-2 gap-1.5 font-medium text-gray-300 hover:text-white hover:bg-stone-800" + : "h-7 px-2 gap-1.5 text-muted-foreground hover:text-foreground font-medium"; + + // Container for overlay variant + const containerClass = isTopbar + ? "flex items-center gap-1" + : "absolute top-0 left-0 z-10 bg-white/95 backdrop-blur-sm shadow-md rounded-br-xl border-b border-r border-gray-200"; + + return ( +
+
+ + + + {pipelineExistsLocally ? ( + + + + ) : ( + + + {pipelineName} + + )} + + + + + + {runId ? ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + title={`Click to copy: ${runId}`} + > + + Run {runId} + + + + + +
+ ) : ( + + Run + + )} +
+
+
+
+
+ ); +}; diff --git a/src/components/layout/AppMenu.tsx b/src/components/layout/AppMenu.tsx index d8fba88eb..e9dc8dd64 100644 --- a/src/components/layout/AppMenu.tsx +++ b/src/components/layout/AppMenu.tsx @@ -1,7 +1,9 @@ +import { useLocation } from "@tanstack/react-router"; import { Menu } from "lucide-react"; import { useState } from "react"; import logo from "/Tangle_white.png"; +import { PipelineRunBreadcrumbs } from "@/components/PipelineRun/components/PipelineRunBreadcrumbs"; import { isAuthorizationRequired } from "@/components/shared/Authentication/helpers"; import { TopBarAuthentication } from "@/components/shared/Authentication/TopBarAuthentication"; import { CopyText } from "@/components/shared/CopyText/CopyText"; @@ -17,6 +19,7 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; +import { RUNS_BASE_PATH } from "@/routes/router"; import { TOP_NAV_HEIGHT } from "@/utils/constants"; import BackendStatus from "../shared/BackendStatus"; @@ -27,9 +30,13 @@ import { PersonalPreferences } from "../shared/Settings/PersonalPreferences"; const AppMenu = () => { const requiresAuthorization = isAuthorizationRequired(); const { componentSpec } = useComponentSpec(); + const location = useLocation(); const title = componentSpec?.name; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + // Check if we're on a run page + const isRunPage = location.pathname.includes(RUNS_BASE_PATH); + return (
{ /> - {title && ( - - {title} - + {/* Show breadcrumbs on run pages, otherwise show simple title */} + {isRunPage ? ( + + ) : ( + title && ( + + {title} + + ) )}