mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 08:18:43 -03:30
Move visualizer/workflow output state logic out to reducer and refactor some of the larger functions. Introduces contexts for state/dispatch that can be used by descendent components of both the visualizer and the workflow output components.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { WorkflowDispatchContext } from '@contexts/Workflow';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { func } from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
PauseIcon,
|
PauseIcon,
|
||||||
@@ -77,12 +77,14 @@ const Close = styled(TimesIcon)`
|
|||||||
top: 15px;
|
top: 15px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowLegend({ i18n, onClose }) {
|
function WorkflowLegend({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header>
|
<Header>
|
||||||
<b>{i18n._(t`Legend`)}</b>
|
<b>{i18n._(t`Legend`)}</b>
|
||||||
<Close onClick={onClose} />
|
<Close onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })} />
|
||||||
</Header>
|
</Header>
|
||||||
<Legend>
|
<Legend>
|
||||||
<li>
|
<li>
|
||||||
@@ -128,8 +130,4 @@ function WorkflowLegend({ i18n, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkflowLegend.propTypes = {
|
|
||||||
onClose: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(WorkflowLegend);
|
export default withI18n()(WorkflowLegend);
|
||||||
|
|||||||
@@ -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 styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
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 { PlusIcon } from '@patternfly/react-icons';
|
||||||
import { constants as wfConstants } from '@components/Workflow/WorkflowUtils';
|
import { constants as wfConstants } from '@components/Workflow/WorkflowUtils';
|
||||||
import {
|
import {
|
||||||
@@ -14,16 +18,11 @@ const StartG = styled.g`
|
|||||||
pointer-events: ${props => (props.ignorePointerEvents ? 'none' : 'auto')};
|
pointer-events: ${props => (props.ignorePointerEvents ? 'none' : 'auto')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowStartNode({
|
function WorkflowStartNode({ i18n, onUpdateHelpText, showActionTooltip }) {
|
||||||
addingLink,
|
|
||||||
i18n,
|
|
||||||
nodePositions,
|
|
||||||
onAddNodeClick,
|
|
||||||
onUpdateHelpText,
|
|
||||||
showActionTooltip,
|
|
||||||
}) {
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { addingLink, nodePositions } = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
const handleNodeMouseEnter = () => {
|
const handleNodeMouseEnter = () => {
|
||||||
ref.current.parentNode.appendChild(ref.current);
|
ref.current.parentNode.appendChild(ref.current);
|
||||||
@@ -62,7 +61,7 @@ function WorkflowStartNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onAddNodeClick(1);
|
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
@@ -77,16 +76,11 @@ function WorkflowStartNode({
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkflowStartNode.propTypes = {
|
WorkflowStartNode.propTypes = {
|
||||||
addingLink: bool,
|
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
onAddNodeClick: func,
|
|
||||||
showActionTooltip: bool.isRequired,
|
showActionTooltip: bool.isRequired,
|
||||||
onUpdateHelpText: func,
|
onUpdateHelpText: func,
|
||||||
};
|
};
|
||||||
|
|
||||||
WorkflowStartNode.defaultProps = {
|
WorkflowStartNode.defaultProps = {
|
||||||
addingLink: false,
|
|
||||||
onAddNodeClick: () => {},
|
|
||||||
onUpdateHelpText: () => {},
|
onUpdateHelpText: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||||
import WorkflowStartNode from './WorkflowStartNode';
|
import WorkflowStartNode from './WorkflowStartNode';
|
||||||
|
|
||||||
const nodePositions = {
|
const nodePositions = {
|
||||||
@@ -13,10 +14,12 @@ describe('WorkflowStartNode', () => {
|
|||||||
test('mounts successfully', () => {
|
test('mounts successfully', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowStartNode
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
nodePositions={nodePositions}
|
<WorkflowStartNode
|
||||||
showActionTooltip={false}
|
nodePositions={nodePositions}
|
||||||
/>
|
showActionTooltip={false}
|
||||||
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
@@ -24,7 +27,9 @@ describe('WorkflowStartNode', () => {
|
|||||||
test('tooltip shown on hover', () => {
|
test('tooltip shown on hover', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
|
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('WorkflowActionTooltip')).toHaveLength(0);
|
expect(wrapper.find('WorkflowActionTooltip')).toHaveLength(0);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { WorkflowDispatchContext } from '@contexts/Workflow';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -53,13 +54,13 @@ const Close = styled(TimesIcon)`
|
|||||||
|
|
||||||
function WorkflowTools({
|
function WorkflowTools({
|
||||||
i18n,
|
i18n,
|
||||||
onClose,
|
|
||||||
onFitGraph,
|
onFitGraph,
|
||||||
onPan,
|
onPan,
|
||||||
onPanToMiddle,
|
onPanToMiddle,
|
||||||
onZoomChange,
|
onZoomChange,
|
||||||
zoomPercentage,
|
zoomPercentage,
|
||||||
}) {
|
}) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const zoomIn = () => {
|
const zoomIn = () => {
|
||||||
const newScale =
|
const newScale =
|
||||||
Math.ceil((zoomPercentage + 10) / 10) * 10 < 200
|
Math.ceil((zoomPercentage + 10) / 10) * 10 < 200
|
||||||
@@ -80,7 +81,7 @@ function WorkflowTools({
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header>
|
<Header>
|
||||||
<b>{i18n._(t`Tools`)}</b>
|
<b>{i18n._(t`Tools`)}</b>
|
||||||
<Close onClick={onClose} />
|
<Close onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })} />
|
||||||
</Header>
|
</Header>
|
||||||
<Tools>
|
<Tools>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -178,7 +179,6 @@ function WorkflowTools({
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkflowTools.propTypes = {
|
WorkflowTools.propTypes = {
|
||||||
onClose: func.isRequired,
|
|
||||||
onFitGraph: func.isRequired,
|
onFitGraph: func.isRequired,
|
||||||
onPan: func.isRequired,
|
onPan: func.isRequired,
|
||||||
onPanToMiddle: func.isRequired,
|
onPanToMiddle: func.isRequired,
|
||||||
|
|||||||
632
awx/ui_next/src/components/Workflow/workflowReducer.js
Normal file
632
awx/ui_next/src/components/Workflow/workflowReducer.js
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
export default function visualizerReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'CREATE_LINK':
|
||||||
|
return createLink(state, action.linkType);
|
||||||
|
case 'CREATE_NODE':
|
||||||
|
return createNode(state, action.node);
|
||||||
|
case 'CANCEL_LINK':
|
||||||
|
return cancelLink(state);
|
||||||
|
case 'CANCEL_LINK_MODAL':
|
||||||
|
return cancelLinkModal(state);
|
||||||
|
case 'CANCEL_NODE_MODAL':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
addNodeSource: null,
|
||||||
|
addNodeTarget: null,
|
||||||
|
nodeToEdit: null,
|
||||||
|
};
|
||||||
|
case 'DELETE_ALL_NODES':
|
||||||
|
return deleteAllNodes(state);
|
||||||
|
case 'DELETE_LINK':
|
||||||
|
return deleteLink(state);
|
||||||
|
case 'DELETE_NODE':
|
||||||
|
return deleteNode(state);
|
||||||
|
case 'GENERATE_NODES_AND_LINKS':
|
||||||
|
return generateNodesAndLinks(state, action.nodes, action.i18n);
|
||||||
|
case 'SELECT_SOURCE_FOR_LINKING':
|
||||||
|
return selectSourceForLinking(state, action.node);
|
||||||
|
case 'SET_ADD_LINK_SOURCE_NODE':
|
||||||
|
return { ...state, addLinkSourceNode: action.value };
|
||||||
|
case 'SET_ADD_LINK_TARGET_NODE':
|
||||||
|
return { ...state, addLinkTargetNode: action.value };
|
||||||
|
case 'SET_ADD_NODE_SOURCE':
|
||||||
|
return { ...state, addNodeSource: action.value };
|
||||||
|
case 'SET_ADD_NODE_TARGET':
|
||||||
|
return { ...state, addNodeTarget: action.value };
|
||||||
|
case 'SET_ADDING_LINK':
|
||||||
|
return { ...state, addingLink: action.value };
|
||||||
|
case 'SET_CONTENT_ERROR':
|
||||||
|
return { ...state, contentError: action.value };
|
||||||
|
case 'SET_IS_LOADING':
|
||||||
|
return { ...state, isLoading: action.value };
|
||||||
|
case 'SET_LINK_TO_DELETE':
|
||||||
|
return { ...state, linkToDelete: action.value };
|
||||||
|
case 'SET_LINK_TO_EDIT':
|
||||||
|
return { ...state, linkToEdit: action.value };
|
||||||
|
case 'SET_LINKS':
|
||||||
|
return { ...state, links: action.value };
|
||||||
|
case 'SET_NEXT_NODE_ID':
|
||||||
|
return { ...state, nextNodeId: action.value };
|
||||||
|
case 'SET_NODE_POSITIONS':
|
||||||
|
return { ...state, nodePositions: action.value };
|
||||||
|
case 'SET_NODE_TO_DELETE':
|
||||||
|
return { ...state, nodeToDelete: action.value };
|
||||||
|
case 'SET_NODE_TO_EDIT':
|
||||||
|
return { ...state, nodeToEdit: action.value };
|
||||||
|
case 'SET_NODE_TO_VIEW':
|
||||||
|
return { ...state, nodeToView: action.value };
|
||||||
|
case 'SET_NODES':
|
||||||
|
return { ...state, nodes: action.value };
|
||||||
|
case 'SET_SHOW_DELETE_ALL_NODES_MODAL':
|
||||||
|
return { ...state, showDeleteAllNodesModal: action.value };
|
||||||
|
case 'SET_SHOW_LEGEND':
|
||||||
|
return { ...state, showLegend: action.value };
|
||||||
|
case 'SET_SHOW_TOOLS':
|
||||||
|
return { ...state, showTools: action.value };
|
||||||
|
case 'SET_SHOW_UNSAVED_CHANGES_MODAL':
|
||||||
|
return { ...state, showUnsavedChangesModal: action.value };
|
||||||
|
case 'SET_UNSAVED_CHANGES':
|
||||||
|
return { ...state, unsavedChanges: action.value };
|
||||||
|
case 'START_ADD_NODE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
addNodeSource: action.sourceNodeId,
|
||||||
|
addNodeTarget: action.targetNodeId || null,
|
||||||
|
};
|
||||||
|
case 'START_DELETE_LINK':
|
||||||
|
return startDeleteLink(state, action.link);
|
||||||
|
case 'TOGGLE_DELETE_ALL_NODES_MODAL':
|
||||||
|
return toggleDeleteAllNodesModal(state);
|
||||||
|
case 'TOGGLE_LEGEND':
|
||||||
|
return toggleLegend(state);
|
||||||
|
case 'TOGGLE_TOOLS':
|
||||||
|
return toggleTools(state);
|
||||||
|
case 'TOGGLE_UNSAVED_CHANGES_MODAL':
|
||||||
|
return toggleUnsavedChangesModal(state);
|
||||||
|
case 'UPDATE_LINK':
|
||||||
|
return updateLink(state, action.linkType);
|
||||||
|
case 'UPDATE_NODE':
|
||||||
|
return updateNode(state, action.node);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unrecognized action type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLink(state, linkType) {
|
||||||
|
const { addLinkSourceNode, addLinkTargetNode, links, nodes } = state;
|
||||||
|
const newLinks = [...links];
|
||||||
|
const newNodes = [...nodes];
|
||||||
|
|
||||||
|
newNodes.forEach(node => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
5
awx/ui_next/src/contexts/Workflow.jsx
Normal file
5
awx/ui_next/src/contexts/Workflow.jsx
Normal file
@@ -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);
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { shape } from 'prop-types';
|
import { shape } from 'prop-types';
|
||||||
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import { layoutGraph } from '@components/Workflow/WorkflowUtils';
|
import { layoutGraph } from '@components/Workflow/WorkflowUtils';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
|
import workflowReducer from '@components/Workflow/workflowReducer';
|
||||||
import { WorkflowJobsAPI } from '@api';
|
import { WorkflowJobsAPI } from '@api';
|
||||||
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
||||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||||
@@ -36,148 +40,50 @@ const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function WorkflowOutput({ job, i18n }) {
|
function WorkflowOutput({ job, i18n }) {
|
||||||
const [contentError, setContentError] = useState(null);
|
const [state, dispatch] = useReducer(workflowReducer, {
|
||||||
const [graphLinks, setGraphLinks] = useState([]);
|
contentError: null,
|
||||||
const [graphNodes, setGraphNodes] = useState([]);
|
isLoading: true,
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
links: [],
|
||||||
const [nodePositions, setNodePositions] = useState(null);
|
nextNodeId: 0,
|
||||||
const [showLegend, setShowLegend] = useState(false);
|
nodePositions: null,
|
||||||
const [showTools, setShowTools] = useState(false);
|
nodes: [],
|
||||||
|
showLegend: false,
|
||||||
|
showTools: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { contentError, isLoading, links, nodePositions, nodes } = state;
|
||||||
|
|
||||||
useEffect(() => {
|
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() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
const nodes = await fetchWorkflowNodes(job.id);
|
const workflowNodes = await fetchWorkflowNodes(job.id);
|
||||||
buildGraphArrays(nodes);
|
dispatch({
|
||||||
|
type: 'GENERATE_NODES_AND_LINKS',
|
||||||
|
nodes: workflowNodes,
|
||||||
|
i18n,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setContentError(error);
|
dispatch({ type: 'SET_CONTENT_ERROR', value: error });
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
dispatch({ type: 'SET_IS_LOADING', value: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [job.id, job.unified_job_template, i18n]);
|
}, [job.id, i18n]);
|
||||||
|
|
||||||
// Update positions of nodes/links
|
// Update positions of nodes/links
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (graphNodes) {
|
if (nodes) {
|
||||||
const newNodePositions = {};
|
const newNodePositions = {};
|
||||||
const g = layoutGraph(graphNodes, graphLinks);
|
const g = layoutGraph(nodes, links);
|
||||||
|
|
||||||
g.nodes().forEach(node => {
|
g.nodes().forEach(node => {
|
||||||
newNodePositions[node] = g.node(node);
|
newNodePositions[node] = g.node(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
setNodePositions(newNodePositions);
|
dispatch({ type: 'SET_NODE_POSITIONS', value: newNodePositions });
|
||||||
}
|
}
|
||||||
}, [graphLinks, graphNodes]);
|
}, [links, nodes]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -196,29 +102,16 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<WorkflowStateContext.Provider value={state}>
|
||||||
<Wrapper>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowOutputToolbar
|
<CardBody>
|
||||||
job={job}
|
<Wrapper>
|
||||||
legendShown={showLegend}
|
<WorkflowOutputToolbar job={job} />
|
||||||
nodes={graphNodes}
|
{nodePositions && <WorkflowOutputGraph />}
|
||||||
onLegendToggle={() => setShowLegend(!showLegend)}
|
</Wrapper>
|
||||||
onToolsToggle={() => setShowTools(!showTools)}
|
</CardBody>
|
||||||
toolsShown={showTools}
|
</WorkflowDispatchContext.Provider>
|
||||||
/>
|
</WorkflowStateContext.Provider>
|
||||||
{nodePositions && (
|
|
||||||
<WorkflowOutputGraph
|
|
||||||
links={graphLinks}
|
|
||||||
nodePositions={nodePositions}
|
|
||||||
nodes={graphNodes}
|
|
||||||
onUpdateShowLegend={setShowLegend}
|
|
||||||
onUpdateShowTools={setShowTools}
|
|
||||||
showLegend={showLegend}
|
|
||||||
showTools={showTools}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
</CardBody>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 * as d3 from 'd3';
|
||||||
import { arrayOf, bool, shape, func } from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
getScaleAndOffsetToFit,
|
getScaleAndOffsetToFit,
|
||||||
getTranslatePointsForZoom,
|
getTranslatePointsForZoom,
|
||||||
@@ -18,21 +18,17 @@ import {
|
|||||||
WorkflowTools,
|
WorkflowTools,
|
||||||
} from '@components/Workflow';
|
} from '@components/Workflow';
|
||||||
|
|
||||||
function WorkflowOutputGraph({
|
function WorkflowOutputGraph() {
|
||||||
links,
|
|
||||||
nodePositions,
|
|
||||||
nodes,
|
|
||||||
onUpdateShowLegend,
|
|
||||||
onUpdateShowTools,
|
|
||||||
showLegend,
|
|
||||||
showTools,
|
|
||||||
}) {
|
|
||||||
const [linkHelp, setLinkHelp] = useState();
|
const [linkHelp, setLinkHelp] = useState();
|
||||||
const [nodeHelp, setNodeHelp] = useState();
|
const [nodeHelp, setNodeHelp] = useState();
|
||||||
const [zoomPercentage, setZoomPercentage] = useState(100);
|
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const gRef = 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
|
// This is the zoom function called by using the mousewheel/click and drag
|
||||||
const zoom = () => {
|
const zoom = () => {
|
||||||
const translation = [d3.event.transform.x, d3.event.transform.y];
|
const translation = [d3.event.transform.x, d3.event.transform.y];
|
||||||
@@ -158,7 +154,7 @@ function WorkflowOutputGraph({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
{(nodeHelp || linkHelp) && (
|
{(nodeHelp || linkHelp) && (
|
||||||
<WorkflowHelp>
|
<WorkflowHelp>
|
||||||
{nodeHelp && <WorkflowNodeHelp node={nodeHelp} />}
|
{nodeHelp && <WorkflowNodeHelp node={nodeHelp} />}
|
||||||
@@ -172,16 +168,11 @@ function WorkflowOutputGraph({
|
|||||||
>
|
>
|
||||||
<g id="workflow-g" ref={gRef}>
|
<g id="workflow-g" ref={gRef}>
|
||||||
{nodePositions && [
|
{nodePositions && [
|
||||||
<WorkflowStartNode
|
<WorkflowStartNode key="start" showActionTooltip={false} />,
|
||||||
key="start"
|
|
||||||
nodePositions={nodePositions}
|
|
||||||
showActionTooltip={false}
|
|
||||||
/>,
|
|
||||||
links.map(link => (
|
links.map(link => (
|
||||||
<WorkflowOutputLink
|
<WorkflowOutputLink
|
||||||
key={`link-${link.source.id}-${link.target.id}`}
|
key={`link-${link.source.id}-${link.target.id}`}
|
||||||
link={link}
|
link={link}
|
||||||
nodePositions={nodePositions}
|
|
||||||
onUpdateLinkHelp={setLinkHelp}
|
onUpdateLinkHelp={setLinkHelp}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
@@ -193,7 +184,6 @@ function WorkflowOutputGraph({
|
|||||||
mouseEnter={() => setNodeHelp(node)}
|
mouseEnter={() => setNodeHelp(node)}
|
||||||
mouseLeave={() => setNodeHelp(null)}
|
mouseLeave={() => setNodeHelp(null)}
|
||||||
node={node}
|
node={node}
|
||||||
nodePositions={nodePositions}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -205,7 +195,6 @@ function WorkflowOutputGraph({
|
|||||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||||
{showTools && (
|
{showTools && (
|
||||||
<WorkflowTools
|
<WorkflowTools
|
||||||
onClose={() => onUpdateShowTools(false)}
|
|
||||||
onFitGraph={handleFitGraph}
|
onFitGraph={handleFitGraph}
|
||||||
onPan={handlePan}
|
onPan={handlePan}
|
||||||
onPanToMiddle={handlePanToMiddle}
|
onPanToMiddle={handlePanToMiddle}
|
||||||
@@ -213,22 +202,10 @@ function WorkflowOutputGraph({
|
|||||||
zoomPercentage={zoomPercentage}
|
zoomPercentage={zoomPercentage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showLegend && (
|
{showLegend && <WorkflowLegend />}
|
||||||
<WorkflowLegend onClose={() => onUpdateShowLegend(false)} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
export default WorkflowOutputGraph;
|
||||||
|
|||||||
@@ -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 { shape } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
generateLine,
|
generateLine,
|
||||||
@@ -6,11 +7,12 @@ import {
|
|||||||
getLinkOverlayPoints,
|
getLinkOverlayPoints,
|
||||||
} from '@components/Workflow/WorkflowUtils';
|
} from '@components/Workflow/WorkflowUtils';
|
||||||
|
|
||||||
function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) {
|
function WorkflowOutputLink({ link, onUpdateLinkHelp }) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
const [pathD, setPathD] = useState();
|
const [pathD, setPathD] = useState();
|
||||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||||
|
const { nodePositions } = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
const handleLinkMouseEnter = () => {
|
const handleLinkMouseEnter = () => {
|
||||||
ref.current.parentNode.appendChild(ref.current);
|
ref.current.parentNode.appendChild(ref.current);
|
||||||
@@ -65,7 +67,6 @@ function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) {
|
|||||||
|
|
||||||
WorkflowOutputLink.propTypes = {
|
WorkflowOutputLink.propTypes = {
|
||||||
link: shape().isRequired,
|
link: shape().isRequired,
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkflowOutputLink;
|
export default WorkflowOutputLink;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mount } from 'enzyme';
|
||||||
|
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||||
import WorkflowOutputLink from './WorkflowOutputLink';
|
import WorkflowOutputLink from './WorkflowOutputLink';
|
||||||
|
|
||||||
const link = {
|
const link = {
|
||||||
@@ -28,13 +29,15 @@ const nodePositions = {
|
|||||||
|
|
||||||
describe('WorkflowOutputLink', () => {
|
describe('WorkflowOutputLink', () => {
|
||||||
test('mounts successfully', () => {
|
test('mounts successfully', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowOutputLink
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
link={link}
|
<WorkflowOutputLink
|
||||||
nodePositions={nodePositions}
|
link={link}
|
||||||
onUpdateLinkHelp={() => {}}
|
nodePositions={nodePositions}
|
||||||
/>
|
onUpdateLinkHelp={() => {}}
|
||||||
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
|
|||||||
@@ -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 { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -55,14 +56,8 @@ const NodeDefaultLabel = styled.p`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowOutputNode({
|
function WorkflowOutputNode({ history, i18n, mouseEnter, mouseLeave, node }) {
|
||||||
history,
|
const { nodePositions } = useContext(WorkflowStateContext);
|
||||||
i18n,
|
|
||||||
mouseEnter,
|
|
||||||
mouseLeave,
|
|
||||||
node,
|
|
||||||
nodePositions,
|
|
||||||
}) {
|
|
||||||
let borderColor = '#93969A';
|
let borderColor = '#93969A';
|
||||||
|
|
||||||
if (node.job) {
|
if (node.job) {
|
||||||
@@ -105,7 +100,7 @@ function WorkflowOutputNode({
|
|||||||
/>
|
/>
|
||||||
<NodeContents height="60" width="180">
|
<NodeContents height="60" width="180">
|
||||||
{node.job ? (
|
{node.job ? (
|
||||||
<Fragment>
|
<>
|
||||||
<JobTopLine>
|
<JobTopLine>
|
||||||
<StatusIcon status={node.job.status} />
|
<StatusIcon status={node.job.status} />
|
||||||
<p>
|
<p>
|
||||||
@@ -115,7 +110,7 @@ function WorkflowOutputNode({
|
|||||||
</p>
|
</p>
|
||||||
</JobTopLine>
|
</JobTopLine>
|
||||||
<Elapsed>{secondsToHHMMSS(node.job.elapsed)}</Elapsed>
|
<Elapsed>{secondsToHHMMSS(node.job.elapsed)}</Elapsed>
|
||||||
</Fragment>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<NodeDefaultLabel>
|
<NodeDefaultLabel>
|
||||||
{node.unifiedJobTemplate
|
{node.unifiedJobTemplate
|
||||||
@@ -134,7 +129,6 @@ WorkflowOutputNode.propTypes = {
|
|||||||
mouseEnter: func.isRequired,
|
mouseEnter: func.isRequired,
|
||||||
mouseLeave: func.isRequired,
|
mouseLeave: func.isRequired,
|
||||||
node: shape().isRequired,
|
node: shape().isRequired,
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(withRouter(WorkflowOutputNode));
|
export default withI18n()(withRouter(WorkflowOutputNode));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import WorkflowOutputNode from './WorkflowOutputNode';
|
import WorkflowOutputNode from './WorkflowOutputNode';
|
||||||
|
|
||||||
@@ -48,12 +49,13 @@ describe('WorkflowOutputNode', () => {
|
|||||||
test('mounts successfully', () => {
|
test('mounts successfully', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowOutputNode
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
mouseEnter={() => {}}
|
<WorkflowOutputNode
|
||||||
mouseLeave={() => {}}
|
mouseEnter={() => {}}
|
||||||
node={nodeWithJT}
|
mouseLeave={() => {}}
|
||||||
nodePositions={nodePositions}
|
node={nodeWithJT}
|
||||||
/>
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
@@ -61,12 +63,13 @@ describe('WorkflowOutputNode', () => {
|
|||||||
test('node contents displayed correctly when Job and Job Template exist', () => {
|
test('node contents displayed correctly when Job and Job Template exist', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowOutputNode
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
mouseEnter={() => {}}
|
<WorkflowOutputNode
|
||||||
mouseLeave={() => {}}
|
mouseEnter={() => {}}
|
||||||
node={nodeWithJT}
|
mouseLeave={() => {}}
|
||||||
nodePositions={nodePositions}
|
node={nodeWithJT}
|
||||||
/>
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper.contains(<p>Automation JT</p>)).toEqual(true);
|
expect(wrapper.contains(<p>Automation JT</p>)).toEqual(true);
|
||||||
@@ -75,12 +78,13 @@ describe('WorkflowOutputNode', () => {
|
|||||||
test('node contents displayed correctly when Job Template deleted', () => {
|
test('node contents displayed correctly when Job Template deleted', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowOutputNode
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
mouseEnter={() => {}}
|
<WorkflowOutputNode
|
||||||
mouseLeave={() => {}}
|
mouseEnter={() => {}}
|
||||||
node={nodeWithoutJT}
|
mouseLeave={() => {}}
|
||||||
nodePositions={nodePositions}
|
node={nodeWithoutJT}
|
||||||
/>
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper.contains(<p>DELETED</p>)).toEqual(true);
|
expect(wrapper.contains(<p>DELETED</p>)).toEqual(true);
|
||||||
@@ -89,12 +93,13 @@ describe('WorkflowOutputNode', () => {
|
|||||||
test('node contents displayed correctly when Job deleted', () => {
|
test('node contents displayed correctly when Job deleted', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<svg>
|
<svg>
|
||||||
<WorkflowOutputNode
|
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||||
mouseEnter={() => {}}
|
<WorkflowOutputNode
|
||||||
mouseLeave={() => {}}
|
mouseEnter={() => {}}
|
||||||
node={{ id: 2 }}
|
mouseLeave={() => {}}
|
||||||
nodePositions={nodePositions}
|
node={{ id: 2 }}
|
||||||
/>
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
expect(wrapper.text()).toBe('DELETED');
|
expect(wrapper.text()).toBe('DELETED');
|
||||||
|
|||||||
@@ -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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
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 { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||||
import { CompassIcon, WrenchIcon } from '@patternfly/react-icons';
|
import { CompassIcon, WrenchIcon } from '@patternfly/react-icons';
|
||||||
import { StatusIcon } from '@components/Sparkline';
|
import { StatusIcon } from '@components/Sparkline';
|
||||||
@@ -53,15 +57,11 @@ const StatusIconWithMargin = styled(StatusIcon)`
|
|||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowOutputToolbar({
|
function WorkflowOutputToolbar({ i18n, job }) {
|
||||||
i18n,
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
job,
|
|
||||||
legendShown,
|
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||||
nodes,
|
|
||||||
onLegendToggle,
|
|
||||||
onToolsToggle,
|
|
||||||
toolsShown,
|
|
||||||
}) {
|
|
||||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,8 +76,8 @@ function WorkflowOutputToolbar({
|
|||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isActive={legendShown}
|
isActive={showLegend}
|
||||||
onClick={onLegendToggle}
|
onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
@@ -85,8 +85,8 @@ function WorkflowOutputToolbar({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isActive={toolsShown}
|
isActive={showTools}
|
||||||
onClick={onToolsToggle}
|
onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<WrenchIcon />
|
<WrenchIcon />
|
||||||
@@ -99,15 +99,6 @@ function WorkflowOutputToolbar({
|
|||||||
|
|
||||||
WorkflowOutputToolbar.propTypes = {
|
WorkflowOutputToolbar.propTypes = {
|
||||||
job: shape().isRequired,
|
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);
|
export default withI18n()(WorkflowOutputToolbar);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||||
|
|
||||||
@@ -7,17 +8,18 @@ const job = {
|
|||||||
status: 'successful',
|
status: 'successful',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const workflowContext = {
|
||||||
|
nodes: [],
|
||||||
|
showLegend: false,
|
||||||
|
showTools: false,
|
||||||
|
};
|
||||||
|
|
||||||
describe('WorkflowOutputToolbar', () => {
|
describe('WorkflowOutputToolbar', () => {
|
||||||
test('mounts successfully', () => {
|
test('mounts successfully', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<WorkflowOutputToolbar
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
job={job}
|
<WorkflowOutputToolbar job={job} />
|
||||||
legendShown={false}
|
</WorkflowStateContext.Provider>
|
||||||
nodes={[]}
|
|
||||||
onLegendToggle={() => {}}
|
|
||||||
onToolsToggle={() => {}}
|
|
||||||
toolsShown={false}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@@ -36,14 +38,9 @@ describe('WorkflowOutputToolbar', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<WorkflowOutputToolbar
|
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
||||||
job={job}
|
<WorkflowOutputToolbar job={job} />
|
||||||
legendShown={false}
|
</WorkflowStateContext.Provider>
|
||||||
nodes={nodes}
|
|
||||||
onLegendToggle={() => {}}
|
|
||||||
onToolsToggle={() => {}}
|
|
||||||
toolsShown={false}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
||||||
expect(wrapper.find('Badge').text()).toBe('1');
|
expect(wrapper.find('Badge').text()).toBe('1');
|
||||||
|
|||||||
@@ -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 { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func } from 'prop-types';
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
function DeleteAllNodesModal({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
actions={[
|
actions={[
|
||||||
@@ -13,7 +14,7 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
|||||||
key="remove"
|
key="remove"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`Confirm removal of all nodes`)}
|
aria-label={i18n._(t`Confirm removal of all nodes`)}
|
||||||
onClick={() => onConfirm()}
|
onClick={() => dispatch({ type: 'DELETE_ALL_NODES' })}
|
||||||
>
|
>
|
||||||
{i18n._(t`Remove`)}
|
{i18n._(t`Remove`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -21,13 +22,13 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
|||||||
key="cancel"
|
key="cancel"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={i18n._(t`Cancel node removal`)}
|
aria-label={i18n._(t`Cancel node removal`)}
|
||||||
onClick={onCancel}
|
onClick={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onCancel}
|
onClose={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })}
|
||||||
title={i18n._(t`Remove All Nodes`)}
|
title={i18n._(t`Remove All Nodes`)}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
@@ -40,9 +41,4 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteAllNodesModal.propTypes = {
|
|
||||||
onCancel: func.isRequired,
|
|
||||||
onConfirm: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(DeleteAllNodesModal);
|
export default withI18n()(DeleteAllNodesModal);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<LinkModal
|
||||||
|
header={
|
||||||
|
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||||
|
{i18n._(t`Add Link`)}
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
onConfirm={linkType => dispatch({ type: 'CREATE_LINK', linkType })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(LinkAddModal);
|
||||||
@@ -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 { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func, shape } from 'prop-types';
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
function LinkDeleteModal({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { linkToDelete } = useContext(WorkflowStateContext);
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title="Remove Link"
|
title="Remove Link"
|
||||||
isOpen={linkToDelete}
|
isOpen={linkToDelete}
|
||||||
onClose={onCancel}
|
onClose={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n._(t`Confirm link removal`)}
|
aria-label={i18n._(t`Confirm link removal`)}
|
||||||
key="remove"
|
key="remove"
|
||||||
onClick={() => onConfirm()}
|
onClick={() => dispatch({ type: 'DELETE_LINK' })}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
{i18n._(t`Remove`)}
|
{i18n._(t`Remove`)}
|
||||||
@@ -24,7 +29,7 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
|||||||
<Button
|
<Button
|
||||||
aria-label={i18n._(t`Cancel link removal`)}
|
aria-label={i18n._(t`Cancel link removal`)}
|
||||||
key="cancel"
|
key="cancel"
|
||||||
onClick={onCancel}
|
onClick={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
@@ -46,10 +51,4 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LinkDeleteModal.propTypes = {
|
|
||||||
linkToDelete: shape().isRequired,
|
|
||||||
onCancel: func.isRequired,
|
|
||||||
onConfirm: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(LinkDeleteModal);
|
export default withI18n()(LinkDeleteModal);
|
||||||
@@ -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 LinkEditModal({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
return (
|
||||||
|
<LinkModal
|
||||||
|
header={
|
||||||
|
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||||
|
{i18n._(t`Edit Link`)}
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
onConfirm={linkType => dispatch({ type: 'UPDATE_LINK', linkType })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(LinkEditModal);
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import { Button, FormGroup, Modal } from '@patternfly/react-core';
|
import { Button, FormGroup, Modal } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func, node, string } from 'prop-types';
|
import { func } from 'prop-types';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
|
||||||
function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
function LinkModal({ header, i18n, onConfirm }) {
|
||||||
const [newLinkType, setNewLinkType] = useState(linkType);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { linkToEdit } = useContext(WorkflowStateContext);
|
||||||
|
const [linkType, setLinkType] = useState(
|
||||||
|
linkToEdit ? linkToEdit.linkType : 'success'
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width={600}
|
width={600}
|
||||||
header={header}
|
header={header}
|
||||||
isOpen
|
isOpen
|
||||||
title={i18n._(t`Workflow Link`)}
|
title={i18n._(t`Workflow Link`)}
|
||||||
onClose={onCancel}
|
onClose={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="save"
|
key="save"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
aria-label={i18n._(t`Save link changes`)}
|
aria-label={i18n._(t`Save link changes`)}
|
||||||
onClick={() => onConfirm(newLinkType)}
|
onClick={() => onConfirm(linkType)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Save`)}
|
{i18n._(t`Save`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -27,7 +35,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
|||||||
key="cancel"
|
key="cancel"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={i18n._(t`Cancel link changes`)}
|
aria-label={i18n._(t`Cancel link changes`)}
|
||||||
onClick={onCancel}
|
onClick={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -36,7 +44,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
|||||||
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="link-select"
|
id="link-select"
|
||||||
value={newLinkType}
|
value={linkType}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: 'always',
|
value: 'always',
|
||||||
@@ -55,7 +63,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
setNewLinkType(value);
|
setLinkType(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -64,14 +72,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LinkModal.propTypes = {
|
LinkModal.propTypes = {
|
||||||
linkType: string,
|
|
||||||
header: node.isRequired,
|
|
||||||
onCancel: func.isRequired,
|
|
||||||
onConfirm: func.isRequired,
|
onConfirm: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinkModal.defaultProps = {
|
|
||||||
linkType: 'success',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(LinkModal);
|
export default withI18n()(LinkModal);
|
||||||
@@ -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';
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default as NodeModal } from './NodeModal';
|
|
||||||
export { default as NodeNextButton } from './NodeNextButton';
|
|
||||||
export { default as RunStep } from './RunStep';
|
|
||||||
@@ -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 (
|
||||||
|
<NodeModal
|
||||||
|
askLinkType={addNodeSource !== 1}
|
||||||
|
onSave={addNode}
|
||||||
|
title={i18n._(t`Add Node`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NodeAddModal);
|
||||||
@@ -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 { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func, shape } from 'prop-types';
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
function NodeDeleteModal({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { nodeToDelete } = useContext(WorkflowStateContext);
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Remove Node`)}
|
title={i18n._(t`Remove Node`)}
|
||||||
isOpen={nodeToDelete}
|
isOpen={nodeToDelete}
|
||||||
onClose={onCancel}
|
onClose={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="remove"
|
key="remove"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`Confirm node removal`)}
|
aria-label={i18n._(t`Confirm node removal`)}
|
||||||
onClick={() => onConfirm()}
|
onClick={() => dispatch({ type: 'DELETE_NODE' })}
|
||||||
>
|
>
|
||||||
{i18n._(t`Remove`)}
|
{i18n._(t`Remove`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -25,7 +30,7 @@ function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
|||||||
key="cancel"
|
key="cancel"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={i18n._(t`Cancel node removal`)}
|
aria-label={i18n._(t`Cancel node removal`)}
|
||||||
onClick={onCancel}
|
onClick={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -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);
|
export default withI18n()(NodeDeleteModal);
|
||||||
@@ -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 (
|
||||||
|
<NodeModal
|
||||||
|
askLinkType={false}
|
||||||
|
onSave={updateNode}
|
||||||
|
title={i18n._(t`Edit Node`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NodeEditModal);
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { bool, func, node, shape } from 'prop-types';
|
import { bool, node, func } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
WizardContextConsumer,
|
WizardContextConsumer,
|
||||||
@@ -12,15 +16,10 @@ import Wizard from '@components/Wizard';
|
|||||||
import { NodeTypeStep } from './NodeTypeStep';
|
import { NodeTypeStep } from './NodeTypeStep';
|
||||||
import { RunStep, NodeNextButton } from '.';
|
import { RunStep, NodeNextButton } from '.';
|
||||||
|
|
||||||
function NodeModal({
|
function NodeModal({ askLinkType, history, i18n, onSave, title }) {
|
||||||
askLinkType,
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
history,
|
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||||
i18n,
|
|
||||||
nodeToEdit,
|
|
||||||
onClose,
|
|
||||||
onSave,
|
|
||||||
title,
|
|
||||||
}) {
|
|
||||||
let defaultApprovalDescription = '';
|
let defaultApprovalDescription = '';
|
||||||
let defaultApprovalName = '';
|
let defaultApprovalName = '';
|
||||||
let defaultApprovalTimeout = 0;
|
let defaultApprovalTimeout = 0;
|
||||||
@@ -104,16 +103,12 @@ function NodeModal({
|
|||||||
}
|
}
|
||||||
: nodeResource;
|
: nodeResource;
|
||||||
|
|
||||||
onSave({
|
onSave(linkType, resource, nodeType);
|
||||||
linkType,
|
|
||||||
nodeResource: resource,
|
|
||||||
nodeType,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
clearQueryParams();
|
clearQueryParams();
|
||||||
onClose();
|
dispatch({ type: 'CANCEL_NODE_MODAL' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNodeTypeChange = newNodeType => {
|
const handleNodeTypeChange = newNodeType => {
|
||||||
@@ -211,14 +206,8 @@ function NodeModal({
|
|||||||
|
|
||||||
NodeModal.propTypes = {
|
NodeModal.propTypes = {
|
||||||
askLinkType: bool.isRequired,
|
askLinkType: bool.isRequired,
|
||||||
nodeToEdit: shape(),
|
|
||||||
onClose: func.isRequired,
|
|
||||||
onSave: func.isRequired,
|
onSave: func.isRequired,
|
||||||
title: node.isRequired,
|
title: node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
NodeModal.defaultProps = {
|
|
||||||
nodeToEdit: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(withRouter(NodeModal));
|
export default withI18n()(withRouter(NodeModal));
|
||||||
@@ -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 (
|
||||||
|
<Modal
|
||||||
|
isLarge
|
||||||
|
isOpen
|
||||||
|
title={i18n._(t`Node Details`)}
|
||||||
|
onClose={() => dispatch({ type: 'SET_NODE_TO_VIEW', value: null })}
|
||||||
|
>
|
||||||
|
Coming soon :)
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NodeViewModal);
|
||||||
@@ -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';
|
||||||
@@ -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 (
|
|
||||||
<Modal isLarge isOpen title={i18n._(t`Node Details`)} onClose={onClose}>
|
|
||||||
Coming soon :)
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeViewModal.propTypes = {
|
|
||||||
onClose: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(NodeViewModal);
|
|
||||||
@@ -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 { Button, Modal } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t, Trans } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { func } from 'prop-types';
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
function UnsavedChangesModal({ i18n, onSaveAndExit, onExit }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width={600}
|
width={600}
|
||||||
isOpen
|
isOpen
|
||||||
title={i18n._(t`Warning: Unsaved Changes`)}
|
title={i18n._(t`Warning: Unsaved Changes`)}
|
||||||
onClose={onCancel}
|
onClose={() => dispatch({ type: 'TOGGLE_UNSAVED_CHANGES_MODAL' })}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="exit"
|
key="exit"
|
||||||
@@ -41,7 +43,6 @@ function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UnsavedChangesModal.propTypes = {
|
UnsavedChangesModal.propTypes = {
|
||||||
onCancel: func.isRequired,
|
|
||||||
onExit: func.isRequired,
|
onExit: func.isRequired,
|
||||||
onSaveAndExit: func.isRequired,
|
onSaveAndExit: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,2 @@
|
|||||||
export { default as DeleteAllNodesModal } from './DeleteAllNodesModal';
|
export { default as DeleteAllNodesModal } from './DeleteAllNodesModal';
|
||||||
export { default as LinkDeleteModal } from './LinkDeleteModal';
|
|
||||||
export { default as LinkModal } from './LinkModal';
|
|
||||||
export { default as NodeDeleteModal } from './NodeDeleteModal';
|
|
||||||
export { default as NodeViewModal } from './NodeViewModal';
|
|
||||||
export { default as UnsavedChangesModal } from './UnsavedChangesModal';
|
export { default as UnsavedChangesModal } from './UnsavedChangesModal';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,12 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
import { bool } from 'prop-types';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import {
|
import {
|
||||||
getScaleAndOffsetToFit,
|
getScaleAndOffsetToFit,
|
||||||
@@ -32,28 +36,7 @@ const WorkflowSVG = styled.svg`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerGraph({
|
function VisualizerGraph({ i18n, readOnly }) {
|
||||||
addLinkSourceNode,
|
|
||||||
addingLink,
|
|
||||||
i18n,
|
|
||||||
links,
|
|
||||||
nodePositions,
|
|
||||||
nodes,
|
|
||||||
onAddNodeClick,
|
|
||||||
onCancelAddLinkClick,
|
|
||||||
onConfirmAddLinkClick,
|
|
||||||
onDeleteLinkClick,
|
|
||||||
onDeleteNodeClick,
|
|
||||||
onEditNodeClick,
|
|
||||||
onLinkEditClick,
|
|
||||||
onStartAddLinkClick,
|
|
||||||
onUpdateShowLegend,
|
|
||||||
onUpdateShowTools,
|
|
||||||
onViewNodeClick,
|
|
||||||
readOnly,
|
|
||||||
showLegend,
|
|
||||||
showTools,
|
|
||||||
}) {
|
|
||||||
const [helpText, setHelpText] = useState(null);
|
const [helpText, setHelpText] = useState(null);
|
||||||
const [linkHelp, setLinkHelp] = useState();
|
const [linkHelp, setLinkHelp] = useState();
|
||||||
const [nodeHelp, setNodeHelp] = useState();
|
const [nodeHelp, setNodeHelp] = useState();
|
||||||
@@ -61,6 +44,18 @@ function VisualizerGraph({
|
|||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const gRef = useRef(null);
|
const gRef = useRef(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
addLinkSourceNode,
|
||||||
|
addingLink,
|
||||||
|
links,
|
||||||
|
nodePositions,
|
||||||
|
nodes,
|
||||||
|
showLegend,
|
||||||
|
showTools,
|
||||||
|
} = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
|
||||||
const drawPotentialLinkToNode = node => {
|
const drawPotentialLinkToNode = node => {
|
||||||
if (node.id !== addLinkSourceNode.id) {
|
if (node.id !== addLinkSourceNode.id) {
|
||||||
const sourceNodeX = nodePositions[addLinkSourceNode.id].x;
|
const sourceNodeX = nodePositions[addLinkSourceNode.id].x;
|
||||||
@@ -81,7 +76,7 @@ function VisualizerGraph({
|
|||||||
|
|
||||||
const handleBackgroundClick = () => {
|
const handleBackgroundClick = () => {
|
||||||
setHelpText(null);
|
setHelpText(null);
|
||||||
onCancelAddLinkClick();
|
dispatch({ type: 'CANCEL_LINK' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawPotentialLinkToCursor = e => {
|
const drawPotentialLinkToCursor = e => {
|
||||||
@@ -274,10 +269,7 @@ function VisualizerGraph({
|
|||||||
<g id="workflow-g" ref={gRef}>
|
<g id="workflow-g" ref={gRef}>
|
||||||
{nodePositions && [
|
{nodePositions && [
|
||||||
<WorkflowStartNode
|
<WorkflowStartNode
|
||||||
addingLink={addingLink}
|
|
||||||
key="start"
|
key="start"
|
||||||
nodePositions={nodePositions}
|
|
||||||
onAddNodeClick={onAddNodeClick}
|
|
||||||
showActionTooltip={!readOnly}
|
showActionTooltip={!readOnly}
|
||||||
onUpdateHelpText={setHelpText}
|
onUpdateHelpText={setHelpText}
|
||||||
/>,
|
/>,
|
||||||
@@ -288,13 +280,8 @@ function VisualizerGraph({
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<VisualizerLink
|
<VisualizerLink
|
||||||
addingLink={addingLink}
|
|
||||||
key={`link-${link.source.id}-${link.target.id}`}
|
key={`link-${link.source.id}-${link.target.id}`}
|
||||||
link={link}
|
link={link}
|
||||||
nodePositions={nodePositions}
|
|
||||||
onAddNodeClick={onAddNodeClick}
|
|
||||||
onDeleteLinkClick={onDeleteLinkClick}
|
|
||||||
onLinkEditClick={onLinkEditClick}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onUpdateHelpText={setHelpText}
|
onUpdateHelpText={setHelpText}
|
||||||
onUpdateLinkHelp={setLinkHelp}
|
onUpdateLinkHelp={setLinkHelp}
|
||||||
@@ -307,19 +294,8 @@ function VisualizerGraph({
|
|||||||
if (node.id > 1 && nodePositions[node.id] && !node.isDeleted) {
|
if (node.id > 1 && nodePositions[node.id] && !node.isDeleted) {
|
||||||
return (
|
return (
|
||||||
<VisualizerNode
|
<VisualizerNode
|
||||||
addingLink={addingLink}
|
|
||||||
isAddLinkSourceNode={
|
|
||||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
|
||||||
}
|
|
||||||
key={`node-${node.id}`}
|
key={`node-${node.id}`}
|
||||||
node={node}
|
node={node}
|
||||||
nodePositions={nodePositions}
|
|
||||||
onAddNodeClick={onAddNodeClick}
|
|
||||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
|
||||||
onDeleteNodeClick={onDeleteNodeClick}
|
|
||||||
onEditNodeClick={onEditNodeClick}
|
|
||||||
onStartAddLinkClick={onStartAddLinkClick}
|
|
||||||
onViewNodeClick={onViewNodeClick}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onUpdateHelpText={setHelpText}
|
onUpdateHelpText={setHelpText}
|
||||||
updateNodeHelp={setNodeHelp}
|
updateNodeHelp={setNodeHelp}
|
||||||
@@ -346,7 +322,6 @@ function VisualizerGraph({
|
|||||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||||
{showTools && (
|
{showTools && (
|
||||||
<WorkflowTools
|
<WorkflowTools
|
||||||
onClose={() => onUpdateShowTools(false)}
|
|
||||||
onFitGraph={handleFitGraph}
|
onFitGraph={handleFitGraph}
|
||||||
onPan={handlePan}
|
onPan={handlePan}
|
||||||
onPanToMiddle={handlePanToMiddle}
|
onPanToMiddle={handlePanToMiddle}
|
||||||
@@ -354,38 +329,14 @@ function VisualizerGraph({
|
|||||||
zoomPercentage={zoomPercentage}
|
zoomPercentage={zoomPercentage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showLegend && (
|
{showLegend && <WorkflowLegend />}
|
||||||
<WorkflowLegend onClose={() => onUpdateShowLegend(false)} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
VisualizerGraph.propTypes = {
|
VisualizerGraph.propTypes = {
|
||||||
addLinkSourceNode: shape(),
|
|
||||||
addingLink: bool.isRequired,
|
|
||||||
links: arrayOf(shape()).isRequired,
|
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
nodes: arrayOf(shape()).isRequired,
|
|
||||||
onAddNodeClick: func.isRequired,
|
|
||||||
onCancelAddLinkClick: func.isRequired,
|
|
||||||
onConfirmAddLinkClick: func.isRequired,
|
|
||||||
onDeleteLinkClick: func.isRequired,
|
|
||||||
onDeleteNodeClick: func.isRequired,
|
|
||||||
onEditNodeClick: func.isRequired,
|
|
||||||
onLinkEditClick: func.isRequired,
|
|
||||||
onStartAddLinkClick: func.isRequired,
|
|
||||||
onUpdateShowLegend: func.isRequired,
|
|
||||||
onUpdateShowTools: func.isRequired,
|
|
||||||
onViewNodeClick: func.isRequired,
|
|
||||||
readOnly: bool.isRequired,
|
readOnly: bool.isRequired,
|
||||||
showLegend: bool.isRequired,
|
|
||||||
showTools: bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
VisualizerGraph.defaultProps = {
|
|
||||||
addLinkSourceNode: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(VisualizerGraph);
|
export default withI18n()(VisualizerGraph);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -19,13 +23,8 @@ const LinkG = styled.g`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerLink({
|
function VisualizerLink({
|
||||||
addingLink,
|
|
||||||
i18n,
|
i18n,
|
||||||
link,
|
link,
|
||||||
nodePositions,
|
|
||||||
onAddNodeClick,
|
|
||||||
onDeleteLinkClick,
|
|
||||||
onLinkEditClick,
|
|
||||||
onUpdateHelpText,
|
onUpdateHelpText,
|
||||||
onUpdateLinkHelp,
|
onUpdateLinkHelp,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -36,6 +35,8 @@ function VisualizerLink({
|
|||||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||||
const [tooltipX, setTooltipX] = useState();
|
const [tooltipX, setTooltipX] = useState();
|
||||||
const [tooltipY, setTooltipY] = useState();
|
const [tooltipY, setTooltipY] = useState();
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { addingLink, nodePositions } = useContext(WorkflowStateContext);
|
||||||
|
|
||||||
const addNodeAction = (
|
const addNodeAction = (
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
@@ -44,7 +45,11 @@ function VisualizerLink({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onAddNodeClick(link.source.id, link.target.id);
|
dispatch({
|
||||||
|
type: 'START_ADD_NODE',
|
||||||
|
sourceNodeId: link.source.id,
|
||||||
|
targetNodeId: link.target.id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
onUpdateHelpText(i18n._(t`Add a new node between these two nodes`))
|
onUpdateHelpText(i18n._(t`Add a new node between these two nodes`))
|
||||||
@@ -63,7 +68,7 @@ function VisualizerLink({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-edit"
|
id="link-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => onLinkEditClick(link)}
|
onClick={() => dispatch({ type: 'SET_LINK_TO_EDIT', value: link })}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this link`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this link`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
>
|
>
|
||||||
@@ -72,7 +77,7 @@ function VisualizerLink({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-delete"
|
id="link-delete"
|
||||||
key="delete"
|
key="delete"
|
||||||
onClick={() => onDeleteLinkClick(link)}
|
onClick={() => dispatch({ type: 'START_DELETE_LINK', link })}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this link`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this link`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
>
|
>
|
||||||
@@ -142,12 +147,7 @@ function VisualizerLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
VisualizerLink.propTypes = {
|
VisualizerLink.propTypes = {
|
||||||
addingLink: bool.isRequired,
|
|
||||||
link: shape().isRequired,
|
link: shape().isRequired,
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
onAddNodeClick: func.isRequired,
|
|
||||||
onDeleteLinkClick: func.isRequired,
|
|
||||||
onLinkEditClick: func.isRequired,
|
|
||||||
readOnly: bool.isRequired,
|
readOnly: bool.isRequired,
|
||||||
onUpdateHelpText: func.isRequired,
|
onUpdateHelpText: func.isRequired,
|
||||||
onUpdateLinkHelp: func.isRequired,
|
onUpdateLinkHelp: func.isRequired,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
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 styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -38,24 +42,21 @@ const NodeDefaultLabel = styled.p`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerNode({
|
function VisualizerNode({
|
||||||
addingLink,
|
|
||||||
i18n,
|
i18n,
|
||||||
isAddLinkSourceNode,
|
|
||||||
node,
|
node,
|
||||||
nodePositions,
|
|
||||||
onAddNodeClick,
|
|
||||||
onConfirmAddLinkClick,
|
|
||||||
onDeleteNodeClick,
|
|
||||||
onEditNodeClick,
|
|
||||||
onMouseOver,
|
onMouseOver,
|
||||||
onStartAddLinkClick,
|
|
||||||
onViewNodeClick,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
onUpdateHelpText,
|
onUpdateHelpText,
|
||||||
updateNodeHelp,
|
updateNodeHelp,
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
|
const { addingLink, addLinkSourceNode, nodePositions } = useContext(
|
||||||
|
WorkflowStateContext
|
||||||
|
);
|
||||||
|
const isAddLinkSourceNode =
|
||||||
|
addLinkSourceNode && addLinkSourceNode.id === node.id;
|
||||||
|
|
||||||
const handleNodeMouseEnter = () => {
|
const handleNodeMouseEnter = () => {
|
||||||
ref.current.parentNode.appendChild(ref.current);
|
ref.current.parentNode.appendChild(ref.current);
|
||||||
@@ -81,7 +82,7 @@ function VisualizerNode({
|
|||||||
|
|
||||||
const handleNodeClick = () => {
|
const handleNodeClick = () => {
|
||||||
if (addingLink && !node.isInvalidLinkTarget && !isAddLinkSourceNode) {
|
if (addingLink && !node.isInvalidLinkTarget && !isAddLinkSourceNode) {
|
||||||
onConfirmAddLinkClick(node);
|
dispatch({ type: 'SET_ADD_LINK_TARGET_NODE', value: node });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ function VisualizerNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onViewNodeClick(node);
|
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`View node details`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`View node details`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
@@ -110,7 +111,7 @@ function VisualizerNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onAddNodeClick(node.id);
|
dispatch({ type: 'START_ADD_NODE', sourceNodeId: node.id });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Add a new node`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`Add a new node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
@@ -124,7 +125,7 @@ function VisualizerNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onEditNodeClick(node);
|
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this node`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
@@ -137,7 +138,7 @@ function VisualizerNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onStartAddLinkClick(node);
|
dispatch({ type: 'SELECT_SOURCE_FOR_LINKING', node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
onUpdateHelpText(i18n._(t`Link to an available node`))
|
onUpdateHelpText(i18n._(t`Link to an available node`))
|
||||||
@@ -152,7 +153,7 @@ function VisualizerNode({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
onUpdateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onDeleteNodeClick(node);
|
dispatch({ type: 'SET_NODE_TO_DELETE', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this node`))}
|
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => onUpdateHelpText(null)}
|
||||||
@@ -214,24 +215,14 @@ function VisualizerNode({
|
|||||||
}
|
}
|
||||||
|
|
||||||
VisualizerNode.propTypes = {
|
VisualizerNode.propTypes = {
|
||||||
addingLink: bool.isRequired,
|
|
||||||
isAddLinkSourceNode: bool,
|
|
||||||
node: shape().isRequired,
|
node: shape().isRequired,
|
||||||
nodePositions: shape().isRequired,
|
|
||||||
onAddNodeClick: func.isRequired,
|
|
||||||
onConfirmAddLinkClick: func.isRequired,
|
|
||||||
onDeleteNodeClick: func.isRequired,
|
|
||||||
onEditNodeClick: func.isRequired,
|
|
||||||
onMouseOver: func,
|
onMouseOver: func,
|
||||||
onStartAddLinkClick: func.isRequired,
|
|
||||||
onViewNodeClick: func.isRequired,
|
|
||||||
readOnly: bool.isRequired,
|
readOnly: bool.isRequired,
|
||||||
onUpdateHelpText: func.isRequired,
|
onUpdateHelpText: func.isRequired,
|
||||||
updateNodeHelp: func.isRequired,
|
updateNodeHelp: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
VisualizerNode.defaultProps = {
|
VisualizerNode.defaultProps = {
|
||||||
isAddLinkSourceNode: false,
|
|
||||||
onMouseOver: () => {},
|
onMouseOver: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { WorkflowDispatchContext } from '@contexts/Workflow';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { func } from 'prop-types';
|
|
||||||
import { Button as PFButton } from '@patternfly/react-core';
|
import { Button as PFButton } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ const StartPanelWrapper = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerStartScreen({ i18n, onStartClick }) {
|
function VisualizerStartScreen({ i18n }) {
|
||||||
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
return (
|
return (
|
||||||
<div css="flex: 1">
|
<div css="flex: 1">
|
||||||
<StartPanelWrapper>
|
<StartPanelWrapper>
|
||||||
@@ -37,7 +38,9 @@ function VisualizerStartScreen({ i18n, onStartClick }) {
|
|||||||
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n._(t`Start`)}
|
aria-label={i18n._(t`Start`)}
|
||||||
onClick={() => onStartClick(1)}
|
onClick={() =>
|
||||||
|
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 })
|
||||||
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
>
|
>
|
||||||
{i18n._(t`Start`)}
|
{i18n._(t`Start`)}
|
||||||
@@ -48,8 +51,4 @@ function VisualizerStartScreen({ i18n, onStartClick }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
VisualizerStartScreen.propTypes = {
|
|
||||||
onStartClick: func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(VisualizerStartScreen);
|
export default withI18n()(VisualizerStartScreen);
|
||||||
|
|||||||
@@ -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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
import { func, shape } from 'prop-types';
|
||||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
BookIcon,
|
BookIcon,
|
||||||
@@ -36,18 +40,11 @@ const ActionButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerToolbar({
|
function VisualizerToolbar({ i18n, onClose, onSave, template }) {
|
||||||
i18n,
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
legendShown,
|
|
||||||
nodes,
|
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||||
onClose,
|
|
||||||
onDeleteAllClick,
|
|
||||||
onLegendToggle,
|
|
||||||
onSave,
|
|
||||||
onToolsToggle,
|
|
||||||
template,
|
|
||||||
toolsShown,
|
|
||||||
}) {
|
|
||||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,8 +61,8 @@ function VisualizerToolbar({
|
|||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isActive={legendShown}
|
isActive={showLegend}
|
||||||
onClick={onLegendToggle}
|
onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
@@ -73,8 +70,8 @@ function VisualizerToolbar({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isActive={toolsShown}
|
isActive={showTools}
|
||||||
onClick={onToolsToggle}
|
onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<WrenchIcon />
|
<WrenchIcon />
|
||||||
@@ -90,7 +87,12 @@ function VisualizerToolbar({
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
aria-label={i18n._(t`Delete all nodes`)}
|
aria-label={i18n._(t`Delete all nodes`)}
|
||||||
isDisabled={totalNodes === 0}
|
isDisabled={totalNodes === 0}
|
||||||
onClick={onDeleteAllClick}
|
onClick={() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
|
||||||
|
value: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
@@ -115,19 +117,9 @@ function VisualizerToolbar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
VisualizerToolbar.propTypes = {
|
VisualizerToolbar.propTypes = {
|
||||||
legendShown: bool.isRequired,
|
|
||||||
nodes: arrayOf(shape()),
|
|
||||||
onClose: func.isRequired,
|
onClose: func.isRequired,
|
||||||
onDeleteAllClick: func.isRequired,
|
|
||||||
onLegendToggle: func.isRequired,
|
|
||||||
onSave: func.isRequired,
|
onSave: func.isRequired,
|
||||||
onToolsToggle: func.isRequired,
|
|
||||||
template: shape().isRequired,
|
template: shape().isRequired,
|
||||||
toolsShown: bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
VisualizerToolbar.defaultProps = {
|
|
||||||
nodes: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(VisualizerToolbar);
|
export default withI18n()(VisualizerToolbar);
|
||||||
|
|||||||
Reference in New Issue
Block a user