diff --git a/react-compiler.config.js b/react-compiler.config.js index b2363e19e..691279cb4 100644 --- a/react-compiler.config.js +++ b/react-compiler.config.js @@ -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 diff --git a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx index 6cd352af9..f754ae799 100644 --- a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx +++ b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx @@ -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"; @@ -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; @@ -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; }) => { @@ -54,6 +61,7 @@ function useSubmitPipeline() { return new Promise((resolve, reject) => { submitPipelineRun(componentSpec, backendUrl, { authorizationToken: authorizationToken.current, + taskArguments, onSuccess: (data) => { resolve(data); onSuccess(data); @@ -84,6 +92,7 @@ const OasisSubmitter = ({ const isAutoRedirect = useBetaFlagValue("redirect-on-new-pipeline-run"); const [submitSuccess, setSubmitSuccess] = useState(null); + const [isArgumentsDialogOpen, setIsArgumentsDialogOpen] = useState(false); const { cooldownTime, setCooldownTime } = useCooldownTimer(0); const notify = useToastNotification(); const navigate = useNavigate(); @@ -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) => { + 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) => { + setIsArgumentsDialogOpen(false); + handleSubmit(args); + }, + [handleSubmit], + ); + + const hasConfigurableInputs = useMemo( + () => (componentSpec?.inputs?.length ?? 0) > 0, + [componentSpec?.inputs], + ); const getButtonText = () => { if (cooldownTime > 0) { @@ -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 ; @@ -217,42 +249,60 @@ const OasisSubmitter = ({ }; return ( - - + {isArgumentsButtonVisible && ( + setIsArgumentsDialogOpen(true)} + disabled={!available} > - {`(backend ${configured ? "unavailable" : "unconfigured"})`} - + + )} - - + + + {componentSpec && ( + setIsArgumentsDialogOpen(false)} + onConfirm={handleSubmitWithArguments} + componentSpec={componentSpec} + /> + )} + ); }; diff --git a/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx new file mode 100644 index 000000000..2dcd468fd --- /dev/null +++ b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx @@ -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) => void; + componentSpec: ComponentSpec; +} + +export const SubmitTaskArgumentsDialog = ({ + open, + onCancel, + onConfirm, + componentSpec, +}: SubmitTaskArgumentsDialogProps) => { + const initialArgs = getArgumentsFromInputs(componentSpec); + + const [taskArguments, setTaskArguments] = + useState>(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 ( + + + + Submit Run with Arguments + + {hasInputs + ? "Customize the pipeline input values before submitting." + : "This pipeline has no configurable inputs."} + + + + {hasInputs && ( + + + {inputs.map((input) => ( + + ))} + + + )} + + + + + + + + ); +}; + +interface ArgumentFieldProps { + input: InputSpec; + value: string; + onChange: (name: string, value: string) => void; +} + +const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => { + const handleChange = (e: ChangeEvent) => { + onChange(input.name, e.target.value); + }; + + const typeLabel = typeSpecToString(input.type); + const isRequired = !input.optional; + const placeholder = input.default ?? ""; + + return ( + + + + {input.name} + + + ({typeLabel} + {isRequired ? "*" : ""}) + + + + {input.description && ( + + {input.description} + + )} + + + + ); +};