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