diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx index 510190b780..1abf65c3a7 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx @@ -33,10 +33,11 @@ const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)` function WorkflowNodeHelp({ node, i18n }) { let nodeType; - if (node.unifiedJobTemplate || node.job) { + const job = node?.originalNodeObject?.summary_fields?.job; + if (node.unifiedJobTemplate || job) { const type = node.unifiedJobTemplate ? node.unifiedJobTemplate.unified_job_type || node.unifiedJobTemplate.type - : node.job.type; + : job.type; switch (type) { case 'job_template': case 'job': @@ -64,8 +65,8 @@ function WorkflowNodeHelp({ node, i18n }) { } let jobStatus; - if (node.job) { - switch (node.job.status) { + if (job) { + switch (job.status) { case 'new': jobStatus = i18n._(t`New`); break; @@ -112,23 +113,22 @@ function WorkflowNodeHelp({ node, i18n }) { return ( <> - {!node.unifiedJobTemplate && - (!node.job || node.job.type !== 'workflow_approval') && ( - <> - - - - The resource associated with this node has been deleted. - - - - )} - {node.job && ( + {!node.unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && ( + <> + + + + The resource associated with this node has been deleted. + + + + )} + {job && (
{i18n._(t`Name`)}
-
{node.job.name}
+
{job.name}
{i18n._(t`Type`)}
@@ -137,19 +137,19 @@ function WorkflowNodeHelp({ node, i18n }) { {i18n._(t`Job Status`)}
{jobStatus}
- {typeof node.job.elapsed === 'number' && ( + {typeof job.elapsed === 'number' && ( <>
{i18n._(t`Elapsed`)}
- {secondsToHHMMSS(node.job.elapsed)} + {secondsToHHMMSS(job.elapsed)}
)}
)} - {node.unifiedJobTemplate && !node.job && ( + {node.unifiedJobTemplate && !job && (
{i18n._(t`Name`)} @@ -161,7 +161,7 @@ function WorkflowNodeHelp({ node, i18n }) {
{nodeType}
)} - {node.job && node.job.type !== 'workflow_approval' && ( + {job && job.type !== 'workflow_approval' && (

{i18n._(t`Click to view job details`)}

)} diff --git a/awx/ui_next/src/components/Workflow/workflowReducer.js b/awx/ui_next/src/components/Workflow/workflowReducer.js index eddd7dce2a..36171811e4 100644 --- a/awx/ui_next/src/components/Workflow/workflowReducer.js +++ b/awx/ui_next/src/components/Workflow/workflowReducer.js @@ -365,9 +365,6 @@ function generateNodes(workflowNodes, i18n) { originalNodeObject: node, }; - if (node.summary_fields.job) { - nodeObj.job = node.summary_fields.job; - } if (node.summary_fields.unified_job_template) { nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template; } diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx index 24f60de2a6..b875e9c768 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx @@ -64,24 +64,25 @@ Elapsed.displayName = 'Elapsed'; function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { const history = useHistory(); const { nodePositions } = useContext(WorkflowStateContext); + const job = node?.originalNodeObject?.summary_fields?.job; let borderColor = '#93969A'; - if (node.job) { + if (job) { if ( - node.job.status === 'failed' || - node.job.status === 'error' || - node.job.status === 'canceled' + job.status === 'failed' || + job.status === 'error' || + job.status === 'canceled' ) { borderColor = '#d9534f'; } - if (node.job.status === 'successful' || node.job.status === 'ok') { + if (job.status === 'successful' || job.status === 'ok') { borderColor = '#5cb85c'; } } const handleNodeClick = () => { - if (node.job && node.job.type !== 'workflow_aproval') { - history.push(`/jobs/${node.job.id}/details`); + if (job && job.type !== 'workflow_aproval') { + history.push(`/jobs/${job.id}/details`); } }; @@ -90,7 +91,7 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { id={`node-${node.id}`} transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id] .y - nodePositions[1].y})`} - job={node.job} + job={job} onClick={handleNodeClick} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave} @@ -106,14 +107,14 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { /> - {node.job ? ( + {job ? ( <> - {node.job.status && } -

{node.job.name || node.unifiedJobTemplate.name}

+ {job.status && } +

{job.name || node.unifiedJobTemplate.name}

- {!!node?.job?.elapsed && ( - {secondsToHHMMSS(node.job.elapsed)} + {!!job?.elapsed && ( + {secondsToHHMMSS(job.elapsed)} )} ) : ( @@ -125,7 +126,7 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { )}
- {(node.unifiedJobTemplate || node.job) && ( + {(node.unifiedJobTemplate || job) && ( )} diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js b/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js index 76bd3c1e9c..254026d757 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js @@ -1,5 +1,18 @@ import { useState, useEffect } from 'react'; import useWebsocket from '../../../util/useWebsocket'; +import { WorkflowJobsAPI } from '../../../api'; + +const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => { + const { data } = await WorkflowJobsAPI.readNodes(jobId, { + page_size: 200, + page: pageNo, + }); + + if (data.next) { + return fetchWorkflowNodes(jobId, pageNo + 1, nodes.concat(data.results)); + } + return nodes.concat(data.results); +}; export default function useWsWorkflowOutput(workflowJobId, initialNodes) { const [nodes, setNodes] = useState(initialNodes); @@ -14,20 +27,53 @@ export default function useWsWorkflowOutput(workflowJobId, initialNodes) { useEffect( function parseWsMessage() { - if ( - !nodes || - nodes.length === 0 || - lastMessage?.workflow_job_id !== workflowJobId - ) { - return; + async function refreshNodeObjects() { + const refreshedNodes = []; + const updatedNodeObjects = await fetchWorkflowNodes(workflowJobId); + const updatedNodeObjectsMap = updatedNodeObjects.reduce((map, node) => { + map[node.id] = node; + return map; + }, {}); + nodes.forEach(node => { + if (node.id === 1) { + // This is our artificial start node + refreshedNodes.push({ + ...node, + }); + } else { + refreshedNodes.push({ + ...node, + originalNodeObject: + updatedNodeObjectsMap[node.originalNodeObject.id], + }); + } + }); + setNodes(refreshedNodes); } - const index = nodes.findIndex( - node => node?.originalNodeObject?.id === lastMessage.workflow_node_id - ); + if ( + lastMessage?.unified_job_id === workflowJobId && + ['successful', 'failed', 'error', 'cancelled'].includes( + lastMessage.status + ) + ) { + refreshNodeObjects(); + } else { + if ( + !nodes || + nodes.length === 0 || + lastMessage?.workflow_job_id !== workflowJobId + ) { + return; + } - if (index > -1) { - setNodes(updateNode(nodes, index, lastMessage)); + const index = nodes.findIndex( + node => node?.originalNodeObject?.id === lastMessage.workflow_node_id + ); + + if (index > -1) { + setNodes(updateNode(nodes, index, lastMessage)); + } } }, [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps