diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 2f6bfc227e..edd33d77c0 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -2,10 +2,10 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import SelectableCard from '@components/SelectableCard'; +import Wizard from '@components/Wizard'; import SelectResourceStep from './SelectResourceStep'; import SelectRoleStep from './SelectRoleStep'; -import { SelectableCard } from '@components/SelectableCard'; -import { Wizard } from '@components/Wizard'; import { TeamsAPI, UsersAPI } from '../../api'; const readUsers = async queryParams => diff --git a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap index dbebf79172..42bcf01688 100644 --- a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap +++ b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap @@ -72,6 +72,7 @@ exports[` initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` stacked={true} > initially renders succesfully 1`] = ` data-pf-content={true} > initially renders succesfully 1`] = ` data-pf-content={true} > { test('renders the expected content', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); + const wrapper = mount( + Step 1

}]} + /> + ); + expect(wrapper).toHaveLength(1); }); }); diff --git a/awx/ui_next/src/components/Wizard/index.js b/awx/ui_next/src/components/Wizard/index.js index f07d6622b0..40da120187 100644 --- a/awx/ui_next/src/components/Wizard/index.js +++ b/awx/ui_next/src/components/Wizard/index.js @@ -1 +1 @@ -export { default as Wizard } from './Wizard'; +export { default } from './Wizard'; diff --git a/awx/ui_next/src/components/Workflow/WorkflowActionTooltip.jsx b/awx/ui_next/src/components/Workflow/WorkflowActionTooltip.jsx index aa3a626578..8946461d63 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowActionTooltip.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowActionTooltip.jsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components'; +import { node, number } from 'prop-types'; const TooltipContents = styled.div` display: flex; @@ -10,32 +11,32 @@ const TooltipArrows = styled.div` `; const TooltipArrowOuter = styled.div` + border-bottom: 10px solid transparent; + border-right: 10px solid #c4c4c4; + border-top: 10px solid transparent; + height: 0; + margin: auto; position: absolute; top: calc(50% - 10px); width: 0; - height: 0; - border-right: 10px solid #c4c4c4; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - margin: auto; `; const TooltipArrowInner = styled.div` - position: absolute; - top: calc(50% - 10px); - left: 2px; - width: 0; - height: 0; + border-bottom: 10px solid transparent; border-right: 10px solid white; border-top: 10px solid transparent; - border-bottom: 10px solid transparent; + height: 0; + left: 2px; margin: auto; + position: absolute; + top: calc(50% - 10px); + width: 0; `; const TooltipActions = styled.div` background-color: white; - border: 1px solid #c4c4c4; border-radius: 2px; + border: 1px solid #c4c4c4; padding: 5px; `; @@ -59,4 +60,10 @@ function WorkflowActionTooltip({ actions, pointX, pointY }) { ); } +WorkflowActionTooltip.propTypes = { + actions: node.isRequired, + pointX: number.isRequired, + pointY: number.isRequired, +}; + export default WorkflowActionTooltip; diff --git a/awx/ui_next/src/components/Workflow/WorkflowActionTooltipItem.jsx b/awx/ui_next/src/components/Workflow/WorkflowActionTooltipItem.jsx index 571b749ae7..dcb2f4f098 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowActionTooltipItem.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowActionTooltipItem.jsx @@ -1,15 +1,16 @@ import React from 'react'; import styled from 'styled-components'; +import { func } from 'prop-types'; const TooltipItem = styled.div` - height: 25px; - width: 25px; - font-size: 12px; - display: flex; align-items: center; - justify-content: center; - cursor: pointer; border-radius: 2px; + cursor: pointer; + display: flex; + font-size: 12px; + height: 25px; + justify-content: center; + width: 25px; &:hover { color: white; @@ -21,21 +22,33 @@ const TooltipItem = styled.div` } `; -function WorkflowActionTooltip({ +function WorkflowActionTooltipItem({ children, + onClick, onMouseEnter, onMouseLeave, - onClick, }) { return ( {children} ); } -export default WorkflowActionTooltip; +WorkflowActionTooltipItem.propTypes = { + onClick: func, + onMouseEnter: func, + onMouseLeave: func, +}; + +WorkflowActionTooltipItem.defaultProps = { + onClick: () => {}, + onMouseEnter: () => {}, + onMouseLeave: () => {}, +}; + +export default WorkflowActionTooltipItem; diff --git a/awx/ui_next/src/components/Workflow/WorkflowHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowHelp.jsx index a69ed75844..4b2251a7ce 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowHelp.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowHelp.jsx @@ -2,20 +2,20 @@ import React from 'react'; import styled from 'styled-components'; const Outer = styled.div` - position: relative; height: 0; pointer-events: none; + position: relative; `; const Inner = styled.div` - position: absolute; - left: 10px; - top: 10px; background-color: #383f44; - color: white; - padding: 5px 10px; border-radius: 2px; + color: white; + left: 10px; max-width: 300px; + padding: 5px 10px; + position: absolute; + top: 10px; `; function WorkflowHelp({ children }) { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerKey.jsx b/awx/ui_next/src/components/Workflow/WorkflowKey.jsx similarity index 96% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerKey.jsx rename to awx/ui_next/src/components/Workflow/WorkflowKey.jsx index 74bed8a577..e0e75dd995 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerKey.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowKey.jsx @@ -5,50 +5,50 @@ import styled from 'styled-components'; import { ExclamationTriangleIcon, PauseIcon } from '@patternfly/react-icons'; const Wrapper = styled.div` - border: 1px solid #c7c7c7; background-color: white; - min-width: 100px; + border: 1px solid #c7c7c7; margin-left: 20px; + min-width: 100px; `; const Header = styled.div` - padding: 10px; border-bottom: 1px solid #c7c7c7; + padding: 10px; `; const Key = styled.ul` padding: 5px 10px; li { - padding: 5px 0px; - display: flex; align-items: center; + display: flex; + padding: 5px 0px; } `; const NodeTypeLetter = styled.div` - font-size: 10px; - color: white; - text-align: center; - line-height: 20px; background-color: #393f43; border-radius: 50%; + color: white; + font-size: 10px; height: 20px; - width: 20px; + line-height: 20px; margin-right: 10px; + text-align: center; + width: 20px; `; const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)` color: #f0ad4d; - margin-right: 10px; height: 20px; + margin-right: 10px; width: 20px; `; const Link = styled.div` height: 5px; - width: 20px; margin-right: 10px; + width: 20px; `; const SuccessLink = styled(Link)` @@ -63,7 +63,7 @@ const AlwaysLink = styled(Link)` background-color: #337ab7; `; -function VisualizerKey({ i18n }) { +function WorkflowKey({ i18n }) { return (
@@ -113,4 +113,4 @@ function VisualizerKey({ i18n }) { ); } -export default withI18n()(VisualizerKey); +export default withI18n()(WorkflowKey); diff --git a/awx/ui_next/src/components/Workflow/WorkflowLinkHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowLinkHelp.jsx index 3cd00b7aae..4252351798 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowLinkHelp.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowLinkHelp.jsx @@ -2,11 +2,12 @@ import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; +import { shape } from 'prop-types'; const GridDL = styled.dl` + column-gap: 15px; display: grid; grid-template-columns: max-content; - column-gap: 15px; row-gap: 0px; dt { grid-column-start: 1; @@ -18,7 +19,7 @@ const GridDL = styled.dl` function WorkflowLinkHelp({ link, i18n }) { let linkType; - switch (link.edgeType) { + switch (link.linkType) { case 'always': linkType = i18n._(t`Always`); break; @@ -42,4 +43,8 @@ function WorkflowLinkHelp({ link, i18n }) { ); } +WorkflowLinkHelp.propTypes = { + link: shape().isRequired, +}; + export default withI18n()(WorkflowLinkHelp); diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx index 17524b720e..07d56c623b 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx @@ -2,12 +2,13 @@ import React, { Fragment } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; +import { shape } from 'prop-types'; import { secondsToHHMMSS } from '@util/dates'; const GridDL = styled.dl` + column-gap: 15px; display: grid; grid-template-columns: max-content; - column-gap: 15px; row-gap: 0px; dt { grid-column-start: 1; @@ -134,4 +135,8 @@ function WorkflowNodeHelp({ node, i18n }) { ); } +WorkflowNodeHelp.propTypes = { + node: shape().isRequired, +}; + export default withI18n()(WorkflowNodeHelp); diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx index 013c1ed88d..eb2364503d 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx @@ -1,14 +1,15 @@ import React from 'react'; import styled from 'styled-components'; +import { shape } from 'prop-types'; import { PauseIcon } from '@patternfly/react-icons'; const NodeTypeLetter = styled.foreignObject` - font-size: 10px; - color: white; - text-align: center; - line-height: 20px; background-color: #393f43; border-radius: 50%; + color: white; + font-size: 10px; + line-height: 20px; + text-align: center; `; function WorkflowNodeTypeLetter({ node }) { @@ -52,4 +53,8 @@ function WorkflowNodeTypeLetter({ node }) { ); } +WorkflowNodeTypeLetter.propTypes = { + node: shape().isRequired, +}; + export default WorkflowNodeTypeLetter; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerTools.jsx b/awx/ui_next/src/components/Workflow/WorkflowTools.jsx similarity index 87% rename from awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerTools.jsx rename to awx/ui_next/src/components/Workflow/WorkflowTools.jsx index 1be36d1851..a1aa6f4c08 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerTools.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowTools.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; +import { func, number } from 'prop-types'; import { Tooltip } from '@patternfly/react-core'; import { CaretDownIcon, @@ -15,19 +16,19 @@ import { } from '@patternfly/react-icons'; const Wrapper = styled.div` - border: 1px solid #c7c7c7; background-color: white; + border: 1px solid #c7c7c7; height: 135px; `; const Header = styled.div` - padding: 10px; border-bottom: 1px solid #c7c7c7; + padding: 10px; `; const Pan = styled.div` - display: flex; align-items: center; + display: flex; `; const PanCenter = styled.div` @@ -36,18 +37,18 @@ const PanCenter = styled.div` `; const Tools = styled.div` - display: flex; align-items: center; + display: flex; padding: 20px; `; -function VisualizerTools({ +function WorkflowTools({ i18n, - zoomPercentage, - onZoomChange, onFitGraph, onPan, onPanToMiddle, + onZoomChange, + zoomPercentage, }) { const zoomIn = () => { const newScale = @@ -81,14 +82,16 @@ function VisualizerTools({ zoomOut()} css="margin-right: 10px;" /> + onZoomChange(parseInt(event.target.value, 10) / 100) + } step="10" - onChange={event => onZoomChange(parseInt(event.target.value) / 100)} - > + type="range" + value={zoomPercentage} + /> zoomIn()} css="margin: 0px 25px 0px 10px;" /> @@ -119,4 +122,12 @@ function VisualizerTools({ ); } -export default withI18n()(VisualizerTools); +WorkflowTools.propTypes = { + onFitGraph: func.isRequired, + onPan: func.isRequired, + onPanToMiddle: func.isRequired, + onZoomChange: func.isRequired, + zoomPercentage: number.isRequired, +}; + +export default withI18n()(WorkflowTools); diff --git a/awx/ui_next/src/components/Workflow/index.js b/awx/ui_next/src/components/Workflow/index.js index 66cc6ef332..c0adfbe7c1 100644 --- a/awx/ui_next/src/components/Workflow/index.js +++ b/awx/ui_next/src/components/Workflow/index.js @@ -1,8 +1,10 @@ -export { default as WorkflowHelp } from './WorkflowHelp'; -export { default as WorkflowLinkHelp } from './WorkflowLinkHelp'; -export { default as WorkflowNodeHelp } from './WorkflowNodeHelp'; -export { default as WorkflowNodeTypeLetter } from './WorkflowNodeTypeLetter'; export { default as WorkflowActionTooltip } from './WorkflowActionTooltip'; export { default as WorkflowActionTooltipItem, } from './WorkflowActionTooltipItem'; +export { default as WorkflowHelp } from './WorkflowHelp'; +export { default as WorkflowKey } from './WorkflowKey'; +export { default as WorkflowLinkHelp } from './WorkflowLinkHelp'; +export { default as WorkflowNodeHelp } from './WorkflowNodeHelp'; +export { default as WorkflowNodeTypeLetter } from './WorkflowNodeTypeLetter'; +export { default as WorkflowTools } from './WorkflowTools'; diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 2db9a79ee6..d93ee7d457 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -25,16 +25,6 @@ import { InventoriesAPI, AdHocCommandsAPI, } from '@api'; -import { JOB_TYPE_URL_SEGMENTS } from '@constants'; - -const ActionButtonWrapper = styled.div` - display: flex; - justify-content: flex-end; - margin-top: 20px; - & > :not(:first-child) { - margin-left: 20px; - } -`; const VariablesInput = styled(_VariablesInput)` .pf-c-form__label { diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx index cee04e61cf..9783d9ea7b 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutput.jsx @@ -2,64 +2,60 @@ import React, { useState, useEffect } 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 { layoutGraph } from '@util/workflow'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import { WorkflowJobsAPI } from '@api'; import WorkflowOutputGraph from './WorkflowOutputGraph'; +import WorkflowOutputToolbar from './WorkflowOutputToolbar'; const CardBody = styled(PFCardBody)` - height: calc(100vh - 240px); display: flex; flex-direction: column; -`; - -const Toolbar = styled.div` - height: 50px; - background-color: grey; + height: calc(100vh - 240px); `; const Wrapper = styled.div` display: flex; flex-flow: column; height: 100%; + position: relative; `; const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => { - try { - const { data } = await WorkflowJobsAPI.readNodes(jobId, { - page_size: 200, - page: pageNo, - }); - if (data.next) { - return await fetchWorkflowNodes( - jobId, - pageNo + 1, - nodes.concat(data.results) - ); - } - return nodes.concat(data.results); - } catch (error) { - throw error; + 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); }; function WorkflowOutput({ job, i18n }) { const [contentError, setContentError] = useState(null); - const [isLoading, setIsLoading] = useState(true); const [graphLinks, setGraphLinks] = useState([]); const [graphNodes, setGraphNodes] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [nodePositions, setNodePositions] = useState(null); + const [showKey, setShowKey] = useState(false); + const [showTools, setShowTools] = useState(false); useEffect(() => { const buildGraphArrays = nodes => { - const nonRootNodeIds = []; const allNodeIds = []; const arrayOfLinksForChart = []; - const nodeIdToChartNodeIdMapping = {}; const chartNodeIdToIndexMapping = {}; + const nodeIdToChartNodeIdMapping = {}; const nodeRef = {}; + const nonRootNodeIds = []; let nodeIdCounter = 1; const arrayOfNodesForChart = [ { @@ -110,7 +106,7 @@ function WorkflowOutput({ job, i18n }) { arrayOfLinksForChart.push({ source: arrayOfNodesForChart[sourceIndex], target: arrayOfNodesForChart[targetIndex], - edgeType: 'success', + linkType: 'success', type: 'link', }); nonRootNodeIds.push(nodeId); @@ -121,7 +117,7 @@ function WorkflowOutput({ job, i18n }) { arrayOfLinksForChart.push({ source: arrayOfNodesForChart[sourceIndex], target: arrayOfNodesForChart[targetIndex], - edgeType: 'failure', + linkType: 'failure', type: 'link', }); nonRootNodeIds.push(nodeId); @@ -132,7 +128,7 @@ function WorkflowOutput({ job, i18n }) { arrayOfLinksForChart.push({ source: arrayOfNodesForChart[sourceIndex], target: arrayOfNodesForChart[targetIndex], - edgeType: 'always', + linkType: 'always', type: 'link', }); nonRootNodeIds.push(nodeId); @@ -151,7 +147,7 @@ function WorkflowOutput({ job, i18n }) { arrayOfLinksForChart.push({ source: arrayOfNodesForChart[0], target: arrayOfNodesForChart[targetIndex], - edgeType: 'always', + linkType: 'always', type: 'link', }); }); @@ -206,12 +202,21 @@ function WorkflowOutput({ job, i18n }) { return ( - Toolbar + setShowKey(!showKey)} + onToolsToggle={() => setShowTools(!showTools)} + toolsShown={showTools} + /> {nodePositions && ( )} @@ -219,4 +224,8 @@ function WorkflowOutput({ job, i18n }) { ); } +WorkflowOutput.propTypes = { + job: shape().isRequired, +}; + export default withI18n()(WorkflowOutput); diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx index 04112d149d..89aff5467d 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx @@ -1,15 +1,28 @@ import React, { Fragment, useEffect, useRef, useState } from 'react'; import * as d3 from 'd3'; -import { WorkflowHelp, WorkflowNodeHelp } from '@components/Workflow'; -import { calcZoomAndFit } from '@util/workflow'; +import { arrayOf, bool, shape } from 'prop-types'; +import { calcZoomAndFit, getZoomTranslate } from '@util/workflow'; import { WorkflowOutputLink, WorkflowOutputNode, WorkflowOutputStartNode, } from '@screens/Job/WorkflowOutput'; +import { + WorkflowHelp, + WorkflowKey, + WorkflowNodeHelp, + WorkflowTools, +} from '@components/Workflow'; -function WorkflowOutputGraph({ links, nodes, nodePositions }) { +function WorkflowOutputGraph({ + links, + nodePositions, + nodes, + showKey, + showTools, +}) { const [nodeHelp, setNodeHelp] = useState(); + const [zoomPercentage, setZoomPercentage] = useState(100); const svgRef = useRef(null); const gRef = useRef(null); @@ -20,6 +33,75 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) { 'transform', `translate(${translation}) scale(${d3.event.transform.k})` ); + + setZoomPercentage(d3.event.transform.k * 100); + }; + + const handlePan = direction => { + const transform = d3.zoomTransform(d3.select(svgRef.current).node()); + + let { x: xPos, y: yPos } = transform; + const { k: currentScale } = transform; + + switch (direction) { + case 'up': + yPos -= 50; + break; + case 'down': + yPos += 50; + break; + case 'left': + xPos -= 50; + break; + case 'right': + xPos += 50; + break; + default: + // Throw an error? + break; + } + + d3.select(svgRef.current).call( + zoomRef.transform, + d3.zoomIdentity.translate(xPos, yPos).scale(currentScale) + ); + }; + + const handlePanToMiddle = () => { + const svgElement = document.getElementById('workflow-svg'); + const svgBoundingClientRect = svgElement.getBoundingClientRect(); + d3.select(svgRef.current).call( + zoomRef.transform, + d3.zoomIdentity + .translate(0, svgBoundingClientRect.height / 2 - 30) + .scale(1) + ); + + setZoomPercentage(100); + }; + + const handleZoomChange = newScale => { + const [translateX, translateY] = getZoomTranslate(svgRef.current, newScale); + + d3.select(svgRef.current).call( + zoomRef.transform, + d3.zoomIdentity.translate(translateX, translateY).scale(newScale) + ); + setZoomPercentage(newScale * 100); + }; + + const handleFitGraph = () => { + const [scaleToFit, yTranslate] = calcZoomAndFit( + gRef.current, + svgRef.current + ); + + d3.select(svgRef.current).call( + zoomRef.transform, + d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit) + ); + + setZoomPercentage(scaleToFit * 100); }; const zoomRef = d3 @@ -34,12 +116,17 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) { // Attempt to zoom the graph to fit the available screen space useEffect(() => { - const [scaleToFit, yTranslate] = calcZoomAndFit(gRef.current); + const [scaleToFit, yTranslate] = calcZoomAndFit( + gRef.current, + svgRef.current + ); d3.select(svgRef.current).call( zoomRef.transform, d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit) ); + + setZoomPercentage(scaleToFit * 100); // We only want this to run once (when the component mounts) // Including zoomRef.transform in the deps array will cause this to // run very frequently. @@ -78,10 +165,10 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) { return ( setNodeHelp(node)} mouseLeave={() => setNodeHelp(null)} + node={node} + nodePositions={nodePositions} /> ); } @@ -90,8 +177,28 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) { ]} +
+ {showTools && ( + + )} + {showKey && } +
); } +WorkflowOutputGraph.propTypes = { + links: arrayOf(shape()).isRequired, + nodePositions: shape().isRequired, + nodes: arrayOf(shape()).isRequired, + showKey: 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 bc9dde7874..fdfc91d6cd 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, useState } from 'react'; +import { shape } from 'prop-types'; import { generateLine, getLinePoints } from '@util/workflow'; function WorkflowOutputLink({ link, nodePositions }) { @@ -6,16 +7,16 @@ function WorkflowOutputLink({ link, nodePositions }) { const [pathStroke, setPathStroke] = useState('#CCCCCC'); useEffect(() => { - if (link.edgeType === 'failure') { + if (link.linkType === 'failure') { setPathStroke('#d9534f'); } - if (link.edgeType === 'success') { + if (link.linkType === 'success') { setPathStroke('#5cb85c'); } - if (link.edgeType === 'always') { + if (link.linkType === 'always') { setPathStroke('#337ab7'); } - }, [link.edgeType]); + }, [link.linkType]); useEffect(() => { const linePoints = getLinePoints(link, nodePositions); @@ -37,4 +38,9 @@ function WorkflowOutputLink({ link, nodePositions }) { ); } +WorkflowOutputLink.propTypes = { + link: shape().isRequired, + nodePositions: shape().isRequired, +}; + export default WorkflowOutputLink; diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx index a8749e60e8..69ebd533aa 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx @@ -1,11 +1,12 @@ import React, { Fragment } from 'react'; +import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; +import { func, shape } from 'prop-types'; import { StatusIcon } from '@components/Sparkline'; import { WorkflowNodeTypeLetter } from '@components/Workflow'; import { secondsToHHMMSS } from '@util/dates'; -import { JOB_TYPE_URL_SEGMENTS } from '@constants'; import { constants as wfConstants } from '@util/workflow'; const NodeG = styled.g` @@ -13,12 +14,12 @@ const NodeG = styled.g` `; const JobTopLine = styled.div` - display: flex; align-items: center; + display: flex; margin-top: 5px; - white-space: nowrap; - text-overflow: ellipsis; overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; p { margin-left: 10px; @@ -29,8 +30,8 @@ const JobTopLine = styled.div` `; const Elapsed = styled.div` - text-align: center; margin-top: 5px; + text-align: center; span { font-size: 12px; @@ -48,18 +49,19 @@ const NodeContents = styled.foreignObject` const NodeDefaultLabel = styled.p` margin-top: 20px; - text-align: center; - white-space: nowrap; - text-overflow: ellipsis; overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; `; function WorkflowOutputNode({ - node, - nodePositions, + history, + i18n, mouseEnter, mouseLeave, - i18n, + node, + nodePositions, }) { let borderColor = '#93969A'; @@ -78,10 +80,7 @@ function WorkflowOutputNode({ const handleNodeClick = () => { if (node.job) { - window.open( - `/#/jobs/${JOB_TYPE_URL_SEGMENTS[node.job.type]}/${node.job.id}`, - '_blank' - ); + history.push(`/jobs/${node.job.id}/details`); } }; @@ -96,13 +95,13 @@ function WorkflowOutputNode({ onMouseLeave={mouseLeave} > {node.job ? ( @@ -133,4 +132,11 @@ function WorkflowOutputNode({ ); } -export default withI18n()(WorkflowOutputNode); +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/WorkflowOutputStartNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.jsx index 0101200732..5b6fd79ee8 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.jsx @@ -1,18 +1,19 @@ import React from 'react'; +import { shape } from 'prop-types'; import { constants as wfConstants } from '@util/workflow'; function WorkflowOutputStartNode({ nodePositions }) { return ( - {/* TODO: Translate this...? */} + {/* TODO: We need to be able to handle translated text here */} START @@ -20,4 +21,8 @@ function WorkflowOutputStartNode({ nodePositions }) { ); } +WorkflowOutputStartNode.propTypes = { + nodePositions: shape().isRequired, +}; + export default WorkflowOutputStartNode; diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx new file mode 100644 index 0000000000..c3f0350796 --- /dev/null +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { arrayOf, bool, func, 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'; +import VerticalSeparator from '@components/VerticalSeparator'; +import styled from 'styled-components'; + +const Badge = styled(PFBadge)` + align-items: center; + display: flex; + justify-content: center; + margin-left: 10px; +`; + +const ActionButton = styled(Button)` + border: none; + margin: 0px 6px; + padding: 6px 10px; + &:hover { + background-color: #0066cc; + color: white; + } + + &.pf-m-active { + background-color: #0066cc; + color: white; + } +`; + +function WorkflowOutputToolbar({ + i18n, + job, + keyShown, + nodes, + onKeyToggle, + onToolsToggle, + toolsShown, +}) { + const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1; + + return ( +
+
+ + {job.name} +
+
+
{i18n._(t`Total Nodes`)}
+ {totalNodes} + + + + + + + + + + + +
+
+ ); +} + +WorkflowOutputToolbar.propTypes = { + job: shape().isRequired, + keyShown: bool.isRequired, + nodes: arrayOf(shape()), + onKeyToggle: 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/index.js b/awx/ui_next/src/screens/Job/WorkflowOutput/index.js index 6580c4f7d3..f3020d0679 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/index.js +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/index.js @@ -3,3 +3,4 @@ export { default as WorkflowOutputGraph } from './WorkflowOutputGraph'; export { default as WorkflowOutputLink } from './WorkflowOutputLink'; export { default as WorkflowOutputNode } from './WorkflowOutputNode'; export { default as WorkflowOutputStartNode } from './WorkflowOutputStartNode'; +export { default as WorkflowOutputToolbar } from './WorkflowOutputToolbar'; diff --git a/awx/ui_next/src/screens/Template/Templates.test.jsx b/awx/ui_next/src/screens/Template/Templates.test.jsx index f5b2f4b300..ec7ef416e1 100644 --- a/awx/ui_next/src/screens/Template/Templates.test.jsx +++ b/awx/ui_next/src/screens/Template/Templates.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; - import { mountWithContexts } from '@testUtils/enzymeHelpers'; import Templates from './Templates'; 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 86a19f586f..727f41f9e0 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/DeleteAllNodesModal.jsx @@ -2,15 +2,12 @@ import React from 'react'; 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 }) { return ( , ]} + isOpen + onClose={onCancel} + title={i18n._(t`Remove All Nodes`)} + variant="danger" >

{i18n._( @@ -39,4 +40,9 @@ 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/LinkDeleteModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx index db8222b8b8..390940937d 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkDeleteModal.jsx @@ -2,6 +2,7 @@ import React, { Fragment } from 'react'; 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 }) { @@ -13,18 +14,18 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) { onClose={onCancel} actions={[ , , @@ -45,4 +46,10 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) { ); } +LinkDeleteModal.propTypes = { + linkToDelete: shape().isRequired, + onCancel: func.isRequired, + onConfirm: func.isRequired, +}; + export default withI18n()(LinkDeleteModal); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModal.jsx index 6c76c80e4f..1e9c486e44 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/LinkModal.jsx @@ -1,23 +1,17 @@ import React, { useState } from 'react'; -import { Button, Modal } from '@patternfly/react-core'; +import { Button, FormGroup, Modal } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { FormGroup } from '@patternfly/react-core'; +import { func, node, string } from 'prop-types'; import AnsibleSelect from '@components/AnsibleSelect'; -function LinkModal({ - i18n, - header, - onCancel, - onConfirm, - edgeType = 'success', -}) { - const [newEdgeType, setNewEdgeType] = useState(edgeType); +function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) { + const [newLinkType, setNewLinkType] = useState(linkType); return ( onConfirm(newEdgeType)} + onClick={() => onConfirm(newLinkType)} > {i18n._(t`Save`)} , @@ -42,7 +36,7 @@ function LinkModal({ { - setNewEdgeType(value); + setNewLinkType(value); }} /> @@ -69,4 +63,15 @@ function LinkModal({ ); } +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/NodeDeleteModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx index 5d98bc2550..31f20fcd76 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeDeleteModal.jsx @@ -2,6 +2,7 @@ import React, { Fragment } from 'react'; 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 }) { @@ -45,4 +46,10 @@ 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/NodeModal/NodeModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx index ed7bd6d546..6848fc1ec8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeModal.jsx @@ -2,82 +2,84 @@ import React, { useState } from 'react'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { bool, func, node, shape } from 'prop-types'; import { Button, WizardContextConsumer, WizardFooter, } from '@patternfly/react-core'; -import NodeTypeStep from './NodeTypeStep/NodeTypeStep'; -import RunStep from './RunStep'; -import NodeNextButton from './NodeNextButton'; -import { Wizard } from '@components/Wizard'; +import Wizard from '@components/Wizard'; +import { NodeTypeStep } from './NodeTypeStep'; +import { RunStep, NodeNextButton } from '.'; function NodeModal({ + askLinkType, history, i18n, - title, + nodeToEdit, onClose, onSave, - node, - askLinkType, + title, }) { - let defaultNodeType = 'job_template'; - let defaultNodeResource = null; - let defaultApprovalName = ''; let defaultApprovalDescription = ''; + let defaultApprovalName = ''; let defaultApprovalTimeout = 0; - if (node && node.unifiedJobTemplate) { + let defaultNodeResource = null; + let defaultNodeType = 'job_template'; + if (nodeToEdit && nodeToEdit.unifiedJobTemplate) { if ( - node && - node.unifiedJobTemplate && - (node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type) + nodeToEdit && + nodeToEdit.unifiedJobTemplate && + (nodeToEdit.unifiedJobTemplate.type || + nodeToEdit.unifiedJobTemplate.unified_job_type) ) { const ujtType = - node.unifiedJobTemplate.type || - node.unifiedJobTemplate.unified_job_type; + nodeToEdit.unifiedJobTemplate.type || + nodeToEdit.unifiedJobTemplate.unified_job_type; switch (ujtType) { case 'job_template': case 'job': defaultNodeType = 'job_template'; - defaultNodeResource = node.unifiedJobTemplate; + defaultNodeResource = nodeToEdit.unifiedJobTemplate; break; case 'project': case 'project_update': defaultNodeType = 'project_sync'; - defaultNodeResource = node.unifiedJobTemplate; + defaultNodeResource = nodeToEdit.unifiedJobTemplate; break; case 'inventory_source': case 'inventory_update': defaultNodeType = 'inventory_source_sync'; - defaultNodeResource = node.unifiedJobTemplate; + defaultNodeResource = nodeToEdit.unifiedJobTemplate; break; case 'workflow_job_template': case 'workflow_job': defaultNodeType = 'workflow_job_template'; - defaultNodeResource = node.unifiedJobTemplate; + defaultNodeResource = nodeToEdit.unifiedJobTemplate; break; case 'workflow_approval_template': case 'workflow_approval': defaultNodeType = 'approval'; - defaultApprovalName = node.unifiedJobTemplate.name; - defaultApprovalDescription = node.unifiedJobTemplate.description; - defaultApprovalTimeout = node.unifiedJobTemplate.timeout; + defaultApprovalName = nodeToEdit.unifiedJobTemplate.name; + defaultApprovalDescription = + nodeToEdit.unifiedJobTemplate.description; + defaultApprovalTimeout = nodeToEdit.unifiedJobTemplate.timeout; break; default: } } } - const [nodeType, setNodeType] = useState(defaultNodeType); - const [linkType, setLinkType] = useState('success'); - const [nodeResource, setNodeResource] = useState(defaultNodeResource); - const [triggerNext, setTriggerNext] = useState(0); - const [approvalName, setApprovalName] = useState(defaultApprovalName); const [approvalDescription, setApprovalDescription] = useState( defaultApprovalDescription ); + const [approvalName, setApprovalName] = useState(defaultApprovalName); const [approvalTimeout, setApprovalTimeout] = useState( defaultApprovalTimeout ); + const [linkType, setLinkType] = useState('success'); + const [nodeResource, setNodeResource] = useState(defaultNodeResource); + const [nodeType, setNodeType] = useState(defaultNodeType); + const [triggerNext, setTriggerNext] = useState(0); const clearQueryParams = () => { const parts = history.location.search.replace(/^\?/, '').split('&'); @@ -95,19 +97,17 @@ function NodeModal({ const resource = nodeType === 'approval' ? { - name: approvalName, description: approvalDescription, + name: approvalName, timeout: approvalTimeout, type: 'workflow_approval_template', } : nodeResource; - // TODO: pick edgeType or linkType and be consistent across all files. - onSave({ - nodeType, - edgeType: linkType, + linkType, nodeResource: resource, + nodeType, }); }; @@ -145,15 +145,15 @@ function NodeModal({ (nodeType === 'approval' && approvalName !== ''), component: ( ), @@ -198,15 +198,27 @@ function NodeModal({ return ( ); } +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/NodeModal/NodeNextButton.jsx index a941cb33da..046b2b4db8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeNextButton.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModal/NodeNextButton.jsx @@ -1,22 +1,20 @@ import React, { useEffect } from 'react'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; +import { func, number, shape, string } from 'prop-types'; import { Button } from '@patternfly/react-core'; function NodeNextButton({ - i18n, activeStep, + buttonText, + onClick, onNext, triggerNext, - onClick, - buttonText, }) { useEffect(() => { if (!triggerNext) { return; } onNext(); - }, [triggerNext]); + }, [onNext, triggerNext]); return ( @@ -47,4 +48,8 @@ function StartScreen({ i18n, onStartClick }) { ); } -export default withI18n()(StartScreen); +VisualizerStartScreen.propTypes = { + onStartClick: func.isRequired, +}; + +export default withI18n()(VisualizerStartScreen); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx index c7ad6e08b7..68d86664f8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx @@ -1,11 +1,11 @@ import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { arrayOf, bool, func, shape } from 'prop-types'; import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core'; import { BookIcon, CompassIcon, - DownloadIcon, RocketIcon, TimesIcon, TrashAltIcon, @@ -36,68 +36,50 @@ const ActionButton = styled(Button)` } `; -function Toolbar({ +function VisualizerToolbar({ i18n, - template, + keyShown, + nodes, onClose, - onSave, - nodes = [], onDeleteAllClick, onKeyToggle, - keyShown, + onSave, onToolsToggle, + template, toolsShown, }) { const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1; return (

-
-
+
+
{i18n._(t`Workflow Visualizer`)} {template.name}
-
+
{i18n._(t`Total Nodes`)}
{totalNodes} - - - @@ -106,10 +88,10 @@ function Toolbar({ @@ -120,9 +102,9 @@ function Toolbar({ @@ -132,4 +114,20 @@ function Toolbar({ ); } -export default withI18n()(Toolbar); +VisualizerToolbar.propTypes = { + keyShown: bool.isRequired, + nodes: arrayOf(shape()), + onClose: func.isRequired, + onDeleteAllClick: func.isRequired, + onKeyToggle: func.isRequired, + onSave: func.isRequired, + onToolsToggle: func.isRequired, + template: shape().isRequired, + toolsShown: bool.isRequired, +}; + +VisualizerToolbar.defaultProps = { + nodes: [], +}; + +export default withI18n()(VisualizerToolbar); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js index 7cff003e08..9c3200ca48 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js @@ -1,9 +1,7 @@ export { default as Visualizer } from './Visualizer'; -export { default as VisualizerToolbar } from './VisualizerToolbar'; export { default as VisualizerGraph } from './VisualizerGraph'; -export { default as VisualizerStartScreen } from './VisualizerStartScreen'; -export { default as VisualizerStartNode } from './VisualizerStartNode'; export { default as VisualizerLink } from './VisualizerLink'; export { default as VisualizerNode } from './VisualizerNode'; -export { default as VisualizerKey } from './VisualizerKey'; -export { default as VisualizerTools } from './VisualizerTools'; +export { default as VisualizerStartNode } from './VisualizerStartNode'; +export { default as VisualizerStartScreen } from './VisualizerStartScreen'; +export { default as VisualizerToolbar } from './VisualizerToolbar'; diff --git a/awx/ui_next/src/util/workflow.jsx b/awx/ui_next/src/util/workflow.jsx index a914aeda80..c37dca118f 100644 --- a/awx/ui_next/src/util/workflow.jsx +++ b/awx/ui_next/src/util/workflow.jsx @@ -18,8 +18,8 @@ export function calcZoomAndFit(gRef, svgRef) { .node() .getBoundingClientRect(); - gBoundingClientRect.height = gBoundingClientRect.height / currentScale; - gBoundingClientRect.width = gBoundingClientRect.width / currentScale; + gBoundingClientRect.height /= currentScale; + gBoundingClientRect.width /= currentScale; const gBBoxDimensions = d3 .select(gRef)