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
41 changes: 30 additions & 11 deletions src/components/shared/PipelineRunDisplay/PipelineRunsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,32 @@ import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper";
import { Button } from "@/components/ui/button";
import { InlineStack } from "@/components/ui/layout";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { PipelineRun } from "@/types/pipelineRun";

import { RecentRunsTitle } from "./components/RecentRunsTitle";
import { usePipelineRuns } from "./usePipelineRuns";

const DEFAULT_SHOWING_RUNS = 4;

interface PipelineRunsListProps {
pipelineName?: string;
showMoreButton?: boolean;
showTitle?: boolean;
disabled?: boolean;
overviewConfig?: ComponentProps<typeof RunOverview>["config"];
onRunClick?: (run: PipelineRun) => void;
}

export const PipelineRunsList = withSuspenseWrapper(
({
pipelineName,
showMoreButton = true,
showTitle = true,
disabled = false,
overviewConfig,
}: {
pipelineName?: string;
showMoreButton?: boolean;
overviewConfig?: ComponentProps<typeof RunOverview>["config"];
}) => {
onRunClick,
}: PipelineRunsListProps) => {
const { data: pipelineRuns } = usePipelineRuns(pipelineName);

const [showingRuns, setShowingRuns] = useState(DEFAULT_SHOWING_RUNS);
Expand All @@ -31,13 +41,22 @@ export const PipelineRunsList = withSuspenseWrapper(

return (
<>
<RecentRunsTitle
pipelineName={pipelineName}
runsCount={pipelineRuns.length}
/>
<ScrollArea>
{showTitle && (
<RecentRunsTitle
pipelineName={pipelineName}
runsCount={pipelineRuns.length}
/>
)}
<ScrollArea
className={cn(disabled && "opacity-50 pointer-events-none")}
>
{pipelineRuns.slice(0, showingRuns).map((run) => (
<RunOverview key={run.id} run={run} config={overviewConfig} />
<RunOverview
key={run.id}
run={run}
config={overviewConfig}
onClick={onRunClick}
/>
))}
{showMoreButton && pipelineRuns.length > showingRuns && (
<InlineStack className="w-full" align="center">
Expand Down
23 changes: 18 additions & 5 deletions src/components/shared/PipelineRunDisplay/RunOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useNavigate } from "@tanstack/react-router";
import type { MouseEvent } from "react";

import { StatusBar, StatusText } from "@/components/shared/Status/";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -41,6 +42,7 @@ interface RunOverviewProps {
showAuthor?: boolean;
};
className?: string;
onClick?: (run: PipelineRun) => void;
}

const defaultConfig = {
Expand All @@ -53,20 +55,31 @@ const defaultConfig = {
showAuthor: false,
};

const RunOverview = ({ run, config, className = "" }: RunOverviewProps) => {
const RunOverview = ({
run,
config,
className = "",
onClick,
}: RunOverviewProps) => {
const navigate = useNavigate();

const combinedConfig = {
...defaultConfig,
...config,
};

const handleClick = (e: MouseEvent) => {
e.stopPropagation();
if (onClick) {
onClick(run);
} else {
navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` });
}
};

return (
<div
onClick={(e) => {
e.stopPropagation();
navigate({ to: `${APP_ROUTES.RUNS}/${run.id}` });
}}
onClick={handleClick}
className={cn(
"flex flex-col p-2 text-sm hover:bg-gray-50 cursor-pointer",
className,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { type ChangeEvent, useState } from "react";

import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList";
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";
Expand All @@ -11,12 +13,23 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Icon } from "@/components/ui/icon";
import { Input } from "@/components/ui/input";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Paragraph } from "@/components/ui/typography";
import useToastNotification from "@/hooks/useToastNotification";
import { cn } from "@/lib/utils";
import { useBackend } from "@/providers/BackendProvider";
import { fetchExecutionDetails } from "@/services/executionService";
import type { PipelineRun } from "@/types/pipelineRun";
import type { ComponentSpec, InputSpec } from "@/utils/componentSpec";
import { getArgumentValue } from "@/utils/nodes/taskArguments";

interface SubmitTaskArgumentsDialogProps {
open: boolean;
Expand All @@ -31,13 +44,35 @@ export const SubmitTaskArgumentsDialog = ({
onConfirm,
componentSpec,
}: SubmitTaskArgumentsDialogProps) => {
const notify = useToastNotification();
const initialArgs = getArgumentsFromInputs(componentSpec);

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

// Track highlighted args with a version key to re-trigger CSS animation
const [highlightedArgs, setHighlightedArgs] = useState<Map<string, number>>(
new Map(),
);

const inputs = componentSpec.inputs ?? [];

const handleCopyFromRun = (args: Record<string, string>) => {
const diff = Object.entries(args).filter(
([key, value]) => taskArguments[key] !== value,
);

setTaskArguments((prev) => ({
...prev,
...args,
}));

const version = Date.now();
setHighlightedArgs(new Map(diff.map(([key]) => [key, version])));

notify(`Copied ${diff.length} arguments`, "success");
};

const handleValueChange = (name: string, value: string) => {
setTaskArguments((prev) => ({
...prev,
Expand All @@ -59,24 +94,46 @@ export const SubmitTaskArgumentsDialog = ({
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Submit Run with Arguments</DialogTitle>
<DialogDescription>
<DialogDescription className="hidden">
{hasInputs
? "Customize the pipeline input values before submitting."
: "This pipeline has no configurable inputs."}
</DialogDescription>

{hasInputs ? (
<BlockStack gap="2">
<Paragraph tone="subdued" size="sm">
Customize the pipeline input values before submitting.
</Paragraph>
<InlineStack align="end" className="w-full">
<CopyFromRunPopover
componentSpec={componentSpec}
onCopy={handleCopyFromRun}
/>
</InlineStack>
</BlockStack>
) : (
<Paragraph tone="subdued">
This pipeline has no configurable inputs.
</Paragraph>
)}
</DialogHeader>

{hasInputs && (
<ScrollArea className="max-h-[60vh] pr-4">
<ScrollArea className="max-h-[60vh] pr-4 w-full">
<BlockStack gap="4" className="p-1">
{inputs.map((input) => (
<ArgumentField
key={input.name}
input={input}
value={taskArguments[input.name] ?? ""}
onChange={handleValueChange}
/>
))}
{inputs.map((input) => {
const highlightVersion = highlightedArgs.get(input.name);
return (
<ArgumentField
key={`${input.name}-${highlightVersion ?? "static"}`}
input={input}
value={taskArguments[input.name] ?? ""}
onChange={handleValueChange}
isHighlighted={highlightVersion !== undefined}
/>
);
})}
</BlockStack>
</ScrollArea>
)}
Expand All @@ -92,13 +149,84 @@ export const SubmitTaskArgumentsDialog = ({
);
};

const CopyFromRunPopover = ({
componentSpec,
onCopy,
}: {
componentSpec: ComponentSpec;
onCopy: (args: Record<string, string>) => void;
}) => {
const { backendUrl } = useBackend();
const pipelineName = componentSpec.name;

const [popoverOpen, setPopoverOpen] = useState(false);

const { mutate: copyFromRunMutation, isPending: isCopyingFromRun } =
useMutation({
mutationFn: async (run: PipelineRun) => {
const executionDetails = await fetchExecutionDetails(
String(run.root_execution_id),
backendUrl,
);
return executionDetails.task_spec.arguments;
},
onSuccess: (runArguments) => {
if (runArguments) {
const newArgs = Object.fromEntries(
Object.entries(runArguments)
.map(([name, _]) => [name, getArgumentValue(runArguments, name)])
.filter(
(entry): entry is [string, string] => entry[1] !== undefined,
),
);
onCopy(newArgs);
}
setPopoverOpen(false);
},
onError: (error) => {
console.error("Failed to fetch run arguments:", error);
setPopoverOpen(false);
},
});

return (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm">
<Icon name="Copy" />
Copy from recent run
</Button>
</PopoverTrigger>
<PopoverContent className="w-100" align="end">
<PipelineRunsList
pipelineName={pipelineName}
onRunClick={copyFromRunMutation}
showTitle={false}
showMoreButton={true}
overviewConfig={{
showName: false,
showTaskStatusBar: false,
}}
disabled={isCopyingFromRun}
/>
</PopoverContent>
</Popover>
);
};

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

const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
const ArgumentField = ({
input,
value,
onChange,
isHighlighted,
}: ArgumentFieldProps) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(input.name, e.target.value);
};
Expand All @@ -108,7 +236,13 @@ const ArgumentField = ({ input, value, onChange }: ArgumentFieldProps) => {
const placeholder = input.default ?? "";

return (
<BlockStack gap="1">
<BlockStack
gap="1"
className={cn(
"rounded-md px-2 py-1 -mx-2",
isHighlighted && "animate-highlight-fade",
)}
>
<InlineStack gap="2" align="start">
<Paragraph size="sm" className="wrap-break-word">
{input.name}
Expand Down
16 changes: 16 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ code {

/* Custom animations */
--animate-revert-copied: revert-copied 0.5s ease-in-out forwards;
--animate-highlight-fade: highlight-fade 2s ease-out forwards;

@keyframes revert-copied {
0%,
Expand All @@ -159,6 +160,21 @@ code {
transform: rotate(-90deg) scale(0);
}
}

@keyframes highlight-fade {
0% {
background-color: oklch(0.765 0.177 163 / 0.2);
box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.5);
}
70% {
background-color: oklch(0.765 0.177 163 / 0.15);
box-shadow: 0 0 0 2px oklch(0.765 0.177 163 / 0.3);
}
100% {
background-color: transparent;
box-shadow: none;
}
}
}

@layer base {
Expand Down