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
144 changes: 102 additions & 42 deletions src/components/shared/Submitters/Oasis/OasisSubmitter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { AlertCircle, CheckCircle, Loader2, SendHorizonal } from "lucide-react";
import { type MouseEvent, useRef, useState } from "react";
import { type DragEvent, type MouseEvent, useRef, useState } from "react";

import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization";
import { useFlagValue } from "@/components/shared/Settings/useFlags";
Expand Down Expand Up @@ -103,6 +103,12 @@ const OasisSubmitter = ({

const [submitSuccess, setSubmitSuccess] = useState<boolean | null>(null);
const [isArgumentsDialogOpen, setIsArgumentsDialogOpen] = useState(false);
const [pendingImportFile, setPendingImportFile] = useState<{
text: string;
extension: string;
} | null>(null);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const dragCounter = useRef(0);
const { cooldownTime, setCooldownTime } = useCooldownTimer(0);
const notify = useToastNotification();
const navigate = useNavigate();
Expand Down Expand Up @@ -307,6 +313,47 @@ const OasisSubmitter = ({
const isArgumentsButtonVisible =
hasConfigurableInputs && !isButtonDisabled && isComponentTreeValid;

const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
dragCounter.current++;
setIsDraggingOver(true);
};

const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDraggingOver(false);
}
};

const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};

const handleDrop = (e: DragEvent) => {
e.preventDefault();
dragCounter.current = 0;
setIsDraggingOver(false);

const file = e.dataTransfer.files[0];
if (!file) return;

const extension = file.name.includes(".")
? `.${file.name.split(".").pop()?.toLowerCase()}`
: "";

const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result;
if (typeof text === "string") {
setPendingImportFile({ text, extension });
setIsArgumentsDialogOpen(true);
}
};
reader.readAsText(file);
};

const getButtonIcon = () => {
if (isSubmitting || isBulkSubmitting) {
return <Loader2 className="animate-spin" />;
Expand All @@ -325,56 +372,69 @@ const OasisSubmitter = ({

return (
<>
<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 && !onlyFixableIssues && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-destructive" : "text-warning",
)}
>
(has validation issues)
</div>
)}
{!available && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-destructive" : "text-warning",
)}
>
{`(backend ${configured ? "unavailable" : "unconfigured"})`}
</div>
)}
</Button>
{isArgumentsButtonVisible && (
<TooltipButton
tooltip="Submit run with arguments"
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
className={cn(
"rounded-md transition-all",
isDraggingOver && "ring-2 ring-primary bg-primary/5",
)}
>
<InlineStack align="space-between" className="pr-2.5">
<Button
onClick={() => handleSubmit()}
className="flex-1 justify-start"
variant="ghost"
size="icon"
data-testid="run-with-arguments-button"
onClick={() => setIsArgumentsDialogOpen(true)}
disabled={!available}
disabled={isButtonDisabled || !available}
>
<Icon name="Split" className="rotate-90" />
</TooltipButton>
)}
</InlineStack>
{getButtonIcon()}
<span className="font-normal text-xs">{getButtonText()}</span>
{!isComponentTreeValid && !onlyFixableIssues && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-destructive" : "text-warning",
)}
>
(has validation issues)
</div>
)}
{!available && (
<div
className={cn(
"text-xs font-light -ml-1",
configured ? "text-destructive" : "text-warning",
)}
>
{`(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}
>
<Icon name="Split" className="rotate-90" />
</TooltipButton>
)}
</InlineStack>
</div>

{componentSpec && (
<SubmitTaskArgumentsDialog
open={isArgumentsDialogOpen}
onCancel={() => setIsArgumentsDialogOpen(false)}
onConfirm={handleSubmitWithArguments}
componentSpec={componentSpec}
initialImportFile={pendingImportFile}
onImportComplete={() => setPendingImportFile(null)}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useMutation } from "@tanstack/react-query";
import yaml from "js-yaml";
import { type ChangeEvent, type KeyboardEvent, useRef, useState } from "react";
import {
type ChangeEvent,
type KeyboardEvent,
useEffect,
useRef,
useState,
} from "react";

import type { TaskSpecOutput } from "@/api/types.gen";
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
Expand All @@ -23,6 +29,12 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Icon } from "@/components/ui/icon";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
Expand Down Expand Up @@ -52,10 +64,14 @@ import {
type InputSpec,
isSecretArgument,
} from "@/utils/componentSpec";
import { generateCsvTemplate } from "@/utils/csvBulkArgumentExport";
import { mapCsvToArguments } from "@/utils/csvBulkArgumentImport";
import { mapJsonToArguments } from "@/utils/jsonBulkArgumentImport";
import { extractTaskArguments } from "@/utils/nodes/taskArguments";
import {
generateCsvTemplate,
generateJsonTemplate,
generateYamlTemplate,
} from "@/utils/templateExport";
import { validateArguments } from "@/utils/validations";

type TaskArguments = TaskSpecOutput["arguments"];
Expand All @@ -69,13 +85,17 @@ interface SubmitTaskArgumentsDialogProps {
bulkInputNames: Set<string>,
) => void;
componentSpec: ComponentSpec;
initialImportFile?: { text: string; extension: string } | null;
onImportComplete?: () => void;
}

export const SubmitTaskArgumentsDialog = ({
open,
onCancel,
onConfirm,
componentSpec,
initialImportFile,
onImportComplete,
}: SubmitTaskArgumentsDialogProps) => {
const notify = useToastNotification();
const initialArgs = getArgumentsFromInputs(componentSpec);
Expand Down Expand Up @@ -103,6 +123,13 @@ export const SubmitTaskArgumentsDialog = ({
!hasBulkMismatch &&
bulkRunCount > 0;

useEffect(() => {
if (initialImportFile && open) {
handleFileImport(initialImportFile.text, initialImportFile.extension);
onImportComplete?.();
}
}, [initialImportFile, open]);

const handleCopyFromRun = (args: Record<string, string>) => {
const diff = Object.entries(args).filter(
([key, value]) => taskArguments[key] !== value,
Expand Down Expand Up @@ -234,7 +261,7 @@ export const SubmitTaskArgumentsDialog = ({
Customize the pipeline input values before submitting.
</Paragraph>
<InlineStack align="end" gap="1" className="w-full">
<DownloadCsvTemplateButton
<DownloadTemplateButton
inputs={inputs}
taskArguments={taskArguments}
bulkInputNames={bulkInputNames}
Expand Down Expand Up @@ -270,7 +297,7 @@ export const SubmitTaskArgumentsDialog = ({
). 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. You can also import a
CSV or JSON file to populate values automatically.
CSV, JSON, or YAML file to populate values automatically.
</Paragraph>
</BlockStack>
)}
Expand Down Expand Up @@ -613,7 +640,34 @@ const ArgumentField = ({
);
};

const DownloadCsvTemplateButton = ({
type TemplateFormat = "csv" | "json" | "yaml";

const templateGenerators: Record<
TemplateFormat,
{
generate: typeof generateCsvTemplate;
mimeType: string;
extension: string;
}
> = {
csv: {
generate: generateCsvTemplate,
mimeType: "text/csv",
extension: "csv",
},
json: {
generate: generateJsonTemplate,
mimeType: "application/json",
extension: "json",
},
yaml: {
generate: generateYamlTemplate,
mimeType: "text/yaml",
extension: "yaml",
},
};

const DownloadTemplateButton = ({
inputs,
taskArguments,
bulkInputNames,
Expand All @@ -624,24 +678,41 @@ const DownloadCsvTemplateButton = ({
bulkInputNames: Set<string>;
pipelineName?: string;
}) => {
const handleDownload = () => {
const csv = generateCsvTemplate(inputs, taskArguments, bulkInputNames);
if (!csv) return;
const downloadFormat = (fmt: TemplateFormat) => {
const { generate, mimeType, extension } = templateGenerators[fmt];
const content = generate(inputs, taskArguments, bulkInputNames);
if (!content) return;

const blob = new Blob([csv], { type: "text/csv" });
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${pipelineName ?? "pipeline"}-arguments.csv`;
a.download = `${pipelineName ?? "pipeline"}-arguments.${extension}`;
a.click();
URL.revokeObjectURL(url);
};

return (
<Button variant="ghost" size="sm" onClick={handleDownload}>
<Icon name="Download" />
Template
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<Icon name="Download" />
Template
<Icon name="ChevronDown" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => downloadFormat("csv")}>
CSV
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => downloadFormat("json")}>
JSON
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => downloadFormat("yaml")}>
YAML
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

Expand Down
Loading
Loading