Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions react-compiler.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/components/shared/HuggingFaceAuth",
"src/components/shared/GitHubLibrary",

"src/components/shared/Submitters/Oasis/components",

// 11-20 useCallback/useMemo
// "src/components/ui", // 12
// "src/components/PipelineRun", // 14
Expand Down
164 changes: 107 additions & 57 deletions src/components/shared/Submitters/Oasis/OasisSubmitter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { AlertCircle, CheckCircle, Loader2, SendHorizonal } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";

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

import { isAuthorizationRequired } from "../../Authentication/helpers";
import { useAuthLocalStorage } from "../../Authentication/useAuthLocalStorage";
import TooltipButton from "../../Buttons/TooltipButton";
import { SubmitTaskArgumentsDialog } from "./components/SubmitTaskArgumentsDialog";

interface OasisSubmitterProps {
componentSpec?: ComponentSpec;
onSubmitComplete?: () => void;
Expand All @@ -36,10 +41,12 @@ function useSubmitPipeline() {
return useMutation({
mutationFn: async ({
componentSpec,
taskArguments,
onSuccess,
onError,
}: {
componentSpec: ComponentSpec;
taskArguments?: TaskSpecOutput["arguments"];
onSuccess: (data: PipelineRun) => void;
onError: (error: Error | string) => void;
}) => {
Expand All @@ -54,6 +61,7 @@ function useSubmitPipeline() {
return new Promise<PipelineRun>((resolve, reject) => {
submitPipelineRun(componentSpec, backendUrl, {
authorizationToken: authorizationToken.current,
taskArguments,
onSuccess: (data) => {
resolve(data);
onSuccess(data);
Expand Down Expand Up @@ -84,6 +92,7 @@ const OasisSubmitter = ({
const isAutoRedirect = useBetaFlagValue("redirect-on-new-pipeline-run");

const [submitSuccess, setSubmitSuccess] = useState<boolean | null>(null);
const [isArgumentsDialogOpen, setIsArgumentsDialogOpen] = useState(false);
const { cooldownTime, setCooldownTime } = useCooldownTimer(0);
const notify = useToastNotification();
const navigate = useNavigate();
Expand Down Expand Up @@ -159,29 +168,50 @@ const OasisSubmitter = ({
[handleError, setCooldownTime],
);

const handleSubmit = useCallback(async () => {
if (!componentSpec) {
handleError("No pipeline to submit");
return;
}
const handleSubmit = useCallback(
async (taskArguments?: Record<string, string>) => {
if (!componentSpec) {
handleError("No pipeline to submit");
return;
}

if (!isComponentTreeValid) {
handleError(
`Pipeline validation failed. Refer to details panel for more info.`,
);
return;
}
if (!isComponentTreeValid) {
handleError(
`Pipeline validation failed. Refer to details panel for more info.`,
);
return;
}

setSubmitSuccess(null);
submit({ componentSpec, onSuccess, onError });
}, [
handleError,
submit,
componentSpec,
isComponentTreeValid,
onSuccess,
onError,
]);
setSubmitSuccess(null);
submit({
componentSpec,
taskArguments,
onSuccess,
onError,
});
},
[
handleError,
submit,
componentSpec,
isComponentTreeValid,
onSuccess,
onError,
],
);

const handleSubmitWithArguments = useCallback(
(args: Record<string, string>) => {
setIsArgumentsDialogOpen(false);
handleSubmit(args);
},
[handleSubmit],
);

const hasConfigurableInputs = useMemo(
() => (componentSpec?.inputs?.length ?? 0) > 0,
[componentSpec?.inputs],
);

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

const isArgumentsButtonVisible = hasConfigurableInputs && !isButtonDisabled;

const getButtonIcon = () => {
if (isSubmitting) {
return <Loader2 className="animate-spin" />;
Expand All @@ -217,42 +249,60 @@ const OasisSubmitter = ({
};

return (
<SidebarMenuButton
asChild
tooltip="Submit Run"
forceTooltip
tooltipPosition="right"
>
<Button
onClick={handleSubmit}
className="w-full justify-start"
variant="ghost"
disabled={isButtonDisabled || !available}
>
{getButtonIcon()}
<span className="font-normal text-xs">{getButtonText()}</span>
{!isComponentTreeValid && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-red-700" : "text-yellow-700",
)}
>
(has validation issues)
</div>
)}
{!available && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-red-700" : "text-yellow-700",
)}
<>
<InlineStack align="space-between" className="pr-2.5">
<Button
onClick={() => handleSubmit()}
className="flex-1 justify-start"
variant="ghost"
disabled={isButtonDisabled || !available}
>
{getButtonIcon()}
<span className="font-normal text-xs">{getButtonText()}</span>
{!isComponentTreeValid && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-red-700" : "text-yellow-700",
)}
>
(has validation issues)
</div>
)}
{!available && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-red-700" : "text-yellow-700",
)}
>
{`(backend ${configured ? "unavailable" : "unconfigured"})`}
</div>
)}
</Button>
{isArgumentsButtonVisible && (
<TooltipButton
tooltip="Submit run with arguments"
variant="ghost"
size="icon"
data-testid="run-with-arguments-button"
onClick={() => setIsArgumentsDialogOpen(true)}
disabled={!available}
>
{`(backend ${configured ? "unavailable" : "unconfigured"})`}
</div>
<Icon name="Split" className="rotate-90" />
</TooltipButton>
)}
</Button>
</SidebarMenuButton>
</InlineStack>

{componentSpec && (
<SubmitTaskArgumentsDialog
open={isArgumentsDialogOpen}
onCancel={() => setIsArgumentsDialogOpen(false)}
onConfirm={handleSubmitWithArguments}
componentSpec={componentSpec}
/>
)}
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { type ChangeEvent, useState } from "react";

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";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Paragraph } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import type { ComponentSpec, InputSpec } from "@/utils/componentSpec";

interface SubmitTaskArgumentsDialogProps {
open: boolean;
onCancel: () => void;
onConfirm: (args: Record<string, string>) => void;
componentSpec: ComponentSpec;
}

export const SubmitTaskArgumentsDialog = ({
open,
onCancel,
onConfirm,
componentSpec,
}: SubmitTaskArgumentsDialogProps) => {
const initialArgs = getArgumentsFromInputs(componentSpec);

const [taskArguments, setTaskArguments] =
useState<Record<string, string>>(initialArgs);

const inputs = componentSpec.inputs ?? [];

const handleValueChange = (name: string, value: string) => {
setTaskArguments((prev) => ({
...prev,
[name]: value,
}));
};

const handleConfirm = () => onConfirm(taskArguments);

const handleCancel = () => {
setTaskArguments(initialArgs);
onCancel();
};

const hasInputs = inputs.length > 0;

return (
<Dialog open={open} onOpenChange={handleCancel}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Submit Run with Arguments</DialogTitle>
<DialogDescription>
{hasInputs
? "Customize the pipeline input values before submitting."
: "This pipeline has no configurable inputs."}
</DialogDescription>
</DialogHeader>

{hasInputs && (
<ScrollArea className="max-h-[60vh] pr-4">
<BlockStack gap="4" className="p-1">
{inputs.map((input) => (
<ArgumentField
key={input.name}
input={input}
value={taskArguments[input.name] ?? ""}
onChange={handleValueChange}
/>
))}
</BlockStack>
</ScrollArea>
)}

<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Submit Run</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

interface ArgumentFieldProps {
input: InputSpec;
value: string;
onChange: (name: string, value: string) => void;
}

const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(input.name, e.target.value);
};

const typeLabel = typeSpecToString(input.type);
const isRequired = !input.optional;
const placeholder = input.default ?? "";

return (
<BlockStack gap="1">
<InlineStack gap="2" align="start">
<Paragraph size="sm" className="wrap-break-word">
{input.name}
</Paragraph>
<Paragraph size="xs" tone="subdued" className="truncate">
({typeLabel}
{isRequired ? "*" : ""})
</Paragraph>
</InlineStack>

{input.description && (
<Paragraph size="xs" tone="subdued" className="italic">
{input.description}
</Paragraph>
)}

<Input
id={input.name}
value={value}
onChange={handleChange}
placeholder={placeholder}
className={cn(isRequired && !value && !placeholder && "border-red-300")}
/>
</BlockStack>
);
};