From 07703ab1e5e61f1735008bf79847af49f01af817 Mon Sep 17 00:00:00 2001 From: Adrien Delannoy Date: Tue, 8 Oct 2024 16:14:07 +0200 Subject: [PATCH] feat(ui): Retry a single workflow step manually (#13343) Signed-off-by: Adrien Delannoy --- .../components/retry-workflow-node-panel.tsx | 100 ++++++++++++++++++ .../workflow-details/workflow-details.tsx | 25 ++++- .../workflow-node-info/workflow-node-info.tsx | 7 ++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 ui/src/app/workflows/components/retry-workflow-node-panel.tsx diff --git a/ui/src/app/workflows/components/retry-workflow-node-panel.tsx b/ui/src/app/workflows/components/retry-workflow-node-panel.tsx new file mode 100644 index 000000000000..b38077ce8f9a --- /dev/null +++ b/ui/src/app/workflows/components/retry-workflow-node-panel.tsx @@ -0,0 +1,100 @@ +import {Checkbox} from 'argo-ui/src/components/checkbox'; +import {Tooltip} from 'argo-ui/src/components/tooltip/tooltip'; +import React, {useState} from 'react'; + +import {Parameter, RetryOpts, Workflow} from '../../../models'; +import {ParametersInput} from '../../shared/components/parameters-input'; +import {services} from '../../shared/services'; +import {Utils} from '../../shared/utils'; +import {ErrorNotice} from '../../shared/components/error-notice'; + +interface Props { + nodeId: string; + workflow: Workflow; + isArchived: boolean; + isWorkflowInCluster: boolean; + onRetrySuccess: () => void; +} + +export function RetryWorkflowNode(props: Props) { + const [overrideParameters, setOverrideParameters] = useState(false); + const [restartSuccessful, setRestartSuccessful] = useState(false); + const [workflowParameters, setWorkflowParameters] = useState(JSON.parse(JSON.stringify(props.workflow.spec.arguments.parameters || []))); + const [error, setError] = useState(); + + async function submit() { + const parameters: RetryOpts['parameters'] = overrideParameters + ? [...workflowParameters.filter(p => Utils.getValueFromParameter(p) !== undefined).map(p => p.name + '=' + Utils.getValueFromParameter(p))] + : []; + const opts: RetryOpts = { + parameters, + restartSuccessful, + nodeFieldSelector: `id=${props.nodeId}` + }; + + try { + props.isArchived && !props.isWorkflowInCluster + ? await services.workflows.retryArchived(props.workflow.metadata.uid, props.workflow.metadata.namespace, opts) + : await services.workflows.retry(props.workflow.metadata.name, props.workflow.metadata.namespace, opts); + props.onRetrySuccess(); + } catch (err) { + setError(err); + } + } + + return ( +
+

Retry Node

+
+ {props.workflow.metadata.namespace}/{props.nodeId} +
+ +

Note: Retrying this node will re-execute this node and all downstream nodes.

+ + {error && } +
+ {/* Override Parameters */} +
+ +
+ +
+
+ + {overrideParameters && ( +
+ + {workflowParameters.length > 0 ? ( + + ) : ( + <> +
+ + + )} +
+ )} + + {/* Restart Successful */} +
+ +
+ +
+
+ + {/* Retry button */} +
+ +
+
+
+ ); +} diff --git a/ui/src/app/workflows/components/workflow-details/workflow-details.tsx b/ui/src/app/workflows/components/workflow-details/workflow-details.tsx index 318d90b1d238..42dae4ed3ce4 100644 --- a/ui/src/app/workflows/components/workflow-details/workflow-details.tsx +++ b/ui/src/app/workflows/components/workflow-details/workflow-details.tsx @@ -43,6 +43,7 @@ import {SuspendInputs} from './suspend-inputs'; import {WorkflowResourcePanel} from './workflow-resource-panel'; import './workflow-details.scss'; +import {RetryWorkflowNode} from '../retry-workflow-node-panel'; function parseSidePanelParam(param: string) { const [type, nodeId, container] = (param || '').split(':'); @@ -102,6 +103,7 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps< const [nodeId, setNodeId] = useState(queryParams.get('nodeId')); const [nodePanelView, setNodePanelView] = useState(queryParams.get('nodePanelView')); const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel')); + const [showRetryNode, setShowRetryNode] = useState(); const [parameters, setParameters] = useState([]); const sidePanelRef = useRef(null); const [workflow, setWorkflow] = useState(); @@ -528,11 +530,29 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps< transition: isSidePanelAnimating ? `width ${ANIMATION_MS}ms` : 'unset', width: isSidePanelExpanded ? `${sidePanelWidth}px` : 0 }}> - +
- {selectedNode && ( + {selectedNode && showRetryNode && ( + setShowRetryNode(false)} + /> + )} + {selectedNode && !showRetryNode && ( setSidePanel(`logs:${x}:${container}`)} onShowEvents={() => setSidePanel(`events:${nodeId}`)} onShowYaml={() => setSidePanel(`yaml:${nodeId}`)} + onRetryNode={() => setShowRetryNode(true)} archived={archived} onResume={() => renderResumePopup()} /> diff --git a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx index 7c4dc96f0a19..66b4b92e578d 100644 --- a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx +++ b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx @@ -67,6 +67,7 @@ interface Props { onTabSelected?: (tabSelected: string) => void; selectedTabKey?: string; onResume?: () => void; + onRetryNode?: () => void; } const AttributeRow = (attr: {title: string; value: any}) => ( @@ -174,6 +175,7 @@ function WorkflowNodeSummary(props: Props) { }); } const showLogs = (x = 'main') => props.onShowContainerLogs(props.node.id, x); + return (
@@ -190,6 +192,11 @@ function WorkflowNodeSummary(props: Props) { MANIFEST )}{' '} + {props.onRetryNode && ['Succeeded', 'Failed'].includes(props.node.phase) && ( + + )}{' '} {props.node.type === 'Pod' && props.onShowContainerLogs && (