Skip to content

Commit

Permalink
feat(ui): Retry a single workflow step manually (#13343)
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Delannoy <[email protected]>
  • Loading branch information
Adrien-D authored Oct 8, 2024
1 parent 35324ce commit 07703ab
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 2 deletions.
100 changes: 100 additions & 0 deletions ui/src/app/workflows/components/retry-workflow-node-panel.tsx
Original file line number Diff line number Diff line change
@@ -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<Parameter[]>(JSON.parse(JSON.stringify(props.workflow.spec.arguments.parameters || [])));
const [error, setError] = useState<Error>();

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 (
<div style={{padding: 16}}>
<h4>Retry Node</h4>
<h5>
{props.workflow.metadata.namespace}/{props.nodeId}
</h5>

<p>Note: Retrying this node will re-execute this node and all downstream nodes.</p>

{error && <ErrorNotice error={error} />}
<div className='white-box'>
{/* Override Parameters */}
<div key='override-parameters' style={{marginBottom: 25}}>
<label>Override Parameters</label>
<div className='columns small-9'>
<Checkbox checked={overrideParameters} onChange={setOverrideParameters} />
</div>
</div>

{overrideParameters && (
<div key='parameters' style={{marginBottom: 25}}>
<label>Parameters</label>
{workflowParameters.length > 0 ? (
<ParametersInput parameters={workflowParameters} onChange={setWorkflowParameters} />
) : (
<>
<br />
<label>No parameters</label>
</>
)}
</div>
)}

{/* Restart Successful */}
<div key='restart-successful' style={{marginBottom: 25}}>
<label>
Restart Successful{' '}
<Tooltip content='Leaving this box unchecked avoids re-running nodes that have run successfully before'>
<i className='fa fa-question-circle' style={{marginLeft: 4}} />
</Tooltip>
</label>
<div className='columns small-9'>
<Checkbox checked={restartSuccessful} onChange={setRestartSuccessful} />
</div>
</div>

{/* Retry button */}
<div key='retry'>
<button onClick={submit} className='argo-button argo-button--base'>
<i className='fa fa-undo-alt' /> Retry
</button>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(':');
Expand Down Expand Up @@ -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<boolean>();
const [parameters, setParameters] = useState<Parameter[]>([]);
const sidePanelRef = useRef<HTMLDivElement>(null);
const [workflow, setWorkflow] = useState<Workflow>();
Expand Down Expand Up @@ -528,11 +530,29 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps<
transition: isSidePanelAnimating ? `width ${ANIMATION_MS}ms` : 'unset',
width: isSidePanelExpanded ? `${sidePanelWidth}px` : 0
}}>
<button className='workflow-details__step-info-close' onClick={() => setNodeId(null)}>
<button
className='workflow-details__step-info-close'
onClick={() => {
if (showRetryNode) {
setShowRetryNode(false);
} else {
setNodeId(null);
}
}}>
<i className='argo-icon-close' />
</button>

<div className='workflow-details__step-info-drag-handle' {...sidePanelDragHandleProps} />
{selectedNode && (
{selectedNode && showRetryNode && (
<RetryWorkflowNode
nodeId={selectedNode.id}
workflow={workflow}
isArchived={isArchivedWorkflow(workflow)}
isWorkflowInCluster={isWorkflowInCluster(workflow)}
onRetrySuccess={() => setShowRetryNode(false)}
/>
)}
{selectedNode && !showRetryNode && (
<WorkflowNodeInfo
node={selectedNode}
onTabSelected={setNodePanelView}
Expand All @@ -542,6 +562,7 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps<
onShowContainerLogs={(x, container) => setSidePanel(`logs:${x}:${container}`)}
onShowEvents={() => setSidePanel(`events:${nodeId}`)}
onShowYaml={() => setSidePanel(`yaml:${nodeId}`)}
onRetryNode={() => setShowRetryNode(true)}
archived={archived}
onResume={() => renderResumePopup()}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ interface Props {
onTabSelected?: (tabSelected: string) => void;
selectedTabKey?: string;
onResume?: () => void;
onRetryNode?: () => void;
}

const AttributeRow = (attr: {title: string; value: any}) => (
Expand Down Expand Up @@ -174,6 +175,7 @@ function WorkflowNodeSummary(props: Props) {
});
}
const showLogs = (x = 'main') => props.onShowContainerLogs(props.node.id, x);

return (
<div className='white-box'>
<div className='white-box__details' style={{paddingBottom: '8px'}}>
Expand All @@ -190,6 +192,11 @@ function WorkflowNodeSummary(props: Props) {
MANIFEST
</Button>
)}{' '}
{props.onRetryNode && ['Succeeded', 'Failed'].includes(props.node.phase) && (
<Button icon='undo-alt' onClick={() => props.onRetryNode()}>
RETRY NODE
</Button>
)}{' '}
{props.node.type === 'Pod' && props.onShowContainerLogs && (
<Button onClick={() => showLogs()} icon='bars'>
LOGS
Expand Down

0 comments on commit 07703ab

Please sign in to comment.