diff --git a/awx/ui_next/src/components/Workflow/WorkflowLegend.jsx b/awx/ui_next/src/components/Workflow/WorkflowLegend.jsx index 870d35c067..79951d4bf7 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowLegend.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowLegend.jsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; -import { func } from 'prop-types'; import { ExclamationTriangleIcon, PauseIcon, @@ -77,12 +77,14 @@ const Close = styled(TimesIcon)` top: 15px; `; -function WorkflowLegend({ i18n, onClose }) { +function WorkflowLegend({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + return (
{i18n._(t`Legend`)} - + dispatch({ type: 'TOGGLE_LEGEND' })} />
  • @@ -128,8 +130,4 @@ function WorkflowLegend({ i18n, onClose }) { ); } -WorkflowLegend.propTypes = { - onClose: func.isRequired, -}; - export default withI18n()(WorkflowLegend); diff --git a/awx/ui_next/src/components/Workflow/WorkflowStartNode.jsx b/awx/ui_next/src/components/Workflow/WorkflowStartNode.jsx index 3b3f89e982..a13e628518 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowStartNode.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowStartNode.jsx @@ -1,8 +1,12 @@ -import React, { useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import styled from 'styled-components'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { bool, func, shape } from 'prop-types'; +import { bool, func } from 'prop-types'; import { PlusIcon } from '@patternfly/react-icons'; import { constants as wfConstants } from '@components/Workflow/WorkflowUtils'; import { @@ -14,16 +18,11 @@ const StartG = styled.g` pointer-events: ${props => (props.ignorePointerEvents ? 'none' : 'auto')}; `; -function WorkflowStartNode({ - addingLink, - i18n, - nodePositions, - onAddNodeClick, - onUpdateHelpText, - showActionTooltip, -}) { +function WorkflowStartNode({ i18n, onUpdateHelpText, showActionTooltip }) { const ref = useRef(null); const [hovering, setHovering] = useState(false); + const dispatch = useContext(WorkflowDispatchContext); + const { addingLink, nodePositions } = useContext(WorkflowStateContext); const handleNodeMouseEnter = () => { ref.current.parentNode.appendChild(ref.current); @@ -62,7 +61,7 @@ function WorkflowStartNode({ onClick={() => { onUpdateHelpText(null); setHovering(false); - onAddNodeClick(1); + dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 }); }} > @@ -77,16 +76,11 @@ function WorkflowStartNode({ } WorkflowStartNode.propTypes = { - addingLink: bool, - nodePositions: shape().isRequired, - onAddNodeClick: func, showActionTooltip: bool.isRequired, onUpdateHelpText: func, }; WorkflowStartNode.defaultProps = { - addingLink: false, - onAddNodeClick: () => {}, onUpdateHelpText: () => {}, }; diff --git a/awx/ui_next/src/components/Workflow/WorkflowStartNode.test.jsx b/awx/ui_next/src/components/Workflow/WorkflowStartNode.test.jsx index 8bc7d733b3..1079694012 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowStartNode.test.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowStartNode.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import { WorkflowStateContext } from '@contexts/Workflow'; import WorkflowStartNode from './WorkflowStartNode'; const nodePositions = { @@ -13,10 +14,12 @@ describe('WorkflowStartNode', () => { test('mounts successfully', () => { const wrapper = mount( - + + + ); expect(wrapper).toHaveLength(1); @@ -24,7 +27,9 @@ describe('WorkflowStartNode', () => { test('tooltip shown on hover', () => { const wrapper = mount( - + + + ); expect(wrapper.find('WorkflowActionTooltip')).toHaveLength(0); diff --git a/awx/ui_next/src/components/Workflow/WorkflowTools.jsx b/awx/ui_next/src/components/Workflow/WorkflowTools.jsx index 6e1876e1c8..1ff435f824 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowTools.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowTools.jsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; @@ -53,13 +54,13 @@ const Close = styled(TimesIcon)` function WorkflowTools({ i18n, - onClose, onFitGraph, onPan, onPanToMiddle, onZoomChange, zoomPercentage, }) { + const dispatch = useContext(WorkflowDispatchContext); const zoomIn = () => { const newScale = Math.ceil((zoomPercentage + 10) / 10) * 10 < 200 @@ -80,7 +81,7 @@ function WorkflowTools({
    {i18n._(t`Tools`)} - + dispatch({ type: 'TOGGLE_TOOLS' })} />
    { + node.isInvalidLinkTarget = false; + }); + + newLinks.push({ + source: { id: addLinkSourceNode.id }, + target: { id: addLinkTargetNode.id }, + linkType, + type: 'link', + }); + + newLinks.forEach((link, index) => { + if (link.source.id === 1 && link.target.id === addLinkTargetNode.id) { + newLinks.splice(index, 1); + } + }); + + return { + ...state, + addLinkSourceNode: null, + addLinkTargetNode: null, + addingLink: false, + linkToEdit: null, + links: newLinks, + nodes: newNodes, + unsavedChanges: true, + }; +} + +function createNode(state, node) { + const { addNodeSource, addNodeTarget, links, nodes, nextNodeId } = state; + const newNodes = [...nodes]; + const newLinks = [...links]; + + newNodes.push({ + id: nextNodeId, + type: 'node', + unifiedJobTemplate: node.nodeResource, + }); + + // Ensures that root nodes appear to always run + // after "START" + if (addNodeSource === 1) { + node.linkType = 'always'; + } + + newLinks.push({ + source: { id: addNodeSource }, + target: { id: nextNodeId }, + linkType: node.linkType, + type: 'link', + }); + + if (addNodeTarget) { + newLinks.forEach(linkToCompare => { + if ( + linkToCompare.source.id === addNodeSource && + linkToCompare.target.id === addNodeTarget + ) { + linkToCompare.source = { id: nextNodeId }; + } + }); + } + + return { + ...state, + addNodeSource: null, + addNodeTarget: null, + links: newLinks, + nextNodeId: nextNodeId + 1, + nodes: newNodes, + unsavedChanges: true, + }; +} + +function cancelLink(state) { + const { nodes } = state; + const newNodes = [...nodes]; + + newNodes.forEach(node => { + node.isInvalidLinkTarget = false; + }); + + return { + ...state, + addLinkSourceNode: null, + addLinkTargetNode: null, + addingLink: false, + nodes: newNodes, + }; +} + +function cancelLinkModal(state) { + const { nodes } = state; + const newNodes = [...nodes]; + + newNodes.forEach(node => { + node.isInvalidLinkTarget = false; + }); + + return { + ...state, + addLinkSourceNode: null, + addLinkTargetNode: null, + addingLink: false, + linkToEdit: null, + nodes: newNodes, + }; +} + +function deleteAllNodes(state) { + const { nodes } = state; + return { + ...state, + addLinkSourceNode: null, + addLinkTargetNode: null, + addingLink: false, + links: [], + nodes: nodes.map(node => { + if (node.id !== 1) { + node.isDeleted = true; + } + + return node; + }), + showDeleteAllNodesModal: false, + }; +} + +function deleteLink(state) { + const { links, linkToDelete } = state; + const newLinks = [...links]; + + for (let i = newLinks.length; i--; ) { + const link = newLinks[i]; + + if ( + link.source.id === linkToDelete.source.id && + link.target.id === linkToDelete.target.id + ) { + newLinks.splice(i, 1); + } + } + + if (!linkToDelete.isConvergenceLink) { + // Add a new link from the start node to the orphaned node + newLinks.push({ + source: { + id: 1, + }, + target: { + id: linkToDelete.target.id, + }, + linkType: 'always', + type: 'link', + }); + } + + return { + ...state, + links: newLinks, + linkToDelete: null, + unsavedChanges: true, + }; +} + +function addLinksFromParentsToChildren( + parents, + children, + newLinks, + linkParentMapping +) { + parents.forEach(parentId => { + children.forEach(child => { + if (parentId === 1) { + // We only want to create a link from the start node to this node if it + // doesn't have any other parents + if (linkParentMapping[child.id].length === 1) { + newLinks.push({ + source: { id: parentId }, + target: { id: child.id }, + linkType: 'always', + type: 'link', + }); + } + } else if (!linkParentMapping[child.id].includes(parentId)) { + newLinks.push({ + source: { id: parentId }, + target: { id: child.id }, + linkType: child.linkType, + type: 'link', + }); + } + }); + }); +} + +function removeLinksFromDeletedNode( + nodeId, + newLinks, + linkParentMapping, + children, + parents +) { + for (let i = newLinks.length; i--; ) { + const link = newLinks[i]; + + if (!linkParentMapping[link.target.id]) { + linkParentMapping[link.target.id] = []; + } + + linkParentMapping[link.target.id].push(link.source.id); + + if (link.source.id === nodeId || link.target.id === nodeId) { + if (link.source.id === nodeId) { + children.push({ id: link.target.id, linkType: link.linkType }); + } else if (link.target.id === nodeId) { + parents.push(link.source.id); + } + newLinks.splice(i, 1); + } + } +} + +function deleteNode(state) { + const { links, nodes, nodeToDelete } = state; + + const nodeId = nodeToDelete.id; + const newNodes = [...nodes]; + const newLinks = [...links]; + + newNodes.find(node => node.id === nodeToDelete.id).isDeleted = true; + + // Update the links + const parents = []; + const children = []; + const linkParentMapping = {}; + + removeLinksFromDeletedNode( + nodeId, + newLinks, + linkParentMapping, + children, + parents + ); + + addLinksFromParentsToChildren(parents, children, newLinks, linkParentMapping); + + return { + ...state, + links: newLinks, + nodeToDelete: null, + nodes: newNodes, + unsavedChanges: true, + }; +} + +function generateNodes(workflowNodes, i18n) { + const allNodeIds = []; + const chartNodeIdToIndexMapping = {}; + const nodeIdToChartNodeIdMapping = {}; + let nodeIdCounter = 2; + const arrayOfNodesForChart = [ + { + id: 1, + unifiedJobTemplate: { + name: i18n._(t`START`), + }, + type: 'node', + }, + ]; + workflowNodes.forEach(node => { + node.workflowMakerNodeId = nodeIdCounter; + + const nodeObj = { + id: nodeIdCounter, + type: 'node', + 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; + } + + arrayOfNodesForChart.push(nodeObj); + allNodeIds.push(node.id); + nodeIdToChartNodeIdMapping[node.id] = node.workflowMakerNodeId; + chartNodeIdToIndexMapping[nodeIdCounter] = nodeIdCounter - 1; + nodeIdCounter++; + }); + + return [ + arrayOfNodesForChart, + allNodeIds, + nodeIdToChartNodeIdMapping, + chartNodeIdToIndexMapping, + nodeIdCounter, + ]; +} + +function generateLinks( + workflowNodes, + chartNodeIdToIndexMapping, + nodeIdToChartNodeIdMapping, + arrayOfNodesForChart +) { + const arrayOfLinksForChart = []; + const nonRootNodeIds = []; + workflowNodes.forEach(node => { + const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId]; + node.success_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + linkType: 'success', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + node.failure_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + linkType: 'failure', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + node.always_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + linkType: 'always', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + }); + + return [arrayOfLinksForChart, nonRootNodeIds]; +} + +// TODO: check to make sure passing i18n into this reducer +// actually works the way we want it to. If not we may +// have to explore other options +function generateNodesAndLinks(state, workflowNodes, i18n) { + const [ + arrayOfNodesForChart, + allNodeIds, + nodeIdToChartNodeIdMapping, + chartNodeIdToIndexMapping, + nodeIdCounter, + ] = generateNodes(workflowNodes, i18n); + const [arrayOfLinksForChart, nonRootNodeIds] = generateLinks( + workflowNodes, + chartNodeIdToIndexMapping, + nodeIdToChartNodeIdMapping, + arrayOfNodesForChart + ); + + const uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds)); + + const rootNodes = allNodeIds.filter( + nodeId => !uniqueNonRootNodeIds.includes(nodeId) + ); + + rootNodes.forEach(rootNodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[rootNodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[0], + target: arrayOfNodesForChart[targetIndex], + linkType: 'always', + type: 'link', + }); + }); + + return { + ...state, + links: arrayOfLinksForChart, + nodes: arrayOfNodesForChart, + nextNodeId: nodeIdCounter, + }; +} + +function selectSourceForLinking(state, sourceNode) { + const { links, nodes } = state; + const newNodes = [...nodes]; + const parentMap = {}; + const invalidLinkTargetIds = []; + // Find and mark any ancestors as disabled to prevent cycles + links.forEach(link => { + // id=1 is our artificial root node so we don't care about that + if (link.source.id === 1) { + return; + } + if (link.source.id === sourceNode.id) { + // Disables direct children from the add link process + invalidLinkTargetIds.push(link.target.id); + } + if (!parentMap[link.target.id]) { + parentMap[link.target.id] = []; + } + parentMap[link.target.id].push(link.source.id); + }); + + const getAncestors = id => { + if (parentMap[id]) { + parentMap[id].forEach(parentId => { + invalidLinkTargetIds.push(parentId); + getAncestors(parentId); + }); + } + }; + + getAncestors(sourceNode.id); + + // Filter out the duplicates + invalidLinkTargetIds + .filter((element, index, array) => index === array.indexOf(element)) + .forEach(ancestorId => { + newNodes.forEach(node => { + if (node.id === ancestorId) { + node.isInvalidLinkTarget = true; + } + }); + }); + + return { + ...state, + addLinkSourceNode: sourceNode, + addingLink: true, + nodes: newNodes, + }; +} + +function startDeleteLink(state, link) { + const { links } = state; + const parentMap = {}; + links.forEach(existingLink => { + if (!parentMap[existingLink.target.id]) { + parentMap[existingLink.target.id] = []; + } + parentMap[existingLink.target.id].push(existingLink.source.id); + }); + + link.isConvergenceLink = parentMap[link.target.id].length > 1; + + return { + ...state, + linkToDelete: link, + }; +} + +function toggleDeleteAllNodesModal(state) { + const { showDeleteAllNodesModal } = state; + return { + ...state, + showDeleteAllNodesModal: !showDeleteAllNodesModal, + }; +} + +function toggleLegend(state) { + const { showLegend } = state; + return { + ...state, + showLegend: !showLegend, + }; +} + +function toggleTools(state) { + const { showTools } = state; + return { + ...state, + showTools: !showTools, + }; +} + +function toggleUnsavedChangesModal(state) { + const { showUnsavedChangesModal } = state; + return { + ...state, + showUnsavedChangesModal: !showUnsavedChangesModal, + }; +} + +function updateLink(state, linkType) { + const { linkToEdit, links } = state; + const newLinks = [...links]; + + newLinks.forEach(link => { + if ( + link.source.id === linkToEdit.source.id && + link.target.id === linkToEdit.target.id + ) { + link.linkType = linkType; + } + }); + + return { + ...state, + linkToEdit: null, + links: newLinks, + unsavedChanges: true, + }; +} + +function updateNode(state, editedNode) { + const { nodeToEdit, nodes } = state; + const newNodes = [...nodes]; + + const matchingNode = newNodes.find(node => node.id === nodeToEdit.id); + matchingNode.unifiedJobTemplate = editedNode.nodeResource; + matchingNode.isEdited = true; + + return { + ...state, + nodeToEdit: null, + nodes: newNodes, + unsavedChanges: true, + }; +} diff --git a/awx/ui_next/src/contexts/Workflow.jsx b/awx/ui_next/src/contexts/Workflow.jsx new file mode 100644 index 0000000000..d79fd40082 --- /dev/null +++ b/awx/ui_next/src/contexts/Workflow.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export const WorkflowDispatchContext = React.createContext(null); +export const WorkflowStateContext = React.createContext(null); diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx index 0e41845ac7..d0f282724f 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx @@ -1,12 +1,16 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useReducer } from 'react'; import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; import styled from 'styled-components'; import { shape } from 'prop-types'; import { CardBody as PFCardBody } from '@patternfly/react-core'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import { layoutGraph } from '@components/Workflow/WorkflowUtils'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; +import workflowReducer from '@components/Workflow/workflowReducer'; import { WorkflowJobsAPI } from '@api'; import WorkflowOutputGraph from './WorkflowOutputGraph'; import WorkflowOutputToolbar from './WorkflowOutputToolbar'; @@ -36,148 +40,50 @@ const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => { }; function WorkflowOutput({ job, i18n }) { - const [contentError, setContentError] = useState(null); - const [graphLinks, setGraphLinks] = useState([]); - const [graphNodes, setGraphNodes] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [nodePositions, setNodePositions] = useState(null); - const [showLegend, setShowLegend] = useState(false); - const [showTools, setShowTools] = useState(false); + const [state, dispatch] = useReducer(workflowReducer, { + contentError: null, + isLoading: true, + links: [], + nextNodeId: 0, + nodePositions: null, + nodes: [], + showLegend: false, + showTools: false, + }); + + const { contentError, isLoading, links, nodePositions, nodes } = state; useEffect(() => { - const buildGraphArrays = nodes => { - const allNodeIds = []; - const arrayOfLinksForChart = []; - const chartNodeIdToIndexMapping = {}; - const nodeIdToChartNodeIdMapping = {}; - const nodeRef = {}; - const nonRootNodeIds = []; - let nodeIdCounter = 1; - const arrayOfNodesForChart = [ - { - id: nodeIdCounter, - unifiedJobTemplate: { - name: i18n._(t`START`), - }, - type: 'node', - }, - ]; - nodeIdCounter++; - // Assign each node an ID - 0 is reserved for the start node. We need to - // make sure that we have an ID on every node including new nodes so the - // ID returned by the api won't do - nodes.forEach(node => { - node.workflowMakerNodeId = nodeIdCounter; - nodeRef[nodeIdCounter] = { - originalNodeObject: node, - }; - - const nodeObj = { - index: nodeIdCounter - 1, - id: nodeIdCounter, - type: 'node', - }; - - if (node.summary_fields.job) { - nodeObj.job = node.summary_fields.job; - } - if (node.summary_fields.unified_job_template) { - nodeRef[nodeIdCounter].unifiedJobTemplate = - node.summary_fields.unified_job_template; - nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template; - } - - arrayOfNodesForChart.push(nodeObj); - allNodeIds.push(node.id); - nodeIdToChartNodeIdMapping[node.id] = node.workflowMakerNodeId; - chartNodeIdToIndexMapping[nodeIdCounter] = nodeIdCounter - 1; - nodeIdCounter++; - }); - - nodes.forEach(node => { - const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId]; - node.success_nodes.forEach(nodeId => { - const targetIndex = - chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; - arrayOfLinksForChart.push({ - source: arrayOfNodesForChart[sourceIndex], - target: arrayOfNodesForChart[targetIndex], - linkType: 'success', - type: 'link', - }); - nonRootNodeIds.push(nodeId); - }); - node.failure_nodes.forEach(nodeId => { - const targetIndex = - chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; - arrayOfLinksForChart.push({ - source: arrayOfNodesForChart[sourceIndex], - target: arrayOfNodesForChart[targetIndex], - linkType: 'failure', - type: 'link', - }); - nonRootNodeIds.push(nodeId); - }); - node.always_nodes.forEach(nodeId => { - const targetIndex = - chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; - arrayOfLinksForChart.push({ - source: arrayOfNodesForChart[sourceIndex], - target: arrayOfNodesForChart[targetIndex], - linkType: 'always', - type: 'link', - }); - nonRootNodeIds.push(nodeId); - }); - }); - - const uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds)); - - const rootNodes = allNodeIds.filter( - nodeId => !uniqueNonRootNodeIds.includes(nodeId) - ); - - rootNodes.forEach(rootNodeId => { - const targetIndex = - chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[rootNodeId]]; - arrayOfLinksForChart.push({ - source: arrayOfNodesForChart[0], - target: arrayOfNodesForChart[targetIndex], - linkType: 'always', - type: 'link', - }); - }); - - setGraphNodes(arrayOfNodesForChart); - setGraphLinks(arrayOfLinksForChart); - }; - async function fetchData() { try { - const nodes = await fetchWorkflowNodes(job.id); - buildGraphArrays(nodes); + const workflowNodes = await fetchWorkflowNodes(job.id); + dispatch({ + type: 'GENERATE_NODES_AND_LINKS', + nodes: workflowNodes, + i18n, + }); } catch (error) { - setContentError(error); + dispatch({ type: 'SET_CONTENT_ERROR', value: error }); } finally { - setIsLoading(false); + dispatch({ type: 'SET_IS_LOADING', value: false }); } } fetchData(); - }, [job.id, job.unified_job_template, i18n]); + }, [job.id, i18n]); // Update positions of nodes/links useEffect(() => { - if (graphNodes) { + if (nodes) { const newNodePositions = {}; - const g = layoutGraph(graphNodes, graphLinks); + const g = layoutGraph(nodes, links); g.nodes().forEach(node => { newNodePositions[node] = g.node(node); }); - setNodePositions(newNodePositions); + dispatch({ type: 'SET_NODE_POSITIONS', value: newNodePositions }); } - }, [graphLinks, graphNodes]); + }, [links, nodes]); if (isLoading) { return ( @@ -196,29 +102,16 @@ function WorkflowOutput({ job, i18n }) { } return ( - - - setShowLegend(!showLegend)} - onToolsToggle={() => setShowTools(!showTools)} - toolsShown={showTools} - /> - {nodePositions && ( - - )} - - + + + + + + {nodePositions && } + + + + ); } diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx index 2e7a3cf341..b3295916e3 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx @@ -1,6 +1,6 @@ -import React, { Fragment, useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { WorkflowStateContext } from '@contexts/Workflow'; import * as d3 from 'd3'; -import { arrayOf, bool, shape, func } from 'prop-types'; import { getScaleAndOffsetToFit, getTranslatePointsForZoom, @@ -18,21 +18,17 @@ import { WorkflowTools, } from '@components/Workflow'; -function WorkflowOutputGraph({ - links, - nodePositions, - nodes, - onUpdateShowLegend, - onUpdateShowTools, - showLegend, - showTools, -}) { +function WorkflowOutputGraph() { const [linkHelp, setLinkHelp] = useState(); const [nodeHelp, setNodeHelp] = useState(); const [zoomPercentage, setZoomPercentage] = useState(100); const svgRef = useRef(null); const gRef = useRef(null); + const { links, nodePositions, nodes, showLegend, showTools } = useContext( + WorkflowStateContext + ); + // This is the zoom function called by using the mousewheel/click and drag const zoom = () => { const translation = [d3.event.transform.x, d3.event.transform.y]; @@ -158,7 +154,7 @@ function WorkflowOutputGraph({ }, []); return ( - + <> {(nodeHelp || linkHelp) && ( {nodeHelp && } @@ -172,16 +168,11 @@ function WorkflowOutputGraph({ > {nodePositions && [ - , + , links.map(link => ( )), @@ -193,7 +184,6 @@ function WorkflowOutputGraph({ mouseEnter={() => setNodeHelp(node)} mouseLeave={() => setNodeHelp(null)} node={node} - nodePositions={nodePositions} /> ); } @@ -205,7 +195,6 @@ function WorkflowOutputGraph({
    {showTools && ( onUpdateShowTools(false)} onFitGraph={handleFitGraph} onPan={handlePan} onPanToMiddle={handlePanToMiddle} @@ -213,22 +202,10 @@ function WorkflowOutputGraph({ zoomPercentage={zoomPercentage} /> )} - {showLegend && ( - onUpdateShowLegend(false)} /> - )} + {showLegend && }
    -
    + ); } -WorkflowOutputGraph.propTypes = { - links: arrayOf(shape()).isRequired, - nodePositions: shape().isRequired, - nodes: arrayOf(shape()).isRequired, - onUpdateShowLegend: func.isRequired, - onUpdateShowTools: func.isRequired, - showLegend: bool.isRequired, - showTools: bool.isRequired, -}; - export default WorkflowOutputGraph; diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx index d33c381943..022cad9de7 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx @@ -1,4 +1,5 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { WorkflowStateContext } from '@contexts/Workflow'; import { shape } from 'prop-types'; import { generateLine, @@ -6,11 +7,12 @@ import { getLinkOverlayPoints, } from '@components/Workflow/WorkflowUtils'; -function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) { +function WorkflowOutputLink({ link, onUpdateLinkHelp }) { const ref = useRef(null); const [hovering, setHovering] = useState(false); const [pathD, setPathD] = useState(); const [pathStroke, setPathStroke] = useState('#CCCCCC'); + const { nodePositions } = useContext(WorkflowStateContext); const handleLinkMouseEnter = () => { ref.current.parentNode.appendChild(ref.current); @@ -65,7 +67,6 @@ function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) { WorkflowOutputLink.propTypes = { link: shape().isRequired, - nodePositions: shape().isRequired, }; export default WorkflowOutputLink; diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx index 651efc1060..09830aab45 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; -import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { mount } from 'enzyme'; +import { WorkflowStateContext } from '@contexts/Workflow'; import WorkflowOutputLink from './WorkflowOutputLink'; const link = { @@ -28,13 +29,15 @@ const nodePositions = { describe('WorkflowOutputLink', () => { test('mounts successfully', () => { - const wrapper = mountWithContexts( + const wrapper = mount( - {}} - /> + + {}} + /> + ); expect(wrapper).toHaveLength(1); diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx index a54a5f6c7e..e8fcf8f68c 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx @@ -1,4 +1,5 @@ -import React, { Fragment } from 'react'; +import React, { useContext } from 'react'; +import { WorkflowStateContext } from '@contexts/Workflow'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -55,14 +56,8 @@ const NodeDefaultLabel = styled.p` white-space: nowrap; `; -function WorkflowOutputNode({ - history, - i18n, - mouseEnter, - mouseLeave, - node, - nodePositions, -}) { +function WorkflowOutputNode({ history, i18n, mouseEnter, mouseLeave, node }) { + const { nodePositions } = useContext(WorkflowStateContext); let borderColor = '#93969A'; if (node.job) { @@ -105,7 +100,7 @@ function WorkflowOutputNode({ /> {node.job ? ( - + <>

    @@ -115,7 +110,7 @@ function WorkflowOutputNode({

    {secondsToHHMMSS(node.job.elapsed)} -
    + ) : ( {node.unifiedJobTemplate @@ -134,7 +129,6 @@ WorkflowOutputNode.propTypes = { mouseEnter: func.isRequired, mouseLeave: func.isRequired, node: shape().isRequired, - nodePositions: shape().isRequired, }; export default withI18n()(withRouter(WorkflowOutputNode)); 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 046ee99c73..e819d079f3 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { WorkflowStateContext } from '@contexts/Workflow'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import WorkflowOutputNode from './WorkflowOutputNode'; @@ -48,12 +49,13 @@ describe('WorkflowOutputNode', () => { test('mounts successfully', () => { const wrapper = mountWithContexts( - {}} - mouseLeave={() => {}} - node={nodeWithJT} - nodePositions={nodePositions} - /> + + {}} + mouseLeave={() => {}} + node={nodeWithJT} + /> + ); expect(wrapper).toHaveLength(1); @@ -61,12 +63,13 @@ describe('WorkflowOutputNode', () => { test('node contents displayed correctly when Job and Job Template exist', () => { const wrapper = mountWithContexts( - {}} - mouseLeave={() => {}} - node={nodeWithJT} - nodePositions={nodePositions} - /> + + {}} + mouseLeave={() => {}} + node={nodeWithJT} + /> + ); expect(wrapper.contains(

    Automation JT

    )).toEqual(true); @@ -75,12 +78,13 @@ describe('WorkflowOutputNode', () => { test('node contents displayed correctly when Job Template deleted', () => { const wrapper = mountWithContexts( - {}} - mouseLeave={() => {}} - node={nodeWithoutJT} - nodePositions={nodePositions} - /> + + {}} + mouseLeave={() => {}} + node={nodeWithoutJT} + /> + ); expect(wrapper.contains(

    DELETED

    )).toEqual(true); @@ -89,12 +93,13 @@ describe('WorkflowOutputNode', () => { test('node contents displayed correctly when Job deleted', () => { const wrapper = mountWithContexts( - {}} - mouseLeave={() => {}} - node={{ id: 2 }} - nodePositions={nodePositions} - /> + + {}} + mouseLeave={() => {}} + node={{ id: 2 }} + /> + ); expect(wrapper.text()).toBe('DELETED'); diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx index 975467adde..27c5bb594a 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx @@ -1,7 +1,11 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { arrayOf, bool, func, shape } from 'prop-types'; +import { shape } from 'prop-types'; import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core'; import { CompassIcon, WrenchIcon } from '@patternfly/react-icons'; import { StatusIcon } from '@components/Sparkline'; @@ -53,15 +57,11 @@ const StatusIconWithMargin = styled(StatusIcon)` margin-right: 20px; `; -function WorkflowOutputToolbar({ - i18n, - job, - legendShown, - nodes, - onLegendToggle, - onToolsToggle, - toolsShown, -}) { +function WorkflowOutputToolbar({ i18n, job }) { + const dispatch = useContext(WorkflowDispatchContext); + + const { nodes, showLegend, showTools } = useContext(WorkflowStateContext); + const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1; return ( @@ -76,8 +76,8 @@ function WorkflowOutputToolbar({ dispatch({ type: 'TOGGLE_LEGEND' })} variant="plain" > @@ -85,8 +85,8 @@ function WorkflowOutputToolbar({ dispatch({ type: 'TOGGLE_TOOLS' })} variant="plain" > @@ -99,15 +99,6 @@ function WorkflowOutputToolbar({ WorkflowOutputToolbar.propTypes = { job: shape().isRequired, - legendShown: bool.isRequired, - nodes: arrayOf(shape()), - onLegendToggle: func.isRequired, - onToolsToggle: func.isRequired, - toolsShown: bool.isRequired, -}; - -WorkflowOutputToolbar.defaultProps = { - nodes: [], }; export default withI18n()(WorkflowOutputToolbar); diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx index 980f47ca23..4afe13e93f 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { WorkflowStateContext } from '@contexts/Workflow'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import WorkflowOutputToolbar from './WorkflowOutputToolbar'; @@ -7,17 +8,18 @@ const job = { status: 'successful', }; +const workflowContext = { + nodes: [], + showLegend: false, + showTools: false, +}; + describe('WorkflowOutputToolbar', () => { test('mounts successfully', () => { const wrapper = mountWithContexts( - {}} - onToolsToggle={() => {}} - toolsShown={false} - /> + + + ); expect(wrapper).toHaveLength(1); }); @@ -36,14 +38,9 @@ describe('WorkflowOutputToolbar', () => { }, ]; const wrapper = mountWithContexts( - {}} - onToolsToggle={() => {}} - toolsShown={false} - /> + + + ); // The start node (id=1) and deleted nodes (isDeleted=true) should be ignored expect(wrapper.find('Badge').text()).toBe('1'); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx index 727f41f9e0..324bb98b87 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx @@ -1,11 +1,12 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; import { Button } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { func } from 'prop-types'; import AlertModal from '@components/AlertModal'; -function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) { +function DeleteAllNodesModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); return ( onConfirm()} + onClick={() => dispatch({ type: 'DELETE_ALL_NODES' })} > {i18n._(t`Remove`)} , @@ -21,13 +22,13 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) { key="cancel" variant="secondary" aria-label={i18n._(t`Cancel node removal`)} - onClick={onCancel} + onClick={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })} > {i18n._(t`Cancel`)} , ]} isOpen - onClose={onCancel} + onClose={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })} title={i18n._(t`Remove All Nodes`)} variant="danger" > @@ -40,9 +41,4 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) { ); } -DeleteAllNodesModal.propTypes = { - onCancel: func.isRequired, - onConfirm: func.isRequired, -}; - export default withI18n()(DeleteAllNodesModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkAddModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkAddModal.jsx new file mode 100644 index 0000000000..c3b707fe5e --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkAddModal.jsx @@ -0,0 +1,22 @@ +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; +import { BaseSizes, Title, TitleLevel } from '@patternfly/react-core'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import LinkModal from './LinkModal'; + +function LinkAddModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + return ( + + {i18n._(t`Add Link`)} + + } + onConfirm={linkType => dispatch({ type: 'CREATE_LINK', linkType })} + /> + ); +} + +export default withI18n()(LinkAddModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkDeleteModal.jsx similarity index 68% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkDeleteModal.jsx index 390940937d..395ac49617 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/LinkDeleteModal.jsx @@ -1,22 +1,27 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useContext } from 'react'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import { Button } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { func, shape } from 'prop-types'; import AlertModal from '@components/AlertModal'; -function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) { +function LinkDeleteModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + const { linkToDelete } = useContext(WorkflowStateContext); return ( dispatch({ type: 'SET_LINK_TO_DELETE', value: null })} actions={[ , @@ -27,7 +35,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) { key="cancel" variant="secondary" aria-label={i18n._(t`Cancel link changes`)} - onClick={onCancel} + onClick={() => dispatch({ type: 'CANCEL_LINK_MODAL' })} > {i18n._(t`Cancel`)} , @@ -36,7 +44,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) { { - setNewLinkType(value); + setLinkType(value); }} /> @@ -64,14 +72,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) { } LinkModal.propTypes = { - linkType: string, - header: node.isRequired, - onCancel: func.isRequired, onConfirm: func.isRequired, }; -LinkModal.defaultProps = { - linkType: 'success', -}; - export default withI18n()(LinkModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/index.js new file mode 100644 index 0000000000..2ec7da9d96 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModals/index.js @@ -0,0 +1,4 @@ +export { default as LinkDeleteModal } from './LinkDeleteModal'; +export { default as LinkAddModal } from './LinkAddModal'; +export { default as LinkEditModal } from './LinkEditModal'; +export { default as LinkModal } from './LinkModal'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/index.js deleted file mode 100644 index c289e043df..0000000000 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as NodeModal } from './NodeModal'; -export { default as NodeNextButton } from './NodeNextButton'; -export { default as RunStep } from './RunStep'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.jsx new file mode 100644 index 0000000000..a0898e3ed7 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeAddModal.jsx @@ -0,0 +1,34 @@ +import React, { useContext } from 'react'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import NodeModal from './NodeModal'; + +function NodeAddModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + const { addNodeSource } = useContext(WorkflowStateContext); + + const addNode = (linkType, resource, nodeType) => { + dispatch({ + type: 'CREATE_NODE', + node: { + linkType, + nodeResource: resource, + nodeType, + }, + }); + }; + + return ( + + ); +} + +export default withI18n()(NodeAddModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeDeleteModal.jsx similarity index 69% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeDeleteModal.jsx index 31f20fcd76..4d245c2955 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeDeleteModal.jsx @@ -1,23 +1,28 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useContext } from 'react'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import { Button } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { func, shape } from 'prop-types'; import AlertModal from '@components/AlertModal'; -function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) { +function NodeDeleteModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + const { nodeToDelete } = useContext(WorkflowStateContext); return ( dispatch({ type: 'SET_NODE_TO_DELETE', value: null })} actions={[ , @@ -25,7 +30,7 @@ function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) { key="cancel" variant="secondary" aria-label={i18n._(t`Cancel node removal`)} - onClick={onCancel} + onClick={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })} > {i18n._(t`Cancel`)} , @@ -46,10 +51,4 @@ function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) { ); } -NodeDeleteModal.propTypes = { - nodeToDelete: shape().isRequired, - onCancel: func.isRequired, - onConfirm: func.isRequired, -}; - export default withI18n()(NodeDeleteModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx new file mode 100644 index 0000000000..485add87d3 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import NodeModal from './NodeModal'; + +function NodeEditModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + + const updateNode = (linkType, resource, nodeType) => { + dispatch({ + type: 'UPDATE_NODE', + node: { + linkType, + nodeResource: resource, + nodeType, + }, + }); + }; + + return ( + + ); +} + +export default withI18n()(NodeEditModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx similarity index 92% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx index 71f0942e52..27e720aba8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx @@ -1,8 +1,12 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { withRouter } from 'react-router-dom'; +import { + WorkflowDispatchContext, + WorkflowStateContext, +} from '@contexts/Workflow'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { bool, func, node, shape } from 'prop-types'; +import { bool, node, func } from 'prop-types'; import { Button, WizardContextConsumer, @@ -12,15 +16,10 @@ import Wizard from '@components/Wizard'; import { NodeTypeStep } from './NodeTypeStep'; import { RunStep, NodeNextButton } from '.'; -function NodeModal({ - askLinkType, - history, - i18n, - nodeToEdit, - onClose, - onSave, - title, -}) { +function NodeModal({ askLinkType, history, i18n, onSave, title }) { + const dispatch = useContext(WorkflowDispatchContext); + const { nodeToEdit } = useContext(WorkflowStateContext); + let defaultApprovalDescription = ''; let defaultApprovalName = ''; let defaultApprovalTimeout = 0; @@ -104,16 +103,12 @@ function NodeModal({ } : nodeResource; - onSave({ - linkType, - nodeResource: resource, - nodeType, - }); + onSave(linkType, resource, nodeType); }; const handleCancel = () => { clearQueryParams(); - onClose(); + dispatch({ type: 'CANCEL_NODE_MODAL' }); }; const handleNodeTypeChange = newNodeType => { @@ -211,14 +206,8 @@ function NodeModal({ NodeModal.propTypes = { askLinkType: bool.isRequired, - nodeToEdit: shape(), - onClose: func.isRequired, onSave: func.isRequired, title: node.isRequired, }; -NodeModal.defaultProps = { - nodeToEdit: null, -}; - export default withI18n()(withRouter(NodeModal)); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeNextButton.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeNextButton.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeNextButton.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeNextButton.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/InventorySourcesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/InventorySourcesList.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/JobTemplatesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/JobTemplatesList.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/NodeTypeStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/NodeTypeStep.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/ProjectsList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/ProjectsList.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/WorkflowJobTemplatesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/WorkflowJobTemplatesList.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/index.js similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeTypeStep/index.js rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/index.js diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx new file mode 100644 index 0000000000..27000d1185 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; +import { Modal } from '@patternfly/react-core'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; + +function NodeViewModal({ i18n }) { + const dispatch = useContext(WorkflowDispatchContext); + return ( + dispatch({ type: 'SET_NODE_TO_VIEW', value: null })} + > + Coming soon :) + + ); +} + +export default withI18n()(NodeViewModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/RunStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/RunStep.jsx similarity index 100% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/RunStep.jsx rename to awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/RunStep.jsx diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/index.js new file mode 100644 index 0000000000..6dc89d09e5 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/index.js @@ -0,0 +1,7 @@ +export { default as NodeAddModal } from './NodeAddModal'; +export { default as NodeDeleteModal } from './NodeDeleteModal'; +export { default as NodeEditModal } from './NodeEditModal'; +export { default as NodeModal } from './NodeModal'; +export { default as NodeNextButton } from './NodeNextButton'; +export { default as NodeViewModal } from './NodeViewModal'; +export { default as RunStep } from './RunStep'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeViewModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeViewModal.jsx deleted file mode 100644 index 3ef2f07d3c..0000000000 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeViewModal.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { Modal } from '@patternfly/react-core'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { func } from 'prop-types'; - -function NodeViewModal({ i18n, onClose }) { - return ( - - Coming soon :) - - ); -} - -NodeViewModal.propTypes = { - onClose: func.isRequired, -}; - -export default withI18n()(NodeViewModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/UnsavedChangesModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/UnsavedChangesModal.jsx index d16b73d52d..7a72289302 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/UnsavedChangesModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/UnsavedChangesModal.jsx @@ -1,16 +1,18 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { WorkflowDispatchContext } from '@contexts/Workflow'; import { Button, Modal } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t, Trans } from '@lingui/macro'; import { func } from 'prop-types'; -function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) { +function UnsavedChangesModal({ i18n, onSaveAndExit, onExit }) { + const dispatch = useContext(WorkflowDispatchContext); return ( dispatch({ type: 'TOGGLE_UNSAVED_CHANGES_MODAL' })} actions={[