mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 02:50:02 -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:
parent
a786118415
commit
2bbcd2d663
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { WorkflowDispatchContext } from '@contexts/Workflow';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { func } from 'prop-types';
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
PauseIcon,
|
||||
@ -77,12 +77,14 @@ const Close = styled(TimesIcon)`
|
||||
top: 15px;
|
||||
`;
|
||||
|
||||
function WorkflowLegend({ i18n, onClose }) {
|
||||
function WorkflowLegend({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header>
|
||||
<b>{i18n._(t`Legend`)}</b>
|
||||
<Close onClick={onClose} />
|
||||
<Close onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })} />
|
||||
</Header>
|
||||
<Legend>
|
||||
<li>
|
||||
@ -128,8 +130,4 @@ function WorkflowLegend({ i18n, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowLegend.propTypes = {
|
||||
onClose: func.isRequired,
|
||||
};
|
||||
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import { bool, func } from 'prop-types';
|
||||
import { PlusIcon } from '@patternfly/react-icons';
|
||||
import { constants as wfConstants } from '@components/Workflow/WorkflowUtils';
|
||||
import {
|
||||
@ -14,16 +18,11 @@ const StartG = styled.g`
|
||||
pointer-events: ${props => (props.ignorePointerEvents ? 'none' : 'auto')};
|
||||
`;
|
||||
|
||||
function WorkflowStartNode({
|
||||
addingLink,
|
||||
i18n,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
onUpdateHelpText,
|
||||
showActionTooltip,
|
||||
}) {
|
||||
function WorkflowStartNode({ i18n, onUpdateHelpText, showActionTooltip }) {
|
||||
const ref = useRef(null);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { addingLink, nodePositions } = useContext(WorkflowStateContext);
|
||||
|
||||
const handleNodeMouseEnter = () => {
|
||||
ref.current.parentNode.appendChild(ref.current);
|
||||
@ -62,7 +61,7 @@ function WorkflowStartNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onAddNodeClick(1);
|
||||
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 });
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
@ -77,16 +76,11 @@ function WorkflowStartNode({
|
||||
}
|
||||
|
||||
WorkflowStartNode.propTypes = {
|
||||
addingLink: bool,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func,
|
||||
showActionTooltip: bool.isRequired,
|
||||
onUpdateHelpText: func,
|
||||
};
|
||||
|
||||
WorkflowStartNode.defaultProps = {
|
||||
addingLink: false,
|
||||
onAddNodeClick: () => {},
|
||||
onUpdateHelpText: () => {},
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import WorkflowStartNode from './WorkflowStartNode';
|
||||
|
||||
const nodePositions = {
|
||||
@ -13,10 +14,12 @@ describe('WorkflowStartNode', () => {
|
||||
test('mounts successfully', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowStartNode
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip={false}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip={false}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
@ -24,7 +27,9 @@ describe('WorkflowStartNode', () => {
|
||||
test('tooltip shown on hover', () => {
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
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 { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
@ -53,13 +54,13 @@ const Close = styled(TimesIcon)`
|
||||
|
||||
function WorkflowTools({
|
||||
i18n,
|
||||
onClose,
|
||||
onFitGraph,
|
||||
onPan,
|
||||
onPanToMiddle,
|
||||
onZoomChange,
|
||||
zoomPercentage,
|
||||
}) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const zoomIn = () => {
|
||||
const newScale =
|
||||
Math.ceil((zoomPercentage + 10) / 10) * 10 < 200
|
||||
@ -80,7 +81,7 @@ function WorkflowTools({
|
||||
<Wrapper>
|
||||
<Header>
|
||||
<b>{i18n._(t`Tools`)}</b>
|
||||
<Close onClick={onClose} />
|
||||
<Close onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })} />
|
||||
</Header>
|
||||
<Tools>
|
||||
<Tooltip
|
||||
@ -178,7 +179,6 @@ function WorkflowTools({
|
||||
}
|
||||
|
||||
WorkflowTools.propTypes = {
|
||||
onClose: func.isRequired,
|
||||
onFitGraph: func.isRequired,
|
||||
onPan: 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 { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
||||
import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
} from '@contexts/Workflow';
|
||||
import { layoutGraph } from '@components/Workflow/WorkflowUtils';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import workflowReducer from '@components/Workflow/workflowReducer';
|
||||
import { WorkflowJobsAPI } from '@api';
|
||||
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||
@ -36,148 +40,50 @@ const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => {
|
||||
};
|
||||
|
||||
function WorkflowOutput({ job, i18n }) {
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [graphLinks, setGraphLinks] = useState([]);
|
||||
const [graphNodes, setGraphNodes] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [nodePositions, setNodePositions] = useState(null);
|
||||
const [showLegend, setShowLegend] = useState(false);
|
||||
const [showTools, setShowTools] = useState(false);
|
||||
const [state, dispatch] = useReducer(workflowReducer, {
|
||||
contentError: null,
|
||||
isLoading: true,
|
||||
links: [],
|
||||
nextNodeId: 0,
|
||||
nodePositions: null,
|
||||
nodes: [],
|
||||
showLegend: false,
|
||||
showTools: false,
|
||||
});
|
||||
|
||||
const { contentError, isLoading, links, nodePositions, nodes } = state;
|
||||
|
||||
useEffect(() => {
|
||||
const buildGraphArrays = nodes => {
|
||||
const allNodeIds = [];
|
||||
const arrayOfLinksForChart = [];
|
||||
const chartNodeIdToIndexMapping = {};
|
||||
const nodeIdToChartNodeIdMapping = {};
|
||||
const nodeRef = {};
|
||||
const nonRootNodeIds = [];
|
||||
let nodeIdCounter = 1;
|
||||
const arrayOfNodesForChart = [
|
||||
{
|
||||
id: nodeIdCounter,
|
||||
unifiedJobTemplate: {
|
||||
name: i18n._(t`START`),
|
||||
},
|
||||
type: 'node',
|
||||
},
|
||||
];
|
||||
nodeIdCounter++;
|
||||
// Assign each node an ID - 0 is reserved for the start node. We need to
|
||||
// make sure that we have an ID on every node including new nodes so the
|
||||
// ID returned by the api won't do
|
||||
nodes.forEach(node => {
|
||||
node.workflowMakerNodeId = nodeIdCounter;
|
||||
nodeRef[nodeIdCounter] = {
|
||||
originalNodeObject: node,
|
||||
};
|
||||
|
||||
const nodeObj = {
|
||||
index: nodeIdCounter - 1,
|
||||
id: nodeIdCounter,
|
||||
type: 'node',
|
||||
};
|
||||
|
||||
if (node.summary_fields.job) {
|
||||
nodeObj.job = node.summary_fields.job;
|
||||
}
|
||||
if (node.summary_fields.unified_job_template) {
|
||||
nodeRef[nodeIdCounter].unifiedJobTemplate =
|
||||
node.summary_fields.unified_job_template;
|
||||
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
|
||||
}
|
||||
|
||||
arrayOfNodesForChart.push(nodeObj);
|
||||
allNodeIds.push(node.id);
|
||||
nodeIdToChartNodeIdMapping[node.id] = node.workflowMakerNodeId;
|
||||
chartNodeIdToIndexMapping[nodeIdCounter] = nodeIdCounter - 1;
|
||||
nodeIdCounter++;
|
||||
});
|
||||
|
||||
nodes.forEach(node => {
|
||||
const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId];
|
||||
node.success_nodes.forEach(nodeId => {
|
||||
const targetIndex =
|
||||
chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
linkType: 'success',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
node.failure_nodes.forEach(nodeId => {
|
||||
const targetIndex =
|
||||
chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
linkType: 'failure',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
node.always_nodes.forEach(nodeId => {
|
||||
const targetIndex =
|
||||
chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds));
|
||||
|
||||
const rootNodes = allNodeIds.filter(
|
||||
nodeId => !uniqueNonRootNodeIds.includes(nodeId)
|
||||
);
|
||||
|
||||
rootNodes.forEach(rootNodeId => {
|
||||
const targetIndex =
|
||||
chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[rootNodeId]];
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[0],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
});
|
||||
|
||||
setGraphNodes(arrayOfNodesForChart);
|
||||
setGraphLinks(arrayOfLinksForChart);
|
||||
};
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const nodes = await fetchWorkflowNodes(job.id);
|
||||
buildGraphArrays(nodes);
|
||||
const workflowNodes = await fetchWorkflowNodes(job.id);
|
||||
dispatch({
|
||||
type: 'GENERATE_NODES_AND_LINKS',
|
||||
nodes: workflowNodes,
|
||||
i18n,
|
||||
});
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
dispatch({ type: 'SET_CONTENT_ERROR', value: error });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
dispatch({ type: 'SET_IS_LOADING', value: false });
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [job.id, job.unified_job_template, i18n]);
|
||||
}, [job.id, i18n]);
|
||||
|
||||
// Update positions of nodes/links
|
||||
useEffect(() => {
|
||||
if (graphNodes) {
|
||||
if (nodes) {
|
||||
const newNodePositions = {};
|
||||
const g = layoutGraph(graphNodes, graphLinks);
|
||||
const g = layoutGraph(nodes, links);
|
||||
|
||||
g.nodes().forEach(node => {
|
||||
newNodePositions[node] = g.node(node);
|
||||
});
|
||||
|
||||
setNodePositions(newNodePositions);
|
||||
dispatch({ type: 'SET_NODE_POSITIONS', value: newNodePositions });
|
||||
}
|
||||
}, [graphLinks, graphNodes]);
|
||||
}, [links, nodes]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@ -196,29 +102,16 @@ function WorkflowOutput({ job, i18n }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<Wrapper>
|
||||
<WorkflowOutputToolbar
|
||||
job={job}
|
||||
legendShown={showLegend}
|
||||
nodes={graphNodes}
|
||||
onLegendToggle={() => setShowLegend(!showLegend)}
|
||||
onToolsToggle={() => setShowTools(!showTools)}
|
||||
toolsShown={showTools}
|
||||
/>
|
||||
{nodePositions && (
|
||||
<WorkflowOutputGraph
|
||||
links={graphLinks}
|
||||
nodePositions={nodePositions}
|
||||
nodes={graphNodes}
|
||||
onUpdateShowLegend={setShowLegend}
|
||||
onUpdateShowTools={setShowTools}
|
||||
showLegend={showLegend}
|
||||
showTools={showTools}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</CardBody>
|
||||
<WorkflowStateContext.Provider value={state}>
|
||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||
<CardBody>
|
||||
<Wrapper>
|
||||
<WorkflowOutputToolbar job={job} />
|
||||
{nodePositions && <WorkflowOutputGraph />}
|
||||
</Wrapper>
|
||||
</CardBody>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
</WorkflowStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import * as d3 from 'd3';
|
||||
import { arrayOf, bool, shape, func } from 'prop-types';
|
||||
import {
|
||||
getScaleAndOffsetToFit,
|
||||
getTranslatePointsForZoom,
|
||||
@ -18,21 +18,17 @@ import {
|
||||
WorkflowTools,
|
||||
} from '@components/Workflow';
|
||||
|
||||
function WorkflowOutputGraph({
|
||||
links,
|
||||
nodePositions,
|
||||
nodes,
|
||||
onUpdateShowLegend,
|
||||
onUpdateShowTools,
|
||||
showLegend,
|
||||
showTools,
|
||||
}) {
|
||||
function WorkflowOutputGraph() {
|
||||
const [linkHelp, setLinkHelp] = useState();
|
||||
const [nodeHelp, setNodeHelp] = useState();
|
||||
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||
const svgRef = useRef(null);
|
||||
const gRef = useRef(null);
|
||||
|
||||
const { links, nodePositions, nodes, showLegend, showTools } = useContext(
|
||||
WorkflowStateContext
|
||||
);
|
||||
|
||||
// This is the zoom function called by using the mousewheel/click and drag
|
||||
const zoom = () => {
|
||||
const translation = [d3.event.transform.x, d3.event.transform.y];
|
||||
@ -158,7 +154,7 @@ function WorkflowOutputGraph({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
{(nodeHelp || linkHelp) && (
|
||||
<WorkflowHelp>
|
||||
{nodeHelp && <WorkflowNodeHelp node={nodeHelp} />}
|
||||
@ -172,16 +168,11 @@ function WorkflowOutputGraph({
|
||||
>
|
||||
<g id="workflow-g" ref={gRef}>
|
||||
{nodePositions && [
|
||||
<WorkflowStartNode
|
||||
key="start"
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip={false}
|
||||
/>,
|
||||
<WorkflowStartNode key="start" showActionTooltip={false} />,
|
||||
links.map(link => (
|
||||
<WorkflowOutputLink
|
||||
key={`link-${link.source.id}-${link.target.id}`}
|
||||
link={link}
|
||||
nodePositions={nodePositions}
|
||||
onUpdateLinkHelp={setLinkHelp}
|
||||
/>
|
||||
)),
|
||||
@ -193,7 +184,6 @@ function WorkflowOutputGraph({
|
||||
mouseEnter={() => setNodeHelp(node)}
|
||||
mouseLeave={() => setNodeHelp(null)}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -205,7 +195,6 @@ function WorkflowOutputGraph({
|
||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||
{showTools && (
|
||||
<WorkflowTools
|
||||
onClose={() => onUpdateShowTools(false)}
|
||||
onFitGraph={handleFitGraph}
|
||||
onPan={handlePan}
|
||||
onPanToMiddle={handlePanToMiddle}
|
||||
@ -213,22 +202,10 @@ function WorkflowOutputGraph({
|
||||
zoomPercentage={zoomPercentage}
|
||||
/>
|
||||
)}
|
||||
{showLegend && (
|
||||
<WorkflowLegend onClose={() => onUpdateShowLegend(false)} />
|
||||
)}
|
||||
{showLegend && <WorkflowLegend />}
|
||||
</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;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import { shape } from 'prop-types';
|
||||
import {
|
||||
generateLine,
|
||||
@ -6,11 +7,12 @@ import {
|
||||
getLinkOverlayPoints,
|
||||
} from '@components/Workflow/WorkflowUtils';
|
||||
|
||||
function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) {
|
||||
function WorkflowOutputLink({ link, onUpdateLinkHelp }) {
|
||||
const ref = useRef(null);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const [pathD, setPathD] = useState();
|
||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||
const { nodePositions } = useContext(WorkflowStateContext);
|
||||
|
||||
const handleLinkMouseEnter = () => {
|
||||
ref.current.parentNode.appendChild(ref.current);
|
||||
@ -65,7 +67,6 @@ function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) {
|
||||
|
||||
WorkflowOutputLink.propTypes = {
|
||||
link: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowOutputLink;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { mount } from 'enzyme';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import WorkflowOutputLink from './WorkflowOutputLink';
|
||||
|
||||
const link = {
|
||||
@ -28,13 +29,15 @@ const nodePositions = {
|
||||
|
||||
describe('WorkflowOutputLink', () => {
|
||||
test('mounts successfully', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
const wrapper = mount(
|
||||
<svg>
|
||||
<WorkflowOutputLink
|
||||
link={link}
|
||||
nodePositions={nodePositions}
|
||||
onUpdateLinkHelp={() => {}}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowOutputLink
|
||||
link={link}
|
||||
nodePositions={nodePositions}
|
||||
onUpdateLinkHelp={() => {}}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -55,14 +56,8 @@ const NodeDefaultLabel = styled.p`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
function WorkflowOutputNode({
|
||||
history,
|
||||
i18n,
|
||||
mouseEnter,
|
||||
mouseLeave,
|
||||
node,
|
||||
nodePositions,
|
||||
}) {
|
||||
function WorkflowOutputNode({ history, i18n, mouseEnter, mouseLeave, node }) {
|
||||
const { nodePositions } = useContext(WorkflowStateContext);
|
||||
let borderColor = '#93969A';
|
||||
|
||||
if (node.job) {
|
||||
@ -105,7 +100,7 @@ function WorkflowOutputNode({
|
||||
/>
|
||||
<NodeContents height="60" width="180">
|
||||
{node.job ? (
|
||||
<Fragment>
|
||||
<>
|
||||
<JobTopLine>
|
||||
<StatusIcon status={node.job.status} />
|
||||
<p>
|
||||
@ -115,7 +110,7 @@ function WorkflowOutputNode({
|
||||
</p>
|
||||
</JobTopLine>
|
||||
<Elapsed>{secondsToHHMMSS(node.job.elapsed)}</Elapsed>
|
||||
</Fragment>
|
||||
</>
|
||||
) : (
|
||||
<NodeDefaultLabel>
|
||||
{node.unifiedJobTemplate
|
||||
@ -134,7 +129,6 @@ WorkflowOutputNode.propTypes = {
|
||||
mouseEnter: func.isRequired,
|
||||
mouseLeave: func.isRequired,
|
||||
node: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(WorkflowOutputNode));
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import WorkflowOutputNode from './WorkflowOutputNode';
|
||||
|
||||
@ -48,12 +49,13 @@ describe('WorkflowOutputNode', () => {
|
||||
test('mounts successfully', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithJT}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithJT}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
@ -61,12 +63,13 @@ describe('WorkflowOutputNode', () => {
|
||||
test('node contents displayed correctly when Job and Job Template exist', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithJT}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithJT}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper.contains(<p>Automation JT</p>)).toEqual(true);
|
||||
@ -75,12 +78,13 @@ describe('WorkflowOutputNode', () => {
|
||||
test('node contents displayed correctly when Job Template deleted', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithoutJT}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={nodeWithoutJT}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper.contains(<p>DELETED</p>)).toEqual(true);
|
||||
@ -89,12 +93,13 @@ describe('WorkflowOutputNode', () => {
|
||||
test('node contents displayed correctly when Job deleted', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={{ id: 2 }}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowOutputNode
|
||||
mouseEnter={() => {}}
|
||||
mouseLeave={() => {}}
|
||||
node={{ id: 2 }}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</svg>
|
||||
);
|
||||
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 { t } from '@lingui/macro';
|
||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||
import { shape } from 'prop-types';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import { CompassIcon, WrenchIcon } from '@patternfly/react-icons';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
@ -53,15 +57,11 @@ const StatusIconWithMargin = styled(StatusIcon)`
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
function WorkflowOutputToolbar({
|
||||
i18n,
|
||||
job,
|
||||
legendShown,
|
||||
nodes,
|
||||
onLegendToggle,
|
||||
onToolsToggle,
|
||||
toolsShown,
|
||||
}) {
|
||||
function WorkflowOutputToolbar({ i18n, job }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||
|
||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||
|
||||
return (
|
||||
@ -76,8 +76,8 @@ function WorkflowOutputToolbar({
|
||||
<VerticalSeparator />
|
||||
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={legendShown}
|
||||
onClick={onLegendToggle}
|
||||
isActive={showLegend}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })}
|
||||
variant="plain"
|
||||
>
|
||||
<CompassIcon />
|
||||
@ -85,8 +85,8 @@ function WorkflowOutputToolbar({
|
||||
</Tooltip>
|
||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={toolsShown}
|
||||
onClick={onToolsToggle}
|
||||
isActive={showTools}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })}
|
||||
variant="plain"
|
||||
>
|
||||
<WrenchIcon />
|
||||
@ -99,15 +99,6 @@ function WorkflowOutputToolbar({
|
||||
|
||||
WorkflowOutputToolbar.propTypes = {
|
||||
job: shape().isRequired,
|
||||
legendShown: bool.isRequired,
|
||||
nodes: arrayOf(shape()),
|
||||
onLegendToggle: func.isRequired,
|
||||
onToolsToggle: func.isRequired,
|
||||
toolsShown: bool.isRequired,
|
||||
};
|
||||
|
||||
WorkflowOutputToolbar.defaultProps = {
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowOutputToolbar);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||
|
||||
@ -7,17 +8,18 @@ const job = {
|
||||
status: 'successful',
|
||||
};
|
||||
|
||||
const workflowContext = {
|
||||
nodes: [],
|
||||
showLegend: false,
|
||||
showTools: false,
|
||||
};
|
||||
|
||||
describe('WorkflowOutputToolbar', () => {
|
||||
test('mounts successfully', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<WorkflowOutputToolbar
|
||||
job={job}
|
||||
legendShown={false}
|
||||
nodes={[]}
|
||||
onLegendToggle={() => {}}
|
||||
onToolsToggle={() => {}}
|
||||
toolsShown={false}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={workflowContext}>
|
||||
<WorkflowOutputToolbar job={job} />
|
||||
</WorkflowStateContext.Provider>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
@ -36,14 +38,9 @@ describe('WorkflowOutputToolbar', () => {
|
||||
},
|
||||
];
|
||||
const wrapper = mountWithContexts(
|
||||
<WorkflowOutputToolbar
|
||||
job={job}
|
||||
legendShown={false}
|
||||
nodes={nodes}
|
||||
onLegendToggle={() => {}}
|
||||
onToolsToggle={() => {}}
|
||||
toolsShown={false}
|
||||
/>
|
||||
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
||||
<WorkflowOutputToolbar job={job} />
|
||||
</WorkflowStateContext.Provider>
|
||||
);
|
||||
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
function DeleteAllNodesModal({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
return (
|
||||
<AlertModal
|
||||
actions={[
|
||||
@ -13,7 +14,7 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
key="remove"
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`Confirm removal of all nodes`)}
|
||||
onClick={() => onConfirm()}
|
||||
onClick={() => dispatch({ type: 'DELETE_ALL_NODES' })}
|
||||
>
|
||||
{i18n._(t`Remove`)}
|
||||
</Button>,
|
||||
@ -21,13 +22,13 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Cancel node removal`)}
|
||||
onClick={onCancel}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
onClose={() => dispatch({ type: 'TOGGLE_DELETE_ALL_NODES_MODAL' })}
|
||||
title={i18n._(t`Remove All Nodes`)}
|
||||
variant="danger"
|
||||
>
|
||||
@ -40,9 +41,4 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
);
|
||||
}
|
||||
|
||||
DeleteAllNodesModal.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(DeleteAllNodesModal);
|
||||
|
||||
@ -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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||
function LinkDeleteModal({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { linkToDelete } = useContext(WorkflowStateContext);
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title="Remove Link"
|
||||
isOpen={linkToDelete}
|
||||
onClose={onCancel}
|
||||
onClose={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}
|
||||
actions={[
|
||||
<Button
|
||||
aria-label={i18n._(t`Confirm link removal`)}
|
||||
key="remove"
|
||||
onClick={() => onConfirm()}
|
||||
onClick={() => dispatch({ type: 'DELETE_LINK' })}
|
||||
variant="danger"
|
||||
>
|
||||
{i18n._(t`Remove`)}
|
||||
@ -24,7 +29,7 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||
<Button
|
||||
aria-label={i18n._(t`Cancel link removal`)}
|
||||
key="cancel"
|
||||
onClick={onCancel}
|
||||
onClick={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}
|
||||
variant="secondary"
|
||||
>
|
||||
{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);
|
||||
@ -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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, node, string } from 'prop-types';
|
||||
import { func } from 'prop-types';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
|
||||
function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
const [newLinkType, setNewLinkType] = useState(linkType);
|
||||
function LinkModal({ header, i18n, onConfirm }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { linkToEdit } = useContext(WorkflowStateContext);
|
||||
const [linkType, setLinkType] = useState(
|
||||
linkToEdit ? linkToEdit.linkType : 'success'
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
header={header}
|
||||
isOpen
|
||||
title={i18n._(t`Workflow Link`)}
|
||||
onClose={onCancel}
|
||||
onClose={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
||||
actions={[
|
||||
<Button
|
||||
key="save"
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Save link changes`)}
|
||||
onClick={() => onConfirm(newLinkType)}
|
||||
onClick={() => onConfirm(linkType)}
|
||||
>
|
||||
{i18n._(t`Save`)}
|
||||
</Button>,
|
||||
@ -27,7 +35,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Cancel link changes`)}
|
||||
onClick={onCancel}
|
||||
onClick={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
@ -36,7 +44,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
||||
<AnsibleSelect
|
||||
id="link-select"
|
||||
value={newLinkType}
|
||||
value={linkType}
|
||||
data={[
|
||||
{
|
||||
value: 'always',
|
||||
@ -55,7 +63,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
},
|
||||
]}
|
||||
onChange={(event, value) => {
|
||||
setNewLinkType(value);
|
||||
setLinkType(value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -64,14 +72,7 @@ function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
}
|
||||
|
||||
LinkModal.propTypes = {
|
||||
linkType: string,
|
||||
header: node.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
LinkModal.defaultProps = {
|
||||
linkType: 'success',
|
||||
};
|
||||
|
||||
export default withI18n()(LinkModal);
|
||||
@ -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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
||||
function NodeDeleteModal({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { nodeToDelete } = useContext(WorkflowStateContext);
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Remove Node`)}
|
||||
isOpen={nodeToDelete}
|
||||
onClose={onCancel}
|
||||
onClose={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })}
|
||||
actions={[
|
||||
<Button
|
||||
key="remove"
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`Confirm node removal`)}
|
||||
onClick={() => onConfirm()}
|
||||
onClick={() => dispatch({ type: 'DELETE_NODE' })}
|
||||
>
|
||||
{i18n._(t`Remove`)}
|
||||
</Button>,
|
||||
@ -25,7 +30,7 @@ function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Cancel node removal`)}
|
||||
onClick={onCancel}
|
||||
onClick={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</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);
|
||||
@ -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 {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
} from '@contexts/Workflow';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, node, shape } from 'prop-types';
|
||||
import { bool, node, func } from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
WizardContextConsumer,
|
||||
@ -12,15 +16,10 @@ import Wizard from '@components/Wizard';
|
||||
import { NodeTypeStep } from './NodeTypeStep';
|
||||
import { RunStep, NodeNextButton } from '.';
|
||||
|
||||
function NodeModal({
|
||||
askLinkType,
|
||||
history,
|
||||
i18n,
|
||||
nodeToEdit,
|
||||
onClose,
|
||||
onSave,
|
||||
title,
|
||||
}) {
|
||||
function NodeModal({ askLinkType, history, i18n, onSave, title }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { nodeToEdit } = useContext(WorkflowStateContext);
|
||||
|
||||
let defaultApprovalDescription = '';
|
||||
let defaultApprovalName = '';
|
||||
let defaultApprovalTimeout = 0;
|
||||
@ -104,16 +103,12 @@ function NodeModal({
|
||||
}
|
||||
: nodeResource;
|
||||
|
||||
onSave({
|
||||
linkType,
|
||||
nodeResource: resource,
|
||||
nodeType,
|
||||
});
|
||||
onSave(linkType, resource, nodeType);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
clearQueryParams();
|
||||
onClose();
|
||||
dispatch({ type: 'CANCEL_NODE_MODAL' });
|
||||
};
|
||||
|
||||
const handleNodeTypeChange = newNodeType => {
|
||||
@ -211,14 +206,8 @@ function NodeModal({
|
||||
|
||||
NodeModal.propTypes = {
|
||||
askLinkType: bool.isRequired,
|
||||
nodeToEdit: shape(),
|
||||
onClose: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
title: node.isRequired,
|
||||
};
|
||||
|
||||
NodeModal.defaultProps = {
|
||||
nodeToEdit: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(NodeModal));
|
||||
@ -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 { withI18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
|
||||
function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
||||
function UnsavedChangesModal({ i18n, onSaveAndExit, onExit }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
isOpen
|
||||
title={i18n._(t`Warning: Unsaved Changes`)}
|
||||
onClose={onCancel}
|
||||
onClose={() => dispatch({ type: 'TOGGLE_UNSAVED_CHANGES_MODAL' })}
|
||||
actions={[
|
||||
<Button
|
||||
key="exit"
|
||||
@ -41,7 +43,6 @@ function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
||||
}
|
||||
|
||||
UnsavedChangesModal.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onExit: func.isRequired,
|
||||
onSaveAndExit: func.isRequired,
|
||||
};
|
||||
|
||||
@ -1,6 +1,2 @@
|
||||
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';
|
||||
|
||||
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 { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||
import { bool } from 'prop-types';
|
||||
import * as d3 from 'd3';
|
||||
import {
|
||||
getScaleAndOffsetToFit,
|
||||
@ -32,28 +36,7 @@ const WorkflowSVG = styled.svg`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
function VisualizerGraph({
|
||||
addLinkSourceNode,
|
||||
addingLink,
|
||||
i18n,
|
||||
links,
|
||||
nodePositions,
|
||||
nodes,
|
||||
onAddNodeClick,
|
||||
onCancelAddLinkClick,
|
||||
onConfirmAddLinkClick,
|
||||
onDeleteLinkClick,
|
||||
onDeleteNodeClick,
|
||||
onEditNodeClick,
|
||||
onLinkEditClick,
|
||||
onStartAddLinkClick,
|
||||
onUpdateShowLegend,
|
||||
onUpdateShowTools,
|
||||
onViewNodeClick,
|
||||
readOnly,
|
||||
showLegend,
|
||||
showTools,
|
||||
}) {
|
||||
function VisualizerGraph({ i18n, readOnly }) {
|
||||
const [helpText, setHelpText] = useState(null);
|
||||
const [linkHelp, setLinkHelp] = useState();
|
||||
const [nodeHelp, setNodeHelp] = useState();
|
||||
@ -61,6 +44,18 @@ function VisualizerGraph({
|
||||
const svgRef = useRef(null);
|
||||
const gRef = useRef(null);
|
||||
|
||||
const {
|
||||
addLinkSourceNode,
|
||||
addingLink,
|
||||
links,
|
||||
nodePositions,
|
||||
nodes,
|
||||
showLegend,
|
||||
showTools,
|
||||
} = useContext(WorkflowStateContext);
|
||||
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
const drawPotentialLinkToNode = node => {
|
||||
if (node.id !== addLinkSourceNode.id) {
|
||||
const sourceNodeX = nodePositions[addLinkSourceNode.id].x;
|
||||
@ -81,7 +76,7 @@ function VisualizerGraph({
|
||||
|
||||
const handleBackgroundClick = () => {
|
||||
setHelpText(null);
|
||||
onCancelAddLinkClick();
|
||||
dispatch({ type: 'CANCEL_LINK' });
|
||||
};
|
||||
|
||||
const drawPotentialLinkToCursor = e => {
|
||||
@ -274,10 +269,7 @@ function VisualizerGraph({
|
||||
<g id="workflow-g" ref={gRef}>
|
||||
{nodePositions && [
|
||||
<WorkflowStartNode
|
||||
addingLink={addingLink}
|
||||
key="start"
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
showActionTooltip={!readOnly}
|
||||
onUpdateHelpText={setHelpText}
|
||||
/>,
|
||||
@ -288,13 +280,8 @@ function VisualizerGraph({
|
||||
) {
|
||||
return (
|
||||
<VisualizerLink
|
||||
addingLink={addingLink}
|
||||
key={`link-${link.source.id}-${link.target.id}`}
|
||||
link={link}
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
onDeleteLinkClick={onDeleteLinkClick}
|
||||
onLinkEditClick={onLinkEditClick}
|
||||
readOnly={readOnly}
|
||||
onUpdateHelpText={setHelpText}
|
||||
onUpdateLinkHelp={setLinkHelp}
|
||||
@ -307,19 +294,8 @@ function VisualizerGraph({
|
||||
if (node.id > 1 && nodePositions[node.id] && !node.isDeleted) {
|
||||
return (
|
||||
<VisualizerNode
|
||||
addingLink={addingLink}
|
||||
isAddLinkSourceNode={
|
||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
||||
}
|
||||
key={`node-${node.id}`}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
||||
onDeleteNodeClick={onDeleteNodeClick}
|
||||
onEditNodeClick={onEditNodeClick}
|
||||
onStartAddLinkClick={onStartAddLinkClick}
|
||||
onViewNodeClick={onViewNodeClick}
|
||||
readOnly={readOnly}
|
||||
onUpdateHelpText={setHelpText}
|
||||
updateNodeHelp={setNodeHelp}
|
||||
@ -346,7 +322,6 @@ function VisualizerGraph({
|
||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||
{showTools && (
|
||||
<WorkflowTools
|
||||
onClose={() => onUpdateShowTools(false)}
|
||||
onFitGraph={handleFitGraph}
|
||||
onPan={handlePan}
|
||||
onPanToMiddle={handlePanToMiddle}
|
||||
@ -354,38 +329,14 @@ function VisualizerGraph({
|
||||
zoomPercentage={zoomPercentage}
|
||||
/>
|
||||
)}
|
||||
{showLegend && (
|
||||
<WorkflowLegend onClose={() => onUpdateShowLegend(false)} />
|
||||
)}
|
||||
{showLegend && <WorkflowLegend />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
showLegend: bool.isRequired,
|
||||
showTools: bool.isRequired,
|
||||
};
|
||||
|
||||
VisualizerGraph.defaultProps = {
|
||||
addLinkSourceNode: {},
|
||||
};
|
||||
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -19,13 +23,8 @@ const LinkG = styled.g`
|
||||
`;
|
||||
|
||||
function VisualizerLink({
|
||||
addingLink,
|
||||
i18n,
|
||||
link,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
onDeleteLinkClick,
|
||||
onLinkEditClick,
|
||||
onUpdateHelpText,
|
||||
onUpdateLinkHelp,
|
||||
readOnly,
|
||||
@ -36,6 +35,8 @@ function VisualizerLink({
|
||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||
const [tooltipX, setTooltipX] = useState();
|
||||
const [tooltipY, setTooltipY] = useState();
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { addingLink, nodePositions } = useContext(WorkflowStateContext);
|
||||
|
||||
const addNodeAction = (
|
||||
<WorkflowActionTooltipItem
|
||||
@ -44,7 +45,11 @@ function VisualizerLink({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onAddNodeClick(link.source.id, link.target.id);
|
||||
dispatch({
|
||||
type: 'START_ADD_NODE',
|
||||
sourceNodeId: link.source.id,
|
||||
targetNodeId: link.target.id,
|
||||
});
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
onUpdateHelpText(i18n._(t`Add a new node between these two nodes`))
|
||||
@ -63,7 +68,7 @@ function VisualizerLink({
|
||||
<WorkflowActionTooltipItem
|
||||
id="link-edit"
|
||||
key="edit"
|
||||
onClick={() => onLinkEditClick(link)}
|
||||
onClick={() => dispatch({ type: 'SET_LINK_TO_EDIT', value: link })}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this link`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
>
|
||||
@ -72,7 +77,7 @@ function VisualizerLink({
|
||||
<WorkflowActionTooltipItem
|
||||
id="link-delete"
|
||||
key="delete"
|
||||
onClick={() => onDeleteLinkClick(link)}
|
||||
onClick={() => dispatch({ type: 'START_DELETE_LINK', link })}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this link`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
>
|
||||
@ -142,12 +147,7 @@ function VisualizerLink({
|
||||
}
|
||||
|
||||
VisualizerLink.propTypes = {
|
||||
addingLink: bool.isRequired,
|
||||
link: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
onDeleteLinkClick: func.isRequired,
|
||||
onLinkEditClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
onUpdateHelpText: 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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -38,24 +42,21 @@ const NodeDefaultLabel = styled.p`
|
||||
`;
|
||||
|
||||
function VisualizerNode({
|
||||
addingLink,
|
||||
i18n,
|
||||
isAddLinkSourceNode,
|
||||
node,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
onConfirmAddLinkClick,
|
||||
onDeleteNodeClick,
|
||||
onEditNodeClick,
|
||||
onMouseOver,
|
||||
onStartAddLinkClick,
|
||||
onViewNodeClick,
|
||||
readOnly,
|
||||
onUpdateHelpText,
|
||||
updateNodeHelp,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
const { addingLink, addLinkSourceNode, nodePositions } = useContext(
|
||||
WorkflowStateContext
|
||||
);
|
||||
const isAddLinkSourceNode =
|
||||
addLinkSourceNode && addLinkSourceNode.id === node.id;
|
||||
|
||||
const handleNodeMouseEnter = () => {
|
||||
ref.current.parentNode.appendChild(ref.current);
|
||||
@ -81,7 +82,7 @@ function VisualizerNode({
|
||||
|
||||
const handleNodeClick = () => {
|
||||
if (addingLink && !node.isInvalidLinkTarget && !isAddLinkSourceNode) {
|
||||
onConfirmAddLinkClick(node);
|
||||
dispatch({ type: 'SET_ADD_LINK_TARGET_NODE', value: node });
|
||||
}
|
||||
};
|
||||
|
||||
@ -92,7 +93,7 @@ function VisualizerNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onViewNodeClick(node);
|
||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
||||
}}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`View node details`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
@ -110,7 +111,7 @@ function VisualizerNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onAddNodeClick(node.id);
|
||||
dispatch({ type: 'START_ADD_NODE', sourceNodeId: node.id });
|
||||
}}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Add a new node`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
@ -124,7 +125,7 @@ function VisualizerNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onEditNodeClick(node);
|
||||
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
||||
}}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this node`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
@ -137,7 +138,7 @@ function VisualizerNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onStartAddLinkClick(node);
|
||||
dispatch({ type: 'SELECT_SOURCE_FOR_LINKING', node });
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
onUpdateHelpText(i18n._(t`Link to an available node`))
|
||||
@ -152,7 +153,7 @@ function VisualizerNode({
|
||||
onClick={() => {
|
||||
onUpdateHelpText(null);
|
||||
setHovering(false);
|
||||
onDeleteNodeClick(node);
|
||||
dispatch({ type: 'SET_NODE_TO_DELETE', value: node });
|
||||
}}
|
||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this node`))}
|
||||
onMouseLeave={() => onUpdateHelpText(null)}
|
||||
@ -214,24 +215,14 @@ function VisualizerNode({
|
||||
}
|
||||
|
||||
VisualizerNode.propTypes = {
|
||||
addingLink: bool.isRequired,
|
||||
isAddLinkSourceNode: bool,
|
||||
node: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
onConfirmAddLinkClick: func.isRequired,
|
||||
onDeleteNodeClick: func.isRequired,
|
||||
onEditNodeClick: func.isRequired,
|
||||
onMouseOver: func,
|
||||
onStartAddLinkClick: func.isRequired,
|
||||
onViewNodeClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
onUpdateHelpText: func.isRequired,
|
||||
updateNodeHelp: func.isRequired,
|
||||
};
|
||||
|
||||
VisualizerNode.defaultProps = {
|
||||
isAddLinkSourceNode: false,
|
||||
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 { t } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
import { Button as PFButton } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -29,7 +29,8 @@ const StartPanelWrapper = styled.div`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
function VisualizerStartScreen({ i18n, onStartClick }) {
|
||||
function VisualizerStartScreen({ i18n }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
return (
|
||||
<div css="flex: 1">
|
||||
<StartPanelWrapper>
|
||||
@ -37,7 +38,9 @@ function VisualizerStartScreen({ i18n, onStartClick }) {
|
||||
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
||||
<Button
|
||||
aria-label={i18n._(t`Start`)}
|
||||
onClick={() => onStartClick(1)}
|
||||
onClick={() =>
|
||||
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 })
|
||||
}
|
||||
variant="primary"
|
||||
>
|
||||
{i18n._(t`Start`)}
|
||||
@ -48,8 +51,4 @@ function VisualizerStartScreen({ i18n, onStartClick }) {
|
||||
);
|
||||
}
|
||||
|
||||
VisualizerStartScreen.propTypes = {
|
||||
onStartClick: func.isRequired,
|
||||
};
|
||||
|
||||
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 { 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 {
|
||||
BookIcon,
|
||||
@ -36,18 +40,11 @@ const ActionButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
function VisualizerToolbar({
|
||||
i18n,
|
||||
legendShown,
|
||||
nodes,
|
||||
onClose,
|
||||
onDeleteAllClick,
|
||||
onLegendToggle,
|
||||
onSave,
|
||||
onToolsToggle,
|
||||
template,
|
||||
toolsShown,
|
||||
}) {
|
||||
function VisualizerToolbar({ i18n, onClose, onSave, template }) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||
|
||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||
|
||||
return (
|
||||
@ -64,8 +61,8 @@ function VisualizerToolbar({
|
||||
<VerticalSeparator />
|
||||
<Tooltip content={i18n._(t`Toggle Legend`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={legendShown}
|
||||
onClick={onLegendToggle}
|
||||
isActive={showLegend}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_LEGEND' })}
|
||||
variant="plain"
|
||||
>
|
||||
<CompassIcon />
|
||||
@ -73,8 +70,8 @@ function VisualizerToolbar({
|
||||
</Tooltip>
|
||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={toolsShown}
|
||||
onClick={onToolsToggle}
|
||||
isActive={showTools}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_TOOLS' })}
|
||||
variant="plain"
|
||||
>
|
||||
<WrenchIcon />
|
||||
@ -90,7 +87,12 @@ function VisualizerToolbar({
|
||||
<ActionButton
|
||||
aria-label={i18n._(t`Delete all nodes`)}
|
||||
isDisabled={totalNodes === 0}
|
||||
onClick={onDeleteAllClick}
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
variant="plain"
|
||||
>
|
||||
<TrashAltIcon />
|
||||
@ -115,19 +117,9 @@ function VisualizerToolbar({
|
||||
}
|
||||
|
||||
VisualizerToolbar.propTypes = {
|
||||
legendShown: bool.isRequired,
|
||||
nodes: arrayOf(shape()),
|
||||
onClose: func.isRequired,
|
||||
onDeleteAllClick: func.isRequired,
|
||||
onLegendToggle: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
onToolsToggle: func.isRequired,
|
||||
template: shape().isRequired,
|
||||
toolsShown: bool.isRequired,
|
||||
};
|
||||
|
||||
VisualizerToolbar.defaultProps = {
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerToolbar);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user