Skip to content

Commit 422cc17

Browse files
committed
feat: submit run with arguments
1 parent a43e156 commit 422cc17

File tree

3 files changed

+246
-57
lines changed

3 files changed

+246
-57
lines changed

react-compiler.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const REACT_COMPILER_ENABLED_DIRS = [
2727
"src/components/shared/HuggingFaceAuth",
2828
"src/components/shared/GitHubLibrary",
2929

30+
"src/components/shared/Submitters/Oasis/components",
31+
3032
// 11-20 useCallback/useMemo
3133
// "src/components/ui", // 12
3234
// "src/components/PipelineRun", // 14

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

Lines changed: 107 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useMutation, useQueryClient } from "@tanstack/react-query";
22
import { useNavigate } from "@tanstack/react-router";
33
import { AlertCircle, CheckCircle, Loader2, SendHorizonal } from "lucide-react";
4-
import { useCallback, useRef, useState } from "react";
4+
import { useCallback, useMemo, useRef, useState } from "react";
55

6+
import type { TaskSpecOutput } from "@/api/types.gen";
67
import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization";
78
import { useBetaFlagValue } from "@/components/shared/Settings/useBetaFlags";
89
import { Button } from "@/components/ui/button";
9-
import { SidebarMenuButton } from "@/components/ui/sidebar";
10+
import { Icon } from "@/components/ui/icon";
11+
import { InlineStack } from "@/components/ui/layout";
1012
import useCooldownTimer from "@/hooks/useCooldownTimer";
1113
import useToastNotification from "@/hooks/useToastNotification";
1214
import { cn } from "@/lib/utils";
@@ -18,6 +20,9 @@ import { submitPipelineRun } from "@/utils/submitPipeline";
1820

1921
import { isAuthorizationRequired } from "../../Authentication/helpers";
2022
import { useAuthLocalStorage } from "../../Authentication/useAuthLocalStorage";
23+
import TooltipButton from "../../Buttons/TooltipButton";
24+
import { SubmitTaskArgumentsDialog } from "./components/SubmitTaskArgumentsDialog";
25+
2126
interface OasisSubmitterProps {
2227
componentSpec?: ComponentSpec;
2328
onSubmitComplete?: () => void;
@@ -36,10 +41,12 @@ function useSubmitPipeline() {
3641
return useMutation({
3742
mutationFn: async ({
3843
componentSpec,
44+
taskArguments,
3945
onSuccess,
4046
onError,
4147
}: {
4248
componentSpec: ComponentSpec;
49+
taskArguments?: TaskSpecOutput["arguments"];
4350
onSuccess: (data: PipelineRun) => void;
4451
onError: (error: Error | string) => void;
4552
}) => {
@@ -54,6 +61,7 @@ function useSubmitPipeline() {
5461
return new Promise<PipelineRun>((resolve, reject) => {
5562
submitPipelineRun(componentSpec, backendUrl, {
5663
authorizationToken: authorizationToken.current,
64+
taskArguments,
5765
onSuccess: (data) => {
5866
resolve(data);
5967
onSuccess(data);
@@ -84,6 +92,7 @@ const OasisSubmitter = ({
8492
const isAutoRedirect = useBetaFlagValue("redirect-on-new-pipeline-run");
8593

8694
const [submitSuccess, setSubmitSuccess] = useState<boolean | null>(null);
95+
const [isArgumentsDialogOpen, setIsArgumentsDialogOpen] = useState(false);
8796
const { cooldownTime, setCooldownTime } = useCooldownTimer(0);
8897
const notify = useToastNotification();
8998
const navigate = useNavigate();
@@ -159,29 +168,50 @@ const OasisSubmitter = ({
159168
[handleError, setCooldownTime],
160169
);
161170

162-
const handleSubmit = useCallback(async () => {
163-
if (!componentSpec) {
164-
handleError("No pipeline to submit");
165-
return;
166-
}
171+
const handleSubmit = useCallback(
172+
async (taskArguments?: Record<string, string>) => {
173+
if (!componentSpec) {
174+
handleError("No pipeline to submit");
175+
return;
176+
}
167177

168-
if (!isComponentTreeValid) {
169-
handleError(
170-
`Pipeline validation failed. Refer to details panel for more info.`,
171-
);
172-
return;
173-
}
178+
if (!isComponentTreeValid) {
179+
handleError(
180+
`Pipeline validation failed. Refer to details panel for more info.`,
181+
);
182+
return;
183+
}
174184

175-
setSubmitSuccess(null);
176-
submit({ componentSpec, onSuccess, onError });
177-
}, [
178-
handleError,
179-
submit,
180-
componentSpec,
181-
isComponentTreeValid,
182-
onSuccess,
183-
onError,
184-
]);
185+
setSubmitSuccess(null);
186+
submit({
187+
componentSpec,
188+
taskArguments,
189+
onSuccess,
190+
onError,
191+
});
192+
},
193+
[
194+
handleError,
195+
submit,
196+
componentSpec,
197+
isComponentTreeValid,
198+
onSuccess,
199+
onError,
200+
],
201+
);
202+
203+
const handleSubmitWithArguments = useCallback(
204+
(args: Record<string, string>) => {
205+
setIsArgumentsDialogOpen(false);
206+
handleSubmit(args);
207+
},
208+
[handleSubmit],
209+
);
210+
211+
const hasConfigurableInputs = useMemo(
212+
() => (componentSpec?.inputs?.length ?? 0) > 0,
213+
[componentSpec?.inputs],
214+
);
185215

186216
const getButtonText = () => {
187217
if (cooldownTime > 0) {
@@ -203,6 +233,8 @@ const OasisSubmitter = ({
203233
("graph" in componentSpec.implementation &&
204234
Object.keys(componentSpec.implementation.graph.tasks).length === 0);
205235

236+
const isArgumentsButtonVisible = hasConfigurableInputs && !isButtonDisabled;
237+
206238
const getButtonIcon = () => {
207239
if (isSubmitting) {
208240
return <Loader2 className="animate-spin" />;
@@ -217,42 +249,60 @@ const OasisSubmitter = ({
217249
};
218250

219251
return (
220-
<SidebarMenuButton
221-
asChild
222-
tooltip="Submit Run"
223-
forceTooltip
224-
tooltipPosition="right"
225-
>
226-
<Button
227-
onClick={handleSubmit}
228-
className="w-full justify-start"
229-
variant="ghost"
230-
disabled={isButtonDisabled || !available}
231-
>
232-
{getButtonIcon()}
233-
<span className="font-normal text-xs">{getButtonText()}</span>
234-
{!isComponentTreeValid && (
235-
<div
236-
className={cn(
237-
"text-xs font-light -ml-1",
238-
configured ? "text-red-700" : "text-yellow-700",
239-
)}
240-
>
241-
(has validation issues)
242-
</div>
243-
)}
244-
{!available && (
245-
<div
246-
className={cn(
247-
"text-xs font-light -ml-1",
248-
configured ? "text-red-700" : "text-yellow-700",
249-
)}
252+
<>
253+
<InlineStack align="space-between" className="pr-2.5">
254+
<Button
255+
onClick={() => handleSubmit()}
256+
className="flex-1 justify-start"
257+
variant="ghost"
258+
disabled={isButtonDisabled || !available}
259+
>
260+
{getButtonIcon()}
261+
<span className="font-normal text-xs">{getButtonText()}</span>
262+
{!isComponentTreeValid && (
263+
<div
264+
className={cn(
265+
"text-xs font-light -ml-1",
266+
configured ? "text-red-700" : "text-yellow-700",
267+
)}
268+
>
269+
(has validation issues)
270+
</div>
271+
)}
272+
{!available && (
273+
<div
274+
className={cn(
275+
"text-xs font-light -ml-1",
276+
configured ? "text-red-700" : "text-yellow-700",
277+
)}
278+
>
279+
{`(backend ${configured ? "unavailable" : "unconfigured"})`}
280+
</div>
281+
)}
282+
</Button>
283+
{isArgumentsButtonVisible && (
284+
<TooltipButton
285+
tooltip="Submit run with arguments"
286+
variant="ghost"
287+
size="icon"
288+
data-testid="run-with-arguments-button"
289+
onClick={() => setIsArgumentsDialogOpen(true)}
290+
disabled={!available}
250291
>
251-
{`(backend ${configured ? "unavailable" : "unconfigured"})`}
252-
</div>
292+
<Icon name="Split" className="rotate-90" />
293+
</TooltipButton>
253294
)}
254-
</Button>
255-
</SidebarMenuButton>
295+
</InlineStack>
296+
297+
{componentSpec && (
298+
<SubmitTaskArgumentsDialog
299+
open={isArgumentsDialogOpen}
300+
onCancel={() => setIsArgumentsDialogOpen(false)}
301+
onConfirm={handleSubmitWithArguments}
302+
componentSpec={componentSpec}
303+
/>
304+
)}
305+
</>
256306
);
257307
};
258308

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { type ChangeEvent, useState } from "react";
2+
3+
import { typeSpecToString } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils";
4+
import { getArgumentsFromInputs } from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogFooter,
11+
DialogHeader,
12+
DialogTitle,
13+
} from "@/components/ui/dialog";
14+
import { Input } from "@/components/ui/input";
15+
import { BlockStack, InlineStack } from "@/components/ui/layout";
16+
import { ScrollArea } from "@/components/ui/scroll-area";
17+
import { Paragraph } from "@/components/ui/typography";
18+
import { cn } from "@/lib/utils";
19+
import type { ComponentSpec, InputSpec } from "@/utils/componentSpec";
20+
21+
interface SubmitTaskArgumentsDialogProps {
22+
open: boolean;
23+
onCancel: () => void;
24+
onConfirm: (args: Record<string, string>) => void;
25+
componentSpec: ComponentSpec;
26+
}
27+
28+
export const SubmitTaskArgumentsDialog = ({
29+
open,
30+
onCancel,
31+
onConfirm,
32+
componentSpec,
33+
}: SubmitTaskArgumentsDialogProps) => {
34+
const initialArgs = getArgumentsFromInputs(componentSpec);
35+
36+
const [taskArguments, setTaskArguments] =
37+
useState<Record<string, string>>(initialArgs);
38+
39+
const inputs = componentSpec.inputs ?? [];
40+
41+
const handleValueChange = (name: string, value: string) => {
42+
setTaskArguments((prev) => ({
43+
...prev,
44+
[name]: value,
45+
}));
46+
};
47+
48+
const handleConfirm = () => onConfirm(taskArguments);
49+
50+
const handleCancel = () => {
51+
setTaskArguments(initialArgs);
52+
onCancel();
53+
};
54+
55+
const hasInputs = inputs.length > 0;
56+
57+
return (
58+
<Dialog open={open} onOpenChange={handleCancel}>
59+
<DialogContent className="sm:max-w-lg">
60+
<DialogHeader>
61+
<DialogTitle>Submit Run with Arguments</DialogTitle>
62+
<DialogDescription>
63+
{hasInputs
64+
? "Customize the pipeline input values before submitting."
65+
: "This pipeline has no configurable inputs."}
66+
</DialogDescription>
67+
</DialogHeader>
68+
69+
{hasInputs && (
70+
<ScrollArea className="max-h-[60vh] pr-4">
71+
<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+
))}
80+
</BlockStack>
81+
</ScrollArea>
82+
)}
83+
84+
<DialogFooter>
85+
<Button variant="outline" onClick={handleCancel}>
86+
Cancel
87+
</Button>
88+
<Button onClick={handleConfirm}>Submit Run</Button>
89+
</DialogFooter>
90+
</DialogContent>
91+
</Dialog>
92+
);
93+
};
94+
95+
interface ArgumentFieldProps {
96+
input: InputSpec;
97+
value: string;
98+
onChange: (name: string, value: string) => void;
99+
}
100+
101+
const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
102+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
103+
onChange(input.name, e.target.value);
104+
};
105+
106+
const typeLabel = typeSpecToString(input.type);
107+
const isRequired = !input.optional;
108+
const placeholder = input.default ?? "";
109+
110+
return (
111+
<BlockStack gap="1">
112+
<InlineStack gap="2" align="start">
113+
<Paragraph size="sm" className="wrap-break-word">
114+
{input.name}
115+
</Paragraph>
116+
<Paragraph size="xs" tone="subdued" className="truncate">
117+
({typeLabel}
118+
{isRequired ? "*" : ""})
119+
</Paragraph>
120+
</InlineStack>
121+
122+
{input.description && (
123+
<Paragraph size="xs" tone="subdued" className="italic">
124+
{input.description}
125+
</Paragraph>
126+
)}
127+
128+
<Input
129+
id={input.name}
130+
value={value}
131+
onChange={handleChange}
132+
placeholder={placeholder}
133+
className={cn(isRequired && !value && !placeholder && "border-red-300")}
134+
/>
135+
</BlockStack>
136+
);
137+
};

0 commit comments

Comments
 (0)