diff --git a/src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx b/src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx index 03dd1ab40..ce8fa730e 100644 --- a/src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx +++ b/src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx @@ -5,22 +5,32 @@ import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper"; import { Button } from "@/components/ui/button"; import { InlineStack } from "@/components/ui/layout"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import type { PipelineRun } from "@/types/pipelineRun"; import { RecentRunsTitle } from "./components/RecentRunsTitle"; import { usePipelineRuns } from "./usePipelineRuns"; const DEFAULT_SHOWING_RUNS = 4; +interface PipelineRunsListProps { + pipelineName?: string; + showMoreButton?: boolean; + showTitle?: boolean; + disabled?: boolean; + overviewConfig?: ComponentProps["config"]; + onRunClick?: (run: PipelineRun) => void; +} + export const PipelineRunsList = withSuspenseWrapper( ({ pipelineName, showMoreButton = true, + showTitle = true, + disabled = false, overviewConfig, - }: { - pipelineName?: string; - showMoreButton?: boolean; - overviewConfig?: ComponentProps["config"]; - }) => { + onRunClick, + }: PipelineRunsListProps) => { const { data: pipelineRuns } = usePipelineRuns(pipelineName); const [showingRuns, setShowingRuns] = useState(DEFAULT_SHOWING_RUNS); @@ -31,13 +41,22 @@ export const PipelineRunsList = withSuspenseWrapper( return ( <> - - + {showTitle && ( + + )} + {pipelineRuns.slice(0, showingRuns).map((run) => ( - + ))} {showMoreButton && pipelineRuns.length > showingRuns && ( diff --git a/src/components/shared/PipelineRunDisplay/RunOverview.tsx b/src/components/shared/PipelineRunDisplay/RunOverview.tsx index 0d4a84926..42533e1b6 100644 --- a/src/components/shared/PipelineRunDisplay/RunOverview.tsx +++ b/src/components/shared/PipelineRunDisplay/RunOverview.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "@tanstack/react-router"; +import type { MouseEvent } from "react"; import { StatusBar, StatusText } from "@/components/shared/Status/"; import { cn } from "@/lib/utils"; @@ -41,6 +42,7 @@ interface RunOverviewProps { showAuthor?: boolean; }; className?: string; + onClick?: (run: PipelineRun) => void; } const defaultConfig = { @@ -53,7 +55,12 @@ const defaultConfig = { showAuthor: false, }; -const RunOverview = ({ run, config, className = "" }: RunOverviewProps) => { +const RunOverview = ({ + run, + config, + className = "", + onClick, +}: RunOverviewProps) => { const navigate = useNavigate(); const combinedConfig = { @@ -61,12 +68,18 @@ const RunOverview = ({ run, config, className = "" }: RunOverviewProps) => { ...config, }; + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + if (onClick) { + onClick(run); + } else { + navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` }); + } + }; + return (
{ - e.stopPropagation(); - navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` }); - }} + onClick={handleClick} className={cn( "flex flex-col p-2 text-sm hover:bg-gray-50 cursor-pointer", className, diff --git a/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx index 2dcd468fd..6b77271e6 100644 --- a/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx +++ b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx @@ -1,5 +1,7 @@ +import { useMutation } from "@tanstack/react-query"; import { type ChangeEvent, useState } from "react"; +import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList"; import { typeSpecToString } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils"; import { getArgumentsFromInputs } from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs"; import { Button } from "@/components/ui/button"; @@ -11,12 +13,23 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Icon } from "@/components/ui/icon"; import { Input } from "@/components/ui/input"; import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Paragraph } from "@/components/ui/typography"; +import useToastNotification from "@/hooks/useToastNotification"; import { cn } from "@/lib/utils"; +import { useBackend } from "@/providers/BackendProvider"; +import { fetchExecutionDetails } from "@/services/executionService"; +import type { PipelineRun } from "@/types/pipelineRun"; import type { ComponentSpec, InputSpec } from "@/utils/componentSpec"; +import { getArgumentValue } from "@/utils/nodes/taskArguments"; interface SubmitTaskArgumentsDialogProps { open: boolean; @@ -31,13 +44,35 @@ export const SubmitTaskArgumentsDialog = ({ onConfirm, componentSpec, }: SubmitTaskArgumentsDialogProps) => { + const notify = useToastNotification(); const initialArgs = getArgumentsFromInputs(componentSpec); const [taskArguments, setTaskArguments] = useState>(initialArgs); + // Track highlighted args with a version key to re-trigger CSS animation + const [highlightedArgs, setHighlightedArgs] = useState>( + new Map(), + ); + const inputs = componentSpec.inputs ?? []; + const handleCopyFromRun = (args: Record) => { + const diff = Object.entries(args).filter( + ([key, value]) => taskArguments[key] !== value, + ); + + setTaskArguments((prev) => ({ + ...prev, + ...args, + })); + + const version = Date.now(); + setHighlightedArgs(new Map(diff.map(([key]) => [key, version]))); + + notify(`Copied ${diff.length} arguments`, "success"); + }; + const handleValueChange = (name: string, value: string) => { setTaskArguments((prev) => ({ ...prev, @@ -59,24 +94,46 @@ export const SubmitTaskArgumentsDialog = ({ Submit Run with Arguments - + {hasInputs ? "Customize the pipeline input values before submitting." : "This pipeline has no configurable inputs."} + + {hasInputs ? ( + + + Customize the pipeline input values before submitting. + + + + + + ) : ( + + This pipeline has no configurable inputs. + + )} {hasInputs && ( - + - {inputs.map((input) => ( - - ))} + {inputs.map((input) => { + const highlightVersion = highlightedArgs.get(input.name); + return ( + + ); + })} )} @@ -92,13 +149,84 @@ export const SubmitTaskArgumentsDialog = ({ ); }; +const CopyFromRunPopover = ({ + componentSpec, + onCopy, +}: { + componentSpec: ComponentSpec; + onCopy: (args: Record) => void; +}) => { + const { backendUrl } = useBackend(); + const pipelineName = componentSpec.name; + + const [popoverOpen, setPopoverOpen] = useState(false); + + const { mutate: copyFromRunMutation, isPending: isCopyingFromRun } = + useMutation({ + mutationFn: async (run: PipelineRun) => { + const executionDetails = await fetchExecutionDetails( + String(run.root_execution_id), + backendUrl, + ); + return executionDetails.task_spec.arguments; + }, + onSuccess: (runArguments) => { + if (runArguments) { + const newArgs = Object.fromEntries( + Object.entries(runArguments) + .map(([name, _]) => [name, getArgumentValue(runArguments, name)]) + .filter( + (entry): entry is [string, string] => entry[1] !== undefined, + ), + ); + onCopy(newArgs); + } + setPopoverOpen(false); + }, + onError: (error) => { + console.error("Failed to fetch run arguments:", error); + setPopoverOpen(false); + }, + }); + + return ( + + + + + + + + + ); +}; + interface ArgumentFieldProps { input: InputSpec; value: string; onChange: (name: string, value: string) => void; + isHighlighted?: boolean; } -const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => { +const ArgumentField = ({ + input, + value, + onChange, + isHighlighted, +}: ArgumentFieldProps) => { const handleChange = (e: ChangeEvent) => { onChange(input.name, e.target.value); }; @@ -108,7 +236,13 @@ const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => { const placeholder = input.default ?? ""; return ( - + {input.name} diff --git a/src/styles/global.css b/src/styles/global.css index 4dd7dec3d..168f910b0 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -147,6 +147,7 @@ code { /* Custom animations */ --animate-revert-copied: revert-copied 0.5s ease-in-out forwards; + --animate-highlight-fade: highlight-fade 2s ease-out forwards; @keyframes revert-copied { 0%, @@ -159,6 +160,21 @@ code { transform: rotate(-90deg) scale(0); } } + + @keyframes highlight-fade { + 0% { + background-color: oklch(0.765 0.177 163 / 0.2); + box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.5); + } + 70% { + background-color: oklch(0.765 0.177 163 / 0.15); + box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.3); + } + 100% { + background-color: transparent; + box-shadow: none; + } + } } @layer base {