Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ type ArtifactVisualizerProps = {
name: string;
type: string;
value?: string;
triggerVariant?: "ghost" | "secondary" | "outline";
};

const ArtifactVisualizer = ({
artifact,
name,
type,
value,
triggerVariant = "ghost",
}: ArtifactVisualizerProps) => {
const { track } = useAnalytics();
const [isFullscreen, setIsFullscreen] = useState(false);
Expand Down Expand Up @@ -88,15 +90,15 @@ const ArtifactVisualizer = ({
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{value ? (
<Button variant="ghost" size="xs">
<Button variant={triggerVariant} size="xs">
<Icon
name="Maximize2"
size="xs"
className="text-muted-foreground"
/>
</Button>
) : (
<Button variant="ghost" size="xs">
<Button variant={triggerVariant} size="xs">
<Icon name="Eye" />
Preview
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ArtifactNodeResponse } from "@/api/types.gen";
import ArtifactVisualizer from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer";
import type { TypeSpecType } from "@/models/componentSpec";

const MAX_VISUALIZABLE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB

interface RunViewOutputPreviewProps {
artifact: ArtifactNodeResponse | null | undefined;
name: string;
type?: TypeSpecType;
}

/**
* Renders the artifact Preview trigger for a single task output in RunView.
* Returns null when there is no previewable artifact, mirroring the
* eligibility rules used by IOCell in the task Artifacts panel.
*/
export const RunViewOutputPreview = ({
artifact,
name,
type,
}: RunViewOutputPreviewProps) => {
if (!artifact) return null;

const artifactData = artifact.artifact_data;
const inlineValue = artifactData?.value;
const hasInlineValue = canShowInlineValue(inlineValue);
const hasDetails = Boolean(artifactData?.uri || hasInlineValue);
const isTooLargeToVisualize =
!hasInlineValue &&
!!artifactData?.total_size &&
artifactData.total_size > MAX_VISUALIZABLE_SIZE_BYTES;

if (!hasDetails || isTooLargeToVisualize) return null;

const artifactType =
type?.toString() ??
artifact.type_name ??
(artifactData?.is_dir ? "Directory" : "Any");

return (
<ArtifactVisualizer
triggerVariant="outline"
artifact={artifact}
name={name}
type={artifactType}
value={hasInlineValue ? inlineValue : undefined}
/>
);
};

const canShowInlineValue = (
value: string | null | undefined,
): value is string => !!value && String(value).trim() !== "";
29 changes: 27 additions & 2 deletions src/routes/v2/pages/RunView/nodes/TaskNode/RunViewTaskNode.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { useQuery } from "@tanstack/react-query";
import { type Node, type NodeProps } from "@xyflow/react";
import { observer } from "mobx-react-lite";

import { StatusIndicator } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/StatusIndicator";
import Logs from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/logs";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { TaskNode } from "@/routes/v2/shared/nodes/TaskNode/TaskNode";
import { useBackend } from "@/providers/BackendProvider";
import { OutputActionsProvider } from "@/routes/v2/shared/nodes/TaskNode/OutputActionsContext";
import {
TaskNode,
type TaskNodeOutput,
} from "@/routes/v2/shared/nodes/TaskNode/TaskNode";
import type { TaskNodeData } from "@/routes/v2/shared/nodes/types";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import { getExecutionArtifacts } from "@/services/executionService";
import { tracking } from "@/utils/tracking";

import { RunViewOutputPreview } from "./RunViewOutputPreview";
import { useTaskRunStatus } from "./useTaskRunStatus";

type TaskNodeType = Node<TaskNodeData, "task">;
Expand All @@ -19,9 +27,16 @@ export const RunViewTaskNode = observer(function RunViewTaskNode(
) {
const { entityId } = props.data;
const { windows } = useSharedStores();
const { backendUrl } = useBackend();
const { task, status, disabledCache, executionId, showLogsButton } =
useTaskRunStatus(entityId);

const { data: artifacts } = useQuery({
queryKey: ["artifacts", executionId],
queryFn: () => getExecutionArtifacts(String(executionId), backendUrl),
enabled: !!executionId,
});

const handleOpenLogs = () => {
if (!task || !executionId) return;
windows.openWindow(<Logs executionId={executionId} status={status} />, {
Expand All @@ -31,6 +46,14 @@ export const RunViewTaskNode = observer(function RunViewTaskNode(
});
};

const renderOutputAction = (output: TaskNodeOutput) => (
<RunViewOutputPreview
artifact={artifacts?.output_artifacts?.[output.name]}
name={output.name}
type={output.type}
/>
);

return (
<div className="relative">
{!!status && (
Expand All @@ -49,7 +72,9 @@ export const RunViewTaskNode = observer(function RunViewTaskNode(
</Button>
)}

<TaskNode {...props} />
<OutputActionsProvider value={renderOutputAction}>
<TaskNode {...props} />
</OutputActionsProvider>
</div>
);
});
18 changes: 18 additions & 0 deletions src/routes/v2/shared/nodes/TaskNode/OutputActionsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createContext, type ReactNode, useContext } from "react";

import type { TaskNodeOutput } from "./TaskNode";

type RenderOutputAction = (output: TaskNodeOutput) => ReactNode;

const OutputActionsContext = createContext<RenderOutputAction | null>(null);

export const OutputActionsProvider = OutputActionsContext.Provider;

/**
* Returns the optional per-output action renderer supplied by a surrounding
* provider (e.g. RunView). Returns null when no provider is present, so the
* shared TaskNodeCard stays decoupled from execution-specific behavior.
*/
export function useOutputAction(): RenderOutputAction | null {
return useContext(OutputActionsContext);
}
38 changes: 25 additions & 13 deletions src/routes/v2/shared/nodes/TaskNode/TaskNodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { pluralize } from "@/utils/string";

import { deriveColorPalette } from "./color.utils";
import { InputAggregatorHandle } from "./InputAggregatorHandle";
import { useOutputAction } from "./OutputActionsContext";
import type { TaskNodeInput, TaskNodeViewProps } from "./TaskNode";

const AGGREGATOR_INTERNAL_INPUTS = new Set([
Expand Down Expand Up @@ -217,6 +218,7 @@ export const TaskNodeCard = observer(function TaskNodeCard({
outputType,
onOutputTypeChange,
}: TaskNodeViewProps) {
const renderOutputAction = useOutputAction();
const isDark = useTheme().resolvedTheme === "dark";
const palette = taskColor ? deriveColorPalette(taskColor, isDark) : undefined;
const cardStyle = palette
Expand Down Expand Up @@ -459,19 +461,29 @@ export const TaskNodeCard = observer(function TaskNodeCard({
{`+${hiddenOutputCount} more ${pluralize(hiddenOutputCount, "output")}`}
</div>
)}
<div className="translate-x-3 min-w-0 inline-block max-w-full">
<div
className={cn(
"text-xs rounded-md px-2 py-1 truncate",
palette
? isDark
? "text-gray-100 bg-white/10 hover:bg-white/15"
: "text-gray-800 bg-black/5 hover:bg-black/10"
: "text-foreground bg-muted hover:bg-accent",
)}
title={`${output.name}${output.type ? `: ${output.type}` : ""}`}
>
{output.name.replace(/_/g, " ")}
<div className="flex items-center gap-0.5 min-w-0">
{!showCondensedOutputs && renderOutputAction && (
<span
className="nodrag nopan"
onClick={(e) => e.stopPropagation()}
>
{renderOutputAction(output)}
</span>
)}
<div className="translate-x-3 min-w-0 inline-block max-w-full">
<div
className={cn(
"text-xs rounded-md px-2 py-1 truncate",
palette
? isDark
? "text-gray-100 bg-white/10 hover:bg-white/15"
: "text-gray-800 bg-black/5 hover:bg-black/10"
: "text-foreground bg-muted hover:bg-accent",
)}
title={`${output.name}${output.type ? `: ${output.type}` : ""}`}
>
{output.name.replace(/_/g, " ")}
</div>
</div>
</div>
</div>
Expand Down
Loading