Skip to content

Commit 73d1e7e

Browse files
committed
feat: copy submit run arguments from recent run
1 parent 3414ae9 commit 73d1e7e

File tree

4 files changed

+216
-29
lines changed

4 files changed

+216
-29
lines changed

src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,32 @@ import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper";
55
import { Button } from "@/components/ui/button";
66
import { InlineStack } from "@/components/ui/layout";
77
import { ScrollArea } from "@/components/ui/scroll-area";
8+
import { cn } from "@/lib/utils";
9+
import type { PipelineRun } from "@/types/pipelineRun";
810

911
import { RecentRunsTitle } from "./components/RecentRunsTitle";
1012
import { usePipelineRuns } from "./usePipelineRuns";
1113

1214
const DEFAULT_SHOWING_RUNS = 4;
1315

16+
interface PipelineRunsListProps {
17+
pipelineName?: string;
18+
showMoreButton?: boolean;
19+
showTitle?: boolean;
20+
disabled?: boolean;
21+
overviewConfig?: ComponentProps<typeof RunOverview>["config"];
22+
onRunClick?: (run: PipelineRun) => void;
23+
}
24+
1425
export const PipelineRunsList = withSuspenseWrapper(
1526
({
1627
pipelineName,
1728
showMoreButton = true,
29+
showTitle = true,
30+
disabled = false,
1831
overviewConfig,
19-
}: {
20-
pipelineName?: string;
21-
showMoreButton?: boolean;
22-
overviewConfig?: ComponentProps<typeof RunOverview>["config"];
23-
}) => {
32+
onRunClick,
33+
}: PipelineRunsListProps) => {
2434
const { data: pipelineRuns } = usePipelineRuns(pipelineName);
2535

2636
const [showingRuns, setShowingRuns] = useState(DEFAULT_SHOWING_RUNS);
@@ -31,13 +41,22 @@ export const PipelineRunsList = withSuspenseWrapper(
3141

3242
return (
3343
<>
34-
<RecentRunsTitle
35-
pipelineName={pipelineName}
36-
runsCount={pipelineRuns.length}
37-
/>
38-
<ScrollArea>
44+
{showTitle && (
45+
<RecentRunsTitle
46+
pipelineName={pipelineName}
47+
runsCount={pipelineRuns.length}
48+
/>
49+
)}
50+
<ScrollArea
51+
className={cn(disabled && "opacity-50 pointer-events-none")}
52+
>
3953
{pipelineRuns.slice(0, showingRuns).map((run) => (
40-
<RunOverview key={run.id} run={run} config={overviewConfig} />
54+
<RunOverview
55+
key={run.id}
56+
run={run}
57+
config={overviewConfig}
58+
onClick={onRunClick}
59+
/>
4160
))}
4261
{showMoreButton && pipelineRuns.length > showingRuns && (
4362
<InlineStack className="w-full" align="center">

src/components/shared/PipelineRunDisplay/RunOverview.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useNavigate } from "@tanstack/react-router";
2+
import type { MouseEvent } from "react";
23

34
import { StatusBar, StatusText } from "@/components/shared/Status/";
45
import { cn } from "@/lib/utils";
@@ -41,6 +42,7 @@ interface RunOverviewProps {
4142
showAuthor?: boolean;
4243
};
4344
className?: string;
45+
onClick?: (run: PipelineRun) => void;
4446
}
4547

4648
const defaultConfig = {
@@ -53,20 +55,31 @@ const defaultConfig = {
5355
showAuthor: false,
5456
};
5557

56-
const RunOverview = ({ run, config, className = "" }: RunOverviewProps) => {
58+
const RunOverview = ({
59+
run,
60+
config,
61+
className = "",
62+
onClick,
63+
}: RunOverviewProps) => {
5764
const navigate = useNavigate();
5865

5966
const combinedConfig = {
6067
...defaultConfig,
6168
...config,
6269
};
6370

71+
const handleClick = (e: MouseEvent) => {
72+
e.stopPropagation();
73+
if (onClick) {
74+
onClick(run);
75+
} else {
76+
navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` });
77+
}
78+
};
79+
6480
return (
6581
<div
66-
onClick={(e) => {
67-
e.stopPropagation();
68-
navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` });
69-
}}
82+
onClick={handleClick}
7083
className={cn(
7184
"flex flex-col p-2 text-sm hover:bg-gray-50 cursor-pointer",
7285
className,

src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useMutation } from "@tanstack/react-query";
12
import { type ChangeEvent, useState } from "react";
23

4+
import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList";
35
import { typeSpecToString } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils";
46
import { getArgumentsFromInputs } from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs";
57
import { 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";
1417
import { Input } from "@/components/ui/input";
1518
import { BlockStack, InlineStack } from "@/components/ui/layout";
19+
import {
20+
Popover,
21+
PopoverContent,
22+
PopoverTrigger,
23+
} from "@/components/ui/popover";
1624
import { ScrollArea } from "@/components/ui/scroll-area";
1725
import { Paragraph } from "@/components/ui/typography";
26+
import useToastNotification from "@/hooks/useToastNotification";
1827
import { cn } from "@/lib/utils";
28+
import { useBackend } from "@/providers/BackendProvider";
29+
import { fetchExecutionDetails } from "@/services/executionService";
30+
import type { PipelineRun } from "@/types/pipelineRun";
1931
import type { ComponentSpec, InputSpec } from "@/utils/componentSpec";
32+
import { getArgumentValue } from "@/utils/nodes/taskArguments";
2033

2134
interface 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+
95218
interface 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
);

src/styles/global.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ code {
147147

148148
/* Custom animations */
149149
--animate-revert-copied: revert-copied 0.5s ease-in-out forwards;
150+
--animate-highlight-fade: highlight-fade 2s ease-out forwards;
150151

151152
@keyframes revert-copied {
152153
0%,
@@ -159,6 +160,21 @@ code {
159160
transform: rotate(-90deg) scale(0);
160161
}
161162
}
163+
164+
@keyframes highlight-fade {
165+
0% {
166+
background-color: oklch(0.765 0.177 163 / 0.2);
167+
box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.5);
168+
}
169+
70% {
170+
background-color: oklch(0.765 0.177 163 / 0.15);
171+
box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.3);
172+
}
173+
100% {
174+
background-color: transparent;
175+
box-shadow: none;
176+
}
177+
}
162178
}
163179

164180
@layer base {

0 commit comments

Comments
 (0)