1+ import { useMutation } from "@tanstack/react-query" ;
12import { type ChangeEvent , useState } from "react" ;
23
4+ import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList" ;
35import { typeSpecToString } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils" ;
46import { getArgumentsFromInputs } from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs" ;
57import { Button } from "@/components/ui/button" ;
@@ -11,12 +13,23 @@ import {
1113 DialogHeader ,
1214 DialogTitle ,
1315} from "@/components/ui/dialog" ;
16+ import { Icon } from "@/components/ui/icon" ;
1417import { Input } from "@/components/ui/input" ;
1518import { BlockStack , InlineStack } from "@/components/ui/layout" ;
19+ import {
20+ Popover ,
21+ PopoverContent ,
22+ PopoverTrigger ,
23+ } from "@/components/ui/popover" ;
1624import { ScrollArea } from "@/components/ui/scroll-area" ;
1725import { Paragraph } from "@/components/ui/typography" ;
26+ import useToastNotification from "@/hooks/useToastNotification" ;
1827import { cn } from "@/lib/utils" ;
28+ import { useBackend } from "@/providers/BackendProvider" ;
29+ import { fetchExecutionDetails } from "@/services/executionService" ;
30+ import type { PipelineRun } from "@/types/pipelineRun" ;
1931import type { ComponentSpec , InputSpec } from "@/utils/componentSpec" ;
32+ import { getArgumentValue } from "@/utils/nodes/taskArguments" ;
2033
2134interface SubmitTaskArgumentsDialogProps {
2235 open : boolean ;
@@ -31,13 +44,35 @@ export const SubmitTaskArgumentsDialog = ({
3144 onConfirm,
3245 componentSpec,
3346} : SubmitTaskArgumentsDialogProps ) => {
47+ const notify = useToastNotification ( ) ;
3448 const initialArgs = getArgumentsFromInputs ( componentSpec ) ;
3549
3650 const [ taskArguments , setTaskArguments ] =
3751 useState < Record < string , string > > ( initialArgs ) ;
3852
53+ // Track highlighted args with a version key to re-trigger CSS animation
54+ const [ highlightedArgs , setHighlightedArgs ] = useState < Map < string , number > > (
55+ new Map ( ) ,
56+ ) ;
57+
3958 const inputs = componentSpec . inputs ?? [ ] ;
4059
60+ const handleCopyFromRun = ( args : Record < string , string > ) => {
61+ const diff = Object . entries ( args ) . filter (
62+ ( [ key , value ] ) => taskArguments [ key ] !== value ,
63+ ) ;
64+
65+ setTaskArguments ( ( prev ) => ( {
66+ ...prev ,
67+ ...args ,
68+ } ) ) ;
69+
70+ const version = Date . now ( ) ;
71+ setHighlightedArgs ( new Map ( diff . map ( ( [ key ] ) => [ key , version ] ) ) ) ;
72+
73+ notify ( `Copied ${ diff . length } arguments` , "success" ) ;
74+ } ;
75+
4176 const handleValueChange = ( name : string , value : string ) => {
4277 setTaskArguments ( ( prev ) => ( {
4378 ...prev ,
@@ -49,6 +84,7 @@ export const SubmitTaskArgumentsDialog = ({
4984
5085 const handleCancel = ( ) => {
5186 setTaskArguments ( initialArgs ) ;
87+ setHighlightedArgs ( new Map ( ) ) ;
5288 onCancel ( ) ;
5389 } ;
5490
@@ -59,24 +95,46 @@ export const SubmitTaskArgumentsDialog = ({
5995 < DialogContent className = "sm:max-w-lg" >
6096 < DialogHeader >
6197 < DialogTitle > Submit Run with Arguments</ DialogTitle >
62- < DialogDescription >
98+ < DialogDescription className = "hidden" >
6399 { hasInputs
64100 ? "Customize the pipeline input values before submitting."
65101 : "This pipeline has no configurable inputs." }
66102 </ DialogDescription >
103+
104+ { hasInputs ? (
105+ < BlockStack gap = "2" >
106+ < Paragraph tone = "subdued" size = "sm" >
107+ Customize the pipeline input values before submitting.
108+ </ Paragraph >
109+ < InlineStack align = "end" className = "w-full" >
110+ < CopyFromRunPopover
111+ componentSpec = { componentSpec }
112+ onCopy = { handleCopyFromRun }
113+ />
114+ </ InlineStack >
115+ </ BlockStack >
116+ ) : (
117+ < Paragraph tone = "subdued" >
118+ This pipeline has no configurable inputs.
119+ </ Paragraph >
120+ ) }
67121 </ DialogHeader >
68122
69123 { hasInputs && (
70- < ScrollArea className = "max-h-[60vh] pr-4" >
124+ < ScrollArea className = "max-h-[60vh] pr-4 w-full " >
71125 < BlockStack gap = "4" className = "p-1" >
72- { inputs . map ( ( input ) => (
73- < ArgumentField
74- key = { input . name }
75- input = { input }
76- value = { taskArguments [ input . name ] ?? "" }
77- onChange = { handleValueChange }
78- />
79- ) ) }
126+ { inputs . map ( ( input ) => {
127+ const highlightVersion = highlightedArgs . get ( input . name ) ;
128+ return (
129+ < ArgumentField
130+ key = { `${ input . name } -${ highlightVersion ?? "static" } ` }
131+ input = { input }
132+ value = { taskArguments [ input . name ] ?? "" }
133+ onChange = { handleValueChange }
134+ isHighlighted = { highlightVersion !== undefined }
135+ />
136+ ) ;
137+ } ) }
80138 </ BlockStack >
81139 </ ScrollArea >
82140 ) }
@@ -92,13 +150,84 @@ export const SubmitTaskArgumentsDialog = ({
92150 ) ;
93151} ;
94152
153+ const CopyFromRunPopover = ( {
154+ componentSpec,
155+ onCopy,
156+ } : {
157+ componentSpec : ComponentSpec ;
158+ onCopy : ( args : Record < string , string > ) => void ;
159+ } ) => {
160+ const { backendUrl } = useBackend ( ) ;
161+ const pipelineName = componentSpec . name ;
162+
163+ const [ popoverOpen , setPopoverOpen ] = useState ( false ) ;
164+
165+ const { mutate : copyFromRunMutation , isPending : isCopyingFromRun } =
166+ useMutation ( {
167+ mutationFn : async ( run : PipelineRun ) => {
168+ const executionDetails = await fetchExecutionDetails (
169+ String ( run . root_execution_id ) ,
170+ backendUrl ,
171+ ) ;
172+ return executionDetails . task_spec . arguments ;
173+ } ,
174+ onSuccess : ( runArguments ) => {
175+ if ( runArguments ) {
176+ const newArgs = Object . fromEntries (
177+ Object . entries ( runArguments )
178+ . map ( ( [ name , _ ] ) => [ name , getArgumentValue ( runArguments , name ) ] )
179+ . filter (
180+ ( entry ) : entry is [ string , string ] => entry [ 1 ] !== undefined ,
181+ ) ,
182+ ) ;
183+ onCopy ( newArgs ) ;
184+ }
185+ setPopoverOpen ( false ) ;
186+ } ,
187+ onError : ( error ) => {
188+ console . error ( "Failed to fetch run arguments:" , error ) ;
189+ setPopoverOpen ( false ) ;
190+ } ,
191+ } ) ;
192+
193+ return (
194+ < Popover open = { popoverOpen } onOpenChange = { setPopoverOpen } >
195+ < PopoverTrigger asChild >
196+ < Button variant = "ghost" size = "sm" >
197+ < Icon name = "Copy" />
198+ Copy from recent run
199+ </ Button >
200+ </ PopoverTrigger >
201+ < PopoverContent className = "w-100" align = "end" >
202+ < PipelineRunsList
203+ pipelineName = { pipelineName }
204+ onRunClick = { copyFromRunMutation }
205+ showTitle = { false }
206+ showMoreButton = { true }
207+ overviewConfig = { {
208+ showName : false ,
209+ showTaskStatusBar : false ,
210+ } }
211+ disabled = { isCopyingFromRun }
212+ />
213+ </ PopoverContent >
214+ </ Popover >
215+ ) ;
216+ } ;
217+
95218interface ArgumentFieldProps {
96219 input : InputSpec ;
97220 value : string ;
98221 onChange : ( name : string , value : string ) => void ;
222+ isHighlighted ?: boolean ;
99223}
100224
101- const ArgumentField = ( { input, value, onChange } : ArgumentFieldProps ) => {
225+ const ArgumentField = ( {
226+ input,
227+ value,
228+ onChange,
229+ isHighlighted,
230+ } : ArgumentFieldProps ) => {
102231 const handleChange = ( e : ChangeEvent < HTMLInputElement > ) => {
103232 onChange ( input . name , e . target . value ) ;
104233 } ;
@@ -108,7 +237,13 @@ const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
108237 const placeholder = input . default ?? "" ;
109238
110239 return (
111- < BlockStack gap = "1" >
240+ < BlockStack
241+ gap = "1"
242+ className = { cn (
243+ "rounded-md px-1 py-1" ,
244+ isHighlighted && "animate-highlight-fade" ,
245+ ) }
246+ >
112247 < InlineStack gap = "2" align = "start" >
113248 < Paragraph size = "sm" className = "wrap-break-word" >
114249 { input . name }
@@ -130,7 +265,11 @@ const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
130265 value = { value }
131266 onChange = { handleChange }
132267 placeholder = { placeholder }
133- className = { cn ( isRequired && ! value && ! placeholder && "border-red-300" ) }
268+ className = { cn (
269+ isRequired && ! value && ! placeholder && "border-red-300" ,
270+ // todo: remove this once we have a proper style in Input component
271+ "bg-white!" ,
272+ ) }
134273 />
135274 </ BlockStack >
136275 ) ;
0 commit comments