From afad2c5e655af0ab8e0866b8c3985774fc8a4ebf Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Wed, 4 Mar 2026 11:53:49 -0500 Subject: [PATCH] bulk submissions --- .../Submitters/Oasis/OasisSubmitter.tsx | 82 ++++++++++- .../components/SubmitTaskArgumentsDialog.tsx | 136 ++++++++++++++++-- src/utils/bulkSubmission.test.ts | 127 ++++++++++++++++ src/utils/bulkSubmission.ts | 96 +++++++++++++ 4 files changed, 424 insertions(+), 17 deletions(-) create mode 100644 src/utils/bulkSubmission.test.ts create mode 100644 src/utils/bulkSubmission.ts diff --git a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx index f7d47228b..4c100e44d 100644 --- a/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx +++ b/src/components/shared/Submitters/Oasis/OasisSubmitter.tsx @@ -15,6 +15,7 @@ import { useBackend } from "@/providers/BackendProvider"; import { APP_ROUTES } from "@/routes/router"; import { updateRunNotes } from "@/services/pipelineRunService"; import type { PipelineRun } from "@/types/pipelineRun"; +import { expandBulkArguments } from "@/utils/bulkSubmission"; import { type ArgumentType, type ComponentSpec, @@ -107,6 +108,11 @@ const OasisSubmitter = ({ const navigate = useNavigate(); const runNotes = useRef(""); + const [bulkProgress, setBulkProgress] = useState<{ + total: number; + completed: number; + failed: number; + } | null>(null); const { mutate: saveNotes } = useMutation({ mutationFn: (runId: string) => @@ -198,18 +204,85 @@ const OasisSubmitter = ({ }); }; - const handleSubmitWithArguments = ( + const handleBulkSubmit = async (argSets: Record[]) => { + if (!componentSpec) { + handleError("No pipeline to submit"); + return; + } + + const total = argSets.length; + setBulkProgress({ total, completed: 0, failed: 0 }); + setSubmitSuccess(null); + + let completed = 0; + let failed = 0; + + for (const args of argSets) { + try { + const response = await new Promise((resolve, reject) => { + submit({ + componentSpec, + taskArguments: args, + onSuccess: resolve, + onError: reject, + }); + }); + + if (runNotes.current.trim() !== "") { + saveNotes(response.id.toString()); + } + + completed++; + } catch { + failed++; + } + + setBulkProgress({ total, completed, failed }); + } + + setBulkProgress(null); + setSubmitSuccess(failed === 0); + setCooldownTime(3); + + if (failed === 0) { + onSubmitComplete?.(); + notify(`All ${total} runs submitted successfully`, "success"); + } else { + notify( + `${completed} of ${total} runs submitted. ${failed} failed.`, + failed === total ? "error" : "warning", + ); + } + }; + + const handleSubmitWithArguments = async ( args: Record, notes: string, + bulkInputNames: Set, ) => { runNotes.current = notes; setIsArgumentsDialogOpen(false); - handleSubmit(args); + + if (bulkInputNames.size === 0) { + handleSubmit(args); + return; + } + + try { + const argSets = expandBulkArguments(args, bulkInputNames); + await handleBulkSubmit(argSets); + } catch (error) { + notify(`Bulk submission failed: ${String(error)}`, "error"); + } }; const hasConfigurableInputs = (componentSpec?.inputs?.length ?? 0) > 0; const getButtonText = () => { + if (bulkProgress) { + const current = bulkProgress.completed + bulkProgress.failed; + return `Submitting ${current + 1} of ${bulkProgress.total}...`; + } if (cooldownTime > 0) { return `Run submitted (${cooldownTime}s)`; } @@ -228,13 +301,14 @@ const OasisSubmitter = ({ isGraphImplementation(componentSpec.implementation) && Object.keys(componentSpec.implementation.graph.tasks).length > 0; - const isButtonDisabled = isSubmitting || !isSubmittable; + const isBulkSubmitting = bulkProgress !== null; + const isButtonDisabled = isSubmitting || isBulkSubmitting || !isSubmittable; const isArgumentsButtonVisible = hasConfigurableInputs && !isButtonDisabled && isComponentTreeValid; const getButtonIcon = () => { - if (isSubmitting) { + if (isSubmitting || isBulkSubmitting) { return ; } if (submitSuccess === false && cooldownTime > 0) { diff --git a/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx index 3d2058dad..135eedab3 100644 --- a/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx +++ b/src/components/shared/Submitters/Oasis/components/SubmitTaskArgumentsDialog.tsx @@ -17,6 +17,7 @@ import { createSecretArgument, extractSecretName, } from "@/components/shared/SecretsManagement/types"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -28,6 +29,7 @@ import { } from "@/components/ui/dialog"; import { Icon } from "@/components/ui/icon"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Popover, @@ -36,6 +38,7 @@ import { } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Spinner } from "@/components/ui/spinner"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { Paragraph } from "@/components/ui/typography"; import useToastNotification from "@/hooks/useToastNotification"; @@ -46,6 +49,7 @@ import { fetchPipelineRun, } from "@/services/executionService"; import type { PipelineRun } from "@/types/pipelineRun"; +import { getBulkRunCount, parseBulkValues } from "@/utils/bulkSubmission"; import { type ArgumentType, type ComponentSpec, @@ -60,7 +64,11 @@ type TaskArguments = TaskSpecOutput["arguments"]; interface SubmitTaskArgumentsDialogProps { open: boolean; onCancel: () => void; - onConfirm: (args: Record, notes: string) => void; + onConfirm: ( + args: Record, + notes: string, + bulkInputNames: Set, + ) => void; componentSpec: ComponentSpec; } @@ -82,8 +90,15 @@ export const SubmitTaskArgumentsDialog = ({ new Map(), ); + const [bulkInputNames, setBulkInputNames] = useState>(new Set()); + const inputs = componentSpec.inputs ?? []; + const bulkRunCount = getBulkRunCount(taskArguments, bulkInputNames); + const hasBulkMismatch = bulkRunCount === -1; + const isBulkMode = bulkInputNames.size > 0; + const effectiveRunCount = isBulkMode ? Math.max(bulkRunCount, 0) : 1; + const [isValidToSubmit, setIsValidToSubmit] = useState( validateArguments(inputs, taskArguments), ); @@ -112,19 +127,31 @@ export const SubmitTaskArgumentsDialog = ({ }; useEffect(() => { - setIsValidToSubmit(validateArguments(inputs, taskArguments)); - }, [inputs, taskArguments]); - - const handleRunNotesChange = (value: string) => { - setRunNotes(value); + const baseValid = validateArguments(inputs, taskArguments); + const bulkValid = !hasBulkMismatch && bulkRunCount > 0; + setIsValidToSubmit(baseValid && bulkValid); + }, [inputs, taskArguments, hasBulkMismatch, bulkRunCount]); + + const handleBulkToggle = (name: string, enabled: boolean) => { + setBulkInputNames((prev) => { + const next = new Set(prev); + if (enabled) { + next.add(name); + } else { + next.delete(name); + } + return next; + }); }; - const handleConfirm = () => onConfirm(taskArguments, runNotes); + const handleConfirm = () => + onConfirm(taskArguments, runNotes, bulkInputNames); const handleCancel = () => { setTaskArguments(initialArgs); setRunNotes(""); setHighlightedArgs(new Map()); + setBulkInputNames(new Set()); onCancel(); }; @@ -160,18 +187,48 @@ export const SubmitTaskArgumentsDialog = ({ )} + {isBulkMode && ( + + + Bulk mode + + + Enter comma-separated values for bulk inputs (e.g.{" "} + + A, B, C + + ). Each value creates a separate run. Non-bulk inputs reuse their + single value across all runs. If multiple inputs are set to bulk, + they must have the same number of values. + + + )} + {hasInputs && ( {inputs.map((input) => { const highlightVersion = highlightedArgs.get(input.name); + const isBulkEnabled = bulkInputNames.has(input.name); + const currentValue = taskArguments[input.name]; + const bulkValueCount = + isBulkEnabled && typeof currentValue === "string" + ? parseBulkValues(currentValue).length + : 0; + return ( ); })} @@ -185,18 +242,39 @@ export const SubmitTaskArgumentsDialog = ({