From 3e99e94b8ce7ba8ce09c6d6d02f8e7e31b37e9fe Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 08:55:41 -0400 Subject: [PATCH 01/10] Redirect user to visualizer page after successful workflow creation --- .../Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx index 0409728aefd1..d9edd770307c 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx @@ -30,7 +30,7 @@ function WorkflowJobTemplateAdd() { data: { id }, } = await WorkflowJobTemplatesAPI.create(templatePayload); await Promise.all(await submitLabels(id, labels, organizationId)); - history.push(`/templates/workflow_job_template/${id}/details`); + history.push(`/templates/workflow_job_template/${id}/visualizer`); } catch (err) { setFormSubmitError(err); } From 5d4ef86db7e3e298e349f59b9505b76f883ea6ae Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 09:38:03 -0400 Subject: [PATCH 02/10] Rename inv/inv source ws functions --- .../src/screens/Inventory/InventoryList/useWsInventories.js | 2 +- .../screens/Inventory/InventorySources/useWsInventorySources.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js index eea15f9a2c28..60fae9e90e4b 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import useWebsocket from '../../../util/useWebsocket'; import useThrottle from '../../../util/useThrottle'; -export default function useWsProjects( +export default function useWsInventories( initialInventories, fetchInventoriesById ) { diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js index 49a9b4f38742..5ea9a5222f23 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js +++ b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import useWebsocket from '../../../util/useWebsocket'; -export default function useWsJobs(initialSources) { +export default function useWsInventorySources(initialSources) { const [sources, setSources] = useState(initialSources); const lastMessage = useWebsocket({ jobs: ['status_changed'], From 328e503f5b90be37b8bd1d885adb9ee9a22b2976 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 11:19:00 -0400 Subject: [PATCH 03/10] Update workflow node job status based on websocket messages --- .../components/Workflow/workflowReducer.js | 2 + .../Job/WorkflowOutput/WorkflowOutput.jsx | 7 ++ .../Job/WorkflowOutput/WorkflowOutputNode.jsx | 6 +- .../Job/WorkflowOutput/useWsWorkflowOutput.js | 65 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js diff --git a/awx/ui_next/src/components/Workflow/workflowReducer.js b/awx/ui_next/src/components/Workflow/workflowReducer.js index 1fe635c47e0c..eddd7dce2aa4 100644 --- a/awx/ui_next/src/components/Workflow/workflowReducer.js +++ b/awx/ui_next/src/components/Workflow/workflowReducer.js @@ -64,6 +64,8 @@ export default function visualizerReducer(state, action) { return { ...state, linkToDelete: action.value }; case 'SET_LINK_TO_EDIT': return { ...state, linkToEdit: action.value }; + case 'SET_NODES': + return { ...state, nodes: action.value }; case 'SET_NODE_POSITIONS': return { ...state, nodePositions: action.value }; case 'SET_NODE_TO_DELETE': diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx index 23a87207a69d..8c50bab16c9c 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx @@ -16,6 +16,7 @@ import workflowReducer, { import { WorkflowJobsAPI } from '../../../api'; import WorkflowOutputGraph from './WorkflowOutputGraph'; import WorkflowOutputToolbar from './WorkflowOutputToolbar'; +import useWsWorkflowOutput from './useWsWorkflowOutput'; const CardBody = styled(PFCardBody)` display: flex; @@ -79,6 +80,12 @@ function WorkflowOutput({ job, i18n }) { } }, [job.id, links, nodes]); + const updatedNodes = useWsWorkflowOutput(job.id, nodes); + + useEffect(() => { + dispatch({ type: 'SET_NODES', value: updatedNodes }); + }, [updatedNodes]); + if (isLoading) { return ( diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx index 4a566834ac37..24f60de2a65b 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx @@ -110,9 +110,11 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { <> {node.job.status && } -

{node.job.name}

+

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

- {secondsToHHMMSS(node.job.elapsed)} + {!!node?.job?.elapsed && ( + {secondsToHHMMSS(node.job.elapsed)} + )} ) : ( diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js b/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js new file mode 100644 index 000000000000..76bd3c1e9c16 --- /dev/null +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/useWsWorkflowOutput.js @@ -0,0 +1,65 @@ +import { useState, useEffect } from 'react'; +import useWebsocket from '../../../util/useWebsocket'; + +export default function useWsWorkflowOutput(workflowJobId, initialNodes) { + const [nodes, setNodes] = useState(initialNodes); + const lastMessage = useWebsocket({ + jobs: ['status_changed'], + control: ['limit_reached_1'], + }); + + useEffect(() => { + setNodes(initialNodes); + }, [initialNodes]); + + useEffect( + function parseWsMessage() { + if ( + !nodes || + nodes.length === 0 || + lastMessage?.workflow_job_id !== workflowJobId + ) { + return; + } + + 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 + ); + + return nodes; +} + +function updateNode(nodes, index, message) { + const node = { + ...nodes[index], + originalNodeObject: { + ...nodes[index]?.originalNodeObject, + job: message.unified_job_id, + summary_fields: { + ...nodes[index]?.originalNodeObject?.summary_fields, + job: { + ...nodes[index]?.originalNodeObject?.summary_fields?.job, + id: message.unified_job_id, + status: message.status, + type: message.type, + }, + }, + }, + job: { + ...nodes[index]?.job, + id: message.unified_job_id, + name: nodes[index]?.job?.name || nodes[index]?.unifiedJobTemplate?.name, + status: message.status, + type: message.type, + }, + }; + + return [...nodes.slice(0, index), node, ...nodes.slice(index + 1)]; +} From af77116f1e0f7e6a843f05517bccf742e4f635de Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 12:36:28 -0400 Subject: [PATCH 04/10] Convert Job.jsx to functional component in preparation for socket hook usage --- awx/ui_next/src/screens/Job/Job.jsx | 239 ++++++++++++---------------- 1 file changed, 102 insertions(+), 137 deletions(-) diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index 9530c4eeb374..48e4f1d4fa1b 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -1,5 +1,13 @@ -import React, { Component } from 'react'; -import { Route, withRouter, Switch, Redirect, Link } from 'react-router-dom'; +import React, { useEffect, useCallback } from 'react'; +import { + Route, + withRouter, + Switch, + Redirect, + Link, + useParams, + useRouteMatch, +} from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { CaretLeftIcon } from '@patternfly/react-icons'; @@ -7,157 +15,114 @@ import { Card, PageSection } from '@patternfly/react-core'; import { JobsAPI } from '../../api'; import ContentError from '../../components/ContentError'; import RoutedTabs from '../../components/RoutedTabs'; +import useRequest from '../../util/useRequest'; import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; +import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; -class Job extends Component { - constructor(props) { - super(props); +function Job({ i18n, lookup, setBreadcrumb }) { + const { id, type } = useParams(); + const match = useRouteMatch(); - this.state = { - job: null, - contentError: null, - hasContentLoading: true, - isInitialized: false, - }; - - this.loadJob = this.loadJob.bind(this); - } - - async componentDidMount() { - await this.loadJob(); - this.setState({ isInitialized: true }); - } - - async componentDidUpdate(prevProps) { - const { location } = this.props; - if (location !== prevProps.location) { - await this.loadJob(); - } - } - - async loadJob() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); - - this.setState({ contentError: null, hasContentLoading: true }); - try { - const { data } = await JobsAPI.readDetail(id, match.params.type); + const { isLoading, error, request: fetchJob, result: job } = useRequest( + useCallback(async () => { + const { data } = await JobsAPI.readDetail(id, type); setBreadcrumb(data); - this.setState({ job: data }); - } catch (err) { - this.setState({ contentError: err }); - } finally { - this.setState({ hasContentLoading: false }); - } + return data; + }, [id, type, setBreadcrumb]), + null + ); + + useEffect(() => { + fetchJob(); + }, [fetchJob]); + + let jobType; + if (job) { + jobType = JOB_TYPE_URL_SEGMENTS[job.type]; } - render() { - const { match, i18n, lookup } = this.props; - - const { job, contentError, hasContentLoading, isInitialized } = this.state; - let jobType; - if (job) { - jobType = JOB_TYPE_URL_SEGMENTS[job.type]; - } - - const tabsArray = [ - { - name: ( - <> - - {i18n._(t`Back to Jobs`)} - - ), - link: `/jobs`, - id: 99, - }, - { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, - { name: i18n._(t`Output`), link: `${match.url}/output`, id: 1 }, - ]; - - let showCardHeader = true; - - if (!isInitialized) { - showCardHeader = false; - } - - if (!hasContentLoading && contentError) { - return ( - - - - {contentError.response.status === 404 && ( - - {i18n._(t`The page you requested could not be found.`)}{' '} - {i18n._(t`View all Jobs.`)} - - )} - - - - ); - } - - if (lookup && job) { - return ( - - - - - - ); - } - +const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Jobs`)} + + ), + link: `/jobs`, + id: 99, + }, + { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, + { name: i18n._(t`Output`), link: `${match.url}/output`, id: 1 }, + ]; + + if (!isLoading && error) { return ( - {showCardHeader && } - - - {job && - job.type === 'workflow_job' && [ - - - , - - - , - ]} - {job && - job.type !== 'workflow_job' && [ - - - , - - - , - - {!hasContentLoading && ( - - - {i18n._(t`View Job Details`)} - - - )} - , - ]} - + + {error.response.status === 404 && ( + + {i18n._(t`The page you requested could not be found.`)}{' '} + {i18n._(t`View all Jobs.`)} + + )} + ); } + + if (lookup && job) { + return ( + + + + + + ); + } + + return ( + + + {!isLoading && } + + + {job && + job.type === 'workflow_job' && [ + + + , + + + , + ]} + {job && + job.type !== 'workflow_job' && [ + + + , + + + , + + {!isLoading && ( + + + {i18n._(t`View Job Details`)} + + + )} + , + ]} + + + + ); } export default withI18n()(withRouter(Job)); From c209c98e3f29aec7e91533b4a66fd901349c4f7c Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 14:15:49 -0400 Subject: [PATCH 05/10] Update job for job detail/workflow details based on websockets. Re-fetch job after job finishes running to display all available info. --- awx/ui_next/src/screens/Job/Job.jsx | 5 ++- awx/ui_next/src/screens/Job/useWsJob.js | 53 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 awx/ui_next/src/screens/Job/useWsJob.js diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index 48e4f1d4fa1b..b62e110494b1 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -20,13 +20,14 @@ import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; +import useWsJob from './useWsJob'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; function Job({ i18n, lookup, setBreadcrumb }) { const { id, type } = useParams(); const match = useRouteMatch(); - const { isLoading, error, request: fetchJob, result: job } = useRequest( + const { isLoading, error, request: fetchJob, result } = useRequest( useCallback(async () => { const { data } = await JobsAPI.readDetail(id, type); setBreadcrumb(data); @@ -39,6 +40,8 @@ function Job({ i18n, lookup, setBreadcrumb }) { fetchJob(); }, [fetchJob]); + const job = useWsJob(result); + let jobType; if (job) { jobType = JOB_TYPE_URL_SEGMENTS[job.type]; diff --git a/awx/ui_next/src/screens/Job/useWsJob.js b/awx/ui_next/src/screens/Job/useWsJob.js new file mode 100644 index 000000000000..c473039d2f98 --- /dev/null +++ b/awx/ui_next/src/screens/Job/useWsJob.js @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import useWebsocket from '../../util/useWebsocket'; +import { JobsAPI } from '../../api'; + +export default function useWsJob(initialJob) { + const { type } = useParams(); + const [job, setJob] = useState(initialJob); + const lastMessage = useWebsocket({ + jobs: ['status_changed'], + control: ['limit_reached_1'], + }); + + useEffect(() => { + setJob(initialJob); + }, [initialJob]); + + useEffect( + function parseWsMessage() { + async function fetchJob() { + const { data } = await JobsAPI.readDetail(job.id, type); + setJob(data); + } + + if (!job || lastMessage?.unified_job_id !== job.id) { + return; + } + + if ( + ['successful', 'failed', 'error', 'cancelled'].includes( + lastMessage.status + ) + ) { + fetchJob(); + } else { + setJob(updateJob(job, lastMessage)); + } + }, + [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps + ); + + return job; +} + +function updateJob(job, message) { + const updatedJob = { + ...job, + finished: message.finished, + status: message.status, + }; + + return updatedJob; +} From e28c9bb3c46389a2c6ef3df389c29d823245d229 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 14:17:09 -0400 Subject: [PATCH 06/10] Remove unnecessary constant variable --- awx/ui_next/src/screens/Job/useWsJob.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/Job/useWsJob.js b/awx/ui_next/src/screens/Job/useWsJob.js index c473039d2f98..ace2cf2ce622 100644 --- a/awx/ui_next/src/screens/Job/useWsJob.js +++ b/awx/ui_next/src/screens/Job/useWsJob.js @@ -43,11 +43,9 @@ export default function useWsJob(initialJob) { } function updateJob(job, message) { - const updatedJob = { + return { ...job, finished: message.finished, status: message.status, }; - - return updatedJob; } From c318a1759087d2a9616f7b976085c9c354ac7ea7 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 18 Aug 2020 15:38:35 -0400 Subject: [PATCH 07/10] Refresh nodes after workflow has finished running so that we can display all job info for relevant nodes. --- .../components/Workflow/WorkflowNodeHelp.jsx | 42 ++++++------ .../components/Workflow/workflowReducer.js | 3 - .../Job/WorkflowOutput/WorkflowOutputNode.jsx | 29 ++++---- .../Job/WorkflowOutput/useWsWorkflowOutput.js | 66 ++++++++++++++++--- 4 files changed, 92 insertions(+), 48 deletions(-) diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx index 510190b780f0..1abf65c3a7e4 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 eddd7dce2aa4..36171811e48b 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 24f60de2a65b..b875e9c768ea 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 76bd3c1e9c16..254026d75782 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() { + 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); + } + if ( - !nodes || - nodes.length === 0 || - lastMessage?.workflow_job_id !== workflowJobId + lastMessage?.unified_job_id === workflowJobId && + ['successful', 'failed', 'error', 'cancelled'].includes( + lastMessage.status + ) ) { - return; - } + refreshNodeObjects(); + } else { + if ( + !nodes || + nodes.length === 0 || + lastMessage?.workflow_job_id !== workflowJobId + ) { + return; + } - const index = nodes.findIndex( - node => node?.originalNodeObject?.id === lastMessage.workflow_node_id - ); + const index = nodes.findIndex( + node => node?.originalNodeObject?.id === lastMessage.workflow_node_id + ); - if (index > -1) { - setNodes(updateNode(nodes, index, lastMessage)); + if (index > -1) { + setNodes(updateNode(nodes, index, lastMessage)); + } } }, [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps From 5b56bda0bbb2075ecf25009ca7427d5c7c762c4c Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Aug 2020 12:53:41 -0400 Subject: [PATCH 08/10] Fix tests after changing node data structure --- .../Workflow/WorkflowNodeHelp.test.jsx | 14 ++++--- .../WorkflowOutputGraph.test.jsx | 14 ++++--- .../WorkflowOutputNode.test.jsx | 42 +++++++++++-------- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.test.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.test.jsx index 183119f5e3c1..9ac9afbacd6b 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.test.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.test.jsx @@ -9,11 +9,15 @@ describe('WorkflowNodeHelp', () => { }); test('renders the expected content for a completed job template job', () => { const node = { - job: { - name: 'Foo Job Template', - elapsed: 9000, - status: 'successful', - type: 'job', + originalNodeObject: { + summary_fields: { + job: { + name: 'Foo Job Template', + elapsed: 9000, + status: 'successful', + type: 'job', + }, + }, }, unifiedJobTemplate: { name: 'Foo Job Template', diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.test.jsx index 39de8a7c0871..7a70117cef49 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.test.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.test.jsx @@ -64,11 +64,15 @@ const workflowContext = { }, { id: 2, - job: { - name: 'Foo JT', - type: 'job', - status: 'successful', - elapsed: 60, + originalNodeObject: { + summary_fields: { + job: { + name: 'Foo JT', + type: 'job', + status: 'successful', + elapsed: 60, + }, + }, }, }, { diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx index feb9de5b3893..a8709ccf4e51 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx @@ -5,28 +5,36 @@ import WorkflowOutputNode from './WorkflowOutputNode'; const nodeWithJT = { id: 2, - job: { - elapsed: 7, - id: 9000, - name: 'Automation JT', - status: 'successful', - type: 'job', - }, - unifiedJobTemplate: { - id: 77, - name: 'Automation JT', - unified_job_type: 'job', + originalNodeObject: { + summary_fields: { + job: { + elapsed: 7, + id: 9000, + name: 'Automation JT', + status: 'successful', + type: 'job', + }, + }, + unifiedJobTemplate: { + id: 77, + name: 'Automation JT', + unified_job_type: 'job', + }, }, }; const nodeWithoutJT = { id: 2, - job: { - elapsed: 7, - id: 9000, - name: 'Automation JT 2', - status: 'successful', - type: 'job', + originalNodeObject: { + summary_fields: { + job: { + elapsed: 7, + id: 9000, + name: 'Automation JT 2', + status: 'successful', + type: 'job', + }, + }, }, }; From 45ca9976f3e364638ad667ef3a48ef28234f9103 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 3 Sep 2020 15:34:07 -0400 Subject: [PATCH 09/10] Fix merge conflict error --- awx/ui_next/src/screens/Job/Job.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index b62e110494b1..e5692ad261b9 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -18,7 +18,6 @@ import RoutedTabs from '../../components/RoutedTabs'; import useRequest from '../../util/useRequest'; import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; -import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; import useWsJob from './useWsJob'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; @@ -47,7 +46,7 @@ function Job({ i18n, lookup, setBreadcrumb }) { jobType = JOB_TYPE_URL_SEGMENTS[job.type]; } -const tabsArray = [ + const tabsArray = [ { name: ( <> @@ -98,7 +97,7 @@ const tabsArray = [ {job && job.type === 'workflow_job' && [ - + , From 8fab4559b9b463480eb7f24e583306f44080f23d Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 11 Sep 2020 09:27:44 -0400 Subject: [PATCH 10/10] Add data-job-status attr to all StatusIcons so that automated tests can determine whether or not a status has been updated via websockets. --- awx/ui_next/src/components/StatusIcon/StatusIcon.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/components/StatusIcon/StatusIcon.jsx b/awx/ui_next/src/components/StatusIcon/StatusIcon.jsx index c468d2f2ddf8..c386a270e9af 100644 --- a/awx/ui_next/src/components/StatusIcon/StatusIcon.jsx +++ b/awx/ui_next/src/components/StatusIcon/StatusIcon.jsx @@ -93,7 +93,7 @@ SkippedBottom.displayName = 'SkippedBottom'; const StatusIcon = ({ status, ...props }) => { return ( -
+
{status === 'running' && } {(status === 'new' || status === 'pending' ||