diff --git a/src/components/shared/Execution/PipelineIO.tsx b/src/components/shared/Execution/PipelineIO.tsx
index 7ff7f5396..4ad7d5c4b 100644
--- a/src/components/shared/Execution/PipelineIO.tsx
+++ b/src/components/shared/Execution/PipelineIO.tsx
@@ -20,8 +20,15 @@ import { getOutputConnectedDetails } from "../../Editor/utils/getOutputConnected
const PipelineIO = ({
taskArguments,
+ section,
}: {
taskArguments?: TaskSpecOutput["arguments"] | null;
+ /**
+ * When set, renders only one side (without the wrapping ContentBlock) so the
+ * caller can supply its own section header (e.g. a collapsible section).
+ * When omitted, renders both Inputs/Arguments and Outputs blocks.
+ */
+ section?: "inputs" | "outputs";
}) => {
const { setContent } = useContextPanel();
const { componentSpec, graphSpec } = useComponentSpec();
@@ -71,58 +78,72 @@ const PipelineIO = ({
},
];
+ const hasInputs = !!componentSpec.inputs && componentSpec.inputs.length > 0;
+ const hasOutputs =
+ !!componentSpec.outputs && componentSpec.outputs.length > 0;
+
+ const inputsContent = hasInputs ? (
+
+ {componentSpec.inputs?.map((input) => (
+
+ ))}
+
+ ) : (
+
+ No inputs
+
+ );
+
+ const outputsContent = hasOutputs ? (
+
+ {componentSpec.outputs?.map((output) => {
+ const connectedOutput = getOutputConnectedDetails(
+ graphSpec,
+ output.name,
+ );
+
+ return (
+
+ );
+ })}
+
+ ) : (
+
+ No outputs
+
+ );
+
+ if (section === "inputs") {
+ return inputsContent;
+ }
+
+ if (section === "outputs") {
+ return outputsContent;
+ }
+
return (
- {componentSpec.inputs && componentSpec.inputs.length > 0 ? (
-
- {componentSpec.inputs.map((input) => (
-
- ))}
-
- ) : (
-
- No inputs
-
- )}
-
-
- {componentSpec.outputs && componentSpec.outputs.length > 0 ? (
-
- {componentSpec.outputs.map((output) => {
- const connectedOutput = getOutputConnectedDetails(
- graphSpec,
- output.name,
- );
-
- return (
-
- );
- })}
-
- ) : (
-
- No outputs
-
- )}
+ {inputsContent}
+ {outputsContent}
);
};
diff --git a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
index c3bb77c02..0ac926913 100644
--- a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
+++ b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
@@ -10,6 +10,7 @@ import { useAnalytics } from "@/providers/AnalyticsProvider";
import { AnnotationsBlock } from "@/routes/v2/pages/Editor/components/AnnotationsBlock/AnnotationsBlock";
import { ValidationSummary } from "@/routes/v2/pages/Editor/components/ValidationSummary";
import { usePipelineActions } from "@/routes/v2/pages/Editor/store/actions/usePipelineActions";
+import { PipelineDetailsCollapsibleSection } from "@/routes/v2/shared/components/PipelineDetailsCollapsibleSection";
import { useSpec } from "@/routes/v2/shared/providers/SpecContext";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import {
@@ -20,7 +21,6 @@ import {
import { InputsBlock } from "./components/InputsBlock";
import { MetadataBlock } from "./components/MetadataBlock";
import { OutputsBlock } from "./components/OutputsBlock";
-import { PipelineDetailsCollapsibleSection } from "./components/PipelineDetailsCollapsibleSection";
import { PipelineDetailsHeader } from "./components/PipelineDetailsHeader";
import { PipelineDetailsTextField } from "./components/PipelineDetailsTextField";
import { TagsBlock } from "./components/TagsBlock";
diff --git a/src/routes/v2/pages/RunView/components/RunActionsBar.tsx b/src/routes/v2/pages/RunView/components/RunActionsBar.tsx
new file mode 100644
index 000000000..93527dcef
--- /dev/null
+++ b/src/routes/v2/pages/RunView/components/RunActionsBar.tsx
@@ -0,0 +1,183 @@
+import TooltipButton from "@/components/shared/Buttons/TooltipButton";
+import ConfirmationDialog from "@/components/shared/Dialogs/ConfirmationDialog";
+import { StatusBar } from "@/components/shared/Status";
+import TaskImplementation from "@/components/shared/TaskDetails/Implementation";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Icon } from "@/components/ui/icon";
+import { BlockStack, InlineStack } from "@/components/ui/layout";
+import { Text } from "@/components/ui/typography";
+import { useCancelPipelineRun } from "@/routes/v2/pages/RunView/hooks/useCancelPipelineRun";
+import { useClonePipelineRun } from "@/routes/v2/pages/RunView/hooks/useClonePipelineRun";
+import { useExportPipelineYaml } from "@/routes/v2/pages/RunView/hooks/useExportPipelineYaml";
+import { useInspectPipeline } from "@/routes/v2/pages/RunView/hooks/useInspectPipeline";
+import { useRerunPipelineRun } from "@/routes/v2/pages/RunView/hooks/useRerunPipelineRun";
+import { useRunViewActions } from "@/routes/v2/pages/RunView/hooks/useRunViewActions";
+import { useYamlViewer } from "@/routes/v2/pages/RunView/hooks/useYamlViewer";
+import type { ExecutionStatusStats } from "@/utils/executionStatus";
+import { tracking } from "@/utils/tracking";
+
+interface RunActionsBarProps {
+ executionStatusStats: ExecutionStatusStats | null | undefined;
+ statusLabel: string;
+}
+
+export function RunActionsBar({
+ executionStatusStats,
+ statusLabel,
+}: RunActionsBarProps) {
+ const actions = useRunViewActions();
+ const componentSpec = actions.ready ? actions.componentSpec : undefined;
+ const runId = actions.ready ? actions.runId : undefined;
+ const pipelineName = actions.ready ? actions.pipelineName : undefined;
+
+ const { yamlViewerOpen, openYamlViewer, closeYamlViewer } = useYamlViewer();
+ const { clone, isCloning } = useClonePipelineRun(componentSpec, runId);
+ const { rerun, isRerunning } = useRerunPipelineRun(componentSpec);
+ const {
+ cancelDialogOpen,
+ isCancelling,
+ requestCancel,
+ confirmCancel,
+ dismissCancel,
+ } = useCancelPipelineRun(runId);
+ const { inspect } = useInspectPipeline(pipelineName);
+ const { exportYaml } = useExportPipelineYaml(componentSpec, pipelineName);
+
+ const status = (
+
+
+ {statusLabel}
+
+
+
+ );
+
+ if (!actions.ready) {
+ return (
+
+ {status}
+
+ );
+ }
+
+ const { canAccessEditorSpec, isRunCreator, isInProgress, isComplete } =
+ actions;
+ const showCancel = isInProgress && isRunCreator;
+ const showSeparator = showCancel || isComplete;
+
+ return (
+ <>
+
+ {status}
+
+
+
+
+
+
+
+
+
+
+
+
+ {canAccessEditorSpec && pipelineName && (
+
+
+ Inspect Pipeline
+
+ )}
+
+
+
+ Clone Pipeline
+
+
+
+
+ Export YAML
+
+
+ {showSeparator && }
+
+ {showCancel && (
+
+
+ Cancel Run
+
+ )}
+
+ {isComplete && (
+
+
+ Rerun Pipeline
+
+ )}
+
+
+
+
+
+
+
+ {yamlViewerOpen && (
+
+ )}
+ >
+ );
+}
diff --git a/src/routes/v2/pages/RunView/components/RunDetailsContent.tsx b/src/routes/v2/pages/RunView/components/RunDetailsContent.tsx
index 71a89824e..bfe29a0cf 100644
--- a/src/routes/v2/pages/RunView/components/RunDetailsContent.tsx
+++ b/src/routes/v2/pages/RunView/components/RunDetailsContent.tsx
@@ -9,22 +9,22 @@ import { RunNotesEditor } from "@/components/PipelineRun/RunNotesEditor";
import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock";
import { KeyValueList } from "@/components/shared/ContextPanel/Blocks/KeyValueList";
import { TextBlock } from "@/components/shared/ContextPanel/Blocks/TextBlock";
-import { CopyText } from "@/components/shared/CopyText/CopyText";
import PipelineIO from "@/components/shared/Execution/PipelineIO";
import { InfoBox } from "@/components/shared/InfoBox";
import { LoadingScreen } from "@/components/shared/LoadingScreen";
import { useFlagValue } from "@/components/shared/Settings/useFlags";
-import { StatusBar } from "@/components/shared/Status";
import { TagList } from "@/components/shared/Tags/TagList";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
-import { BlockStack, InlineStack } from "@/components/ui/layout";
-import { Paragraph, Text } from "@/components/ui/typography";
+import { BlockStack } from "@/components/ui/layout";
+import { Separator } from "@/components/ui/separator";
+import { Paragraph } from "@/components/ui/typography";
import { useUserDetails } from "@/hooks/useUserDetails";
import type { ComponentSpec } from "@/models/componentSpec";
import { useBackend } from "@/providers/BackendProvider";
import { useExecutionData } from "@/providers/ExecutionDataProvider";
import { useStartOptimizationChat } from "@/routes/v2/pages/RunView/hooks/useStartOptimizationChat";
+import { PipelineDetailsCollapsibleSection } from "@/routes/v2/shared/components/PipelineDetailsCollapsibleSection";
import { useSpec } from "@/routes/v2/shared/providers/SpecContext";
import {
PIPELINE_NOTES_ANNOTATION,
@@ -37,6 +37,8 @@ import {
getOverallExecutionStatusFromStats,
} from "@/utils/executionStatus";
+import { RunDetailsHeader } from "./RunDetailsHeader";
+
export const RunDetailsContent = observer(function RunDetailsContent() {
const { configured } = useBackend();
const spec = useSpec();
@@ -109,7 +111,6 @@ function RunDetailsContentLoaded({
const specAnnotations = spec.annotations;
const pipelineNotes = specAnnotations.get(PIPELINE_NOTES_ANNOTATION);
-
const tags = specAnnotations.get(PIPELINE_TAGS_ANNOTATION);
const displayedAnnotations = specAnnotations
@@ -117,68 +118,91 @@ function RunDetailsContentLoaded({
.map((a) => ({ label: a.key, value: String(a.value) }));
return (
-
-
- {spec.name ?? "Unnamed Pipeline"}
-
-
-
-
- {metadata && }
+
+
- {spec.description && (
-
- )}
+
-
-
-
- {statusLabel}
-
-
-
-
+
+
+ {metadata ? (
+
+ ) : (
+
+ No run information available.
+
+ )}
+
- {displayedAnnotations.length > 0 && (
-
- )}
+
+
+
+
+
+
-
+
+
+
-
+
+
+
-
-
-
+
+
+
+ Metrics and optimizations
+
+
+
+
+
);
}
-function OptimizationButton() {
- const aiEnabled = useFlagValue("ai-assistant");
- const startOptimizationChat = useStartOptimizationChat();
-
- if (!aiEnabled) return null;
-
- return (
-
- );
-}
-
function RunInfoSection({ metadata }: { metadata: PipelineRunResponse }) {
return (
+ {description && }
+
+
+
+
+
+ {annotations.length > 0 && (
+
+ )}
+
+ );
+}
+
interface NotesSectionProps {
pipelineNotes: string | undefined;
metadata: PipelineRunResponse | undefined;
@@ -209,21 +259,37 @@ function NotesSection({
!!currentUserId && metadata?.created_by === currentUserId;
return (
-
-
+
+
+ Pipeline Notes
+
+ {pipelineNotes || "No notes available for this pipeline."}
+
+
+ {!!metadata?.id && (
- Pipeline Notes
-
- {pipelineNotes || "No notes available for this pipeline."}
-
+ Run Notes
+
- {!!metadata?.id && (
-
- Run Notes
-
-
- )}
-
-
+ )}
+
+ );
+}
+
+function SuggestOptimizationButton() {
+ const aiEnabled = useFlagValue("ai-assistant");
+ const startOptimizationChat = useStartOptimizationChat();
+
+ if (!aiEnabled) return null;
+
+ return (
+
);
}
diff --git a/src/routes/v2/pages/RunView/components/RunDetailsHeader.tsx b/src/routes/v2/pages/RunView/components/RunDetailsHeader.tsx
new file mode 100644
index 000000000..2e9777184
--- /dev/null
+++ b/src/routes/v2/pages/RunView/components/RunDetailsHeader.tsx
@@ -0,0 +1,33 @@
+import { CopyText } from "@/components/shared/CopyText/CopyText";
+import { BlockStack } from "@/components/ui/layout";
+import type { ExecutionStatusStats } from "@/utils/executionStatus";
+
+import { RunActionsBar } from "./RunActionsBar";
+
+interface RunDetailsHeaderProps {
+ pipelineName: string;
+ executionStatusStats: ExecutionStatusStats | null | undefined;
+ statusLabel: string;
+}
+
+export function RunDetailsHeader({
+ pipelineName,
+ executionStatusStats,
+ statusLabel,
+}: RunDetailsHeaderProps) {
+ return (
+
+
+ {pipelineName}
+
+
+
+
+ );
+}
diff --git a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/components/PipelineDetailsCollapsibleSection.tsx b/src/routes/v2/shared/components/PipelineDetailsCollapsibleSection.tsx
similarity index 100%
rename from src/routes/v2/pages/Editor/components/PipelineDetailsContent/components/PipelineDetailsCollapsibleSection.tsx
rename to src/routes/v2/shared/components/PipelineDetailsCollapsibleSection.tsx