mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Merge pull request #4295 from mabashian/3964-edge-conflict-v2
Implement edge conflicts when editing the workflow graph
This commit is contained in:
@@ -33,27 +33,27 @@ export default
|
|||||||
edgeType: {
|
edgeType: {
|
||||||
label: i18n._('Type'),
|
label: i18n._('Type'),
|
||||||
type: 'radio_group',
|
type: 'radio_group',
|
||||||
ngShow: 'selectedTemplate && showTypeOptions',
|
ngShow: 'selectedTemplate && edgeFlags.showTypeOptions',
|
||||||
ngDisabled: '!canAddWorkflowJobTemplate',
|
ngDisabled: '!canAddWorkflowJobTemplate',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: i18n._('On Success'),
|
label: i18n._('On Success'),
|
||||||
value: 'success',
|
value: 'success',
|
||||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"'
|
ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "successFailure"'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n._('On Failure'),
|
label: i18n._('On Failure'),
|
||||||
value: 'failure',
|
value: 'failure',
|
||||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"'
|
ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "successFailure"'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n._('Always'),
|
label: i18n._('Always'),
|
||||||
value: 'always',
|
value: 'always',
|
||||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "always"'
|
ngShow: '!edgeFlags.typeRestriction || edgeFlags.typeRestriction === "always"'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
awRequiredWhen: {
|
awRequiredWhen: {
|
||||||
reqExpression: 'showTypeOptions'
|
reqExpression: 'edgeFlags.showTypeOptions'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
credential: {
|
credential: {
|
||||||
|
|||||||
@@ -90,3 +90,10 @@
|
|||||||
width: 90px;
|
width: 90px;
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
}
|
}
|
||||||
|
.WorkflowChart-conflictIcon {
|
||||||
|
color: @default-err;
|
||||||
|
}
|
||||||
|
.WorkflowChart-conflictText {
|
||||||
|
width: 90px;
|
||||||
|
color: @default-interface-txt;
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,8 +119,7 @@ export default [ '$state',
|
|||||||
.attr("class", "node")
|
.attr("class", "node")
|
||||||
.attr("id", function(d){return "node-" + d.id;})
|
.attr("id", function(d){return "node-" + d.id;})
|
||||||
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
|
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
|
||||||
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
|
||||||
.attr("fill", "red");
|
|
||||||
|
|
||||||
nodeEnter.each(function(d) {
|
nodeEnter.each(function(d) {
|
||||||
let thisNode = d3.select(this);
|
let thisNode = d3.select(this);
|
||||||
@@ -171,6 +170,16 @@ export default [ '$state',
|
|||||||
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
||||||
}).each(wrap);
|
}).each(wrap);
|
||||||
|
|
||||||
|
thisNode.append("foreignObject")
|
||||||
|
.attr("x", 43)
|
||||||
|
.attr("y", 45)
|
||||||
|
.style("font-size","0.7em")
|
||||||
|
.attr("class", "WorkflowChart-conflictText")
|
||||||
|
.html(function () {
|
||||||
|
return "<span class=\"WorkflowChart-conflictIcon\">\uf06a</span><span> EDGE CONFLICT</span>";
|
||||||
|
})
|
||||||
|
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
|
||||||
|
|
||||||
thisNode.append("foreignObject")
|
thisNode.append("foreignObject")
|
||||||
.attr("x", 17)
|
.attr("x", 17)
|
||||||
.attr("y", 22)
|
.attr("y", 22)
|
||||||
@@ -347,7 +356,7 @@ export default [ '$state',
|
|||||||
|
|
||||||
let link = svgGroup.selectAll("g.link")
|
let link = svgGroup.selectAll("g.link")
|
||||||
.data(links, function(d) {
|
.data(links, function(d) {
|
||||||
return d.target.id;
|
return d.source.id + "-" + d.target.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
let linkEnter = link.enter().append("g")
|
let linkEnter = link.enter().append("g")
|
||||||
@@ -485,6 +494,7 @@ export default [ '$state',
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.selectAll(".node")
|
t.selectAll(".node")
|
||||||
|
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
|
||||||
.attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; });
|
.attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; });
|
||||||
|
|
||||||
t.selectAll(".WorkflowChart-nodeTypeCircle")
|
t.selectAll(".WorkflowChart-nodeTypeCircle")
|
||||||
@@ -558,6 +568,9 @@ export default [ '$state',
|
|||||||
t.selectAll(".WorkflowChart-incompleteText")
|
t.selectAll(".WorkflowChart-incompleteText")
|
||||||
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
|
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
|
||||||
|
|
||||||
|
t.selectAll(".WorkflowChart-conflictText")
|
||||||
|
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_node() {
|
function add_node() {
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
value: "check"
|
value: "check"
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
$scope.edgeFlags = {
|
||||||
|
conflict: false,
|
||||||
|
typeRestriction: null,
|
||||||
|
showTypeOptions: false
|
||||||
|
};
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
$scope.treeDataMaster = angular.copy($scope.treeData.data);
|
$scope.treeDataMaster = angular.copy($scope.treeData.data);
|
||||||
$scope.$broadcast("refreshWorkflowChart");
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
@@ -36,7 +42,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
|
|
||||||
function resetNodeForm() {
|
function resetNodeForm() {
|
||||||
$scope.workflowMakerFormConfig.nodeMode = "idle";
|
$scope.workflowMakerFormConfig.nodeMode = "idle";
|
||||||
$scope.showTypeOptions = false;
|
$scope.edgeFlags.showTypeOptions = false;
|
||||||
delete $scope.selectedTemplate;
|
delete $scope.selectedTemplate;
|
||||||
delete $scope.workflow_job_templates;
|
delete $scope.workflow_job_templates;
|
||||||
delete $scope.workflow_projects;
|
delete $scope.workflow_projects;
|
||||||
@@ -44,7 +50,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
delete $scope.placeholderNode;
|
delete $scope.placeholderNode;
|
||||||
delete $scope.betweenTwoNodes;
|
delete $scope.betweenTwoNodes;
|
||||||
$scope.nodeBeingEdited = null;
|
$scope.nodeBeingEdited = null;
|
||||||
$scope.edgeTypeRestriction = null;
|
$scope.edgeFlags.typeRestriction = null;
|
||||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +95,8 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
|
|
||||||
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
|
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
|
||||||
tree: $scope.treeData.data,
|
tree: $scope.treeData.data,
|
||||||
parentId: betweenTwoNodes ? parent.source.id : parent.id
|
parentId: betweenTwoNodes ? parent.source.id : parent.id,
|
||||||
|
childId: $scope.placeholderNode.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the default to success
|
// Set the default to success
|
||||||
@@ -99,21 +106,27 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
// We don't want to give the user the option to select
|
// We don't want to give the user the option to select
|
||||||
// a type as this node will always be executed
|
// a type as this node will always be executed
|
||||||
edgeType = "always";
|
edgeType = "always";
|
||||||
$scope.showTypeOptions = false;
|
$scope.edgeFlags.showTypeOptions = false;
|
||||||
} else {
|
} else {
|
||||||
if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) {
|
if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) {
|
||||||
// This is a problem...
|
// This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving
|
||||||
|
$scope.edgeFlags.typeRestriction = null;
|
||||||
} else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
|
} else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
|
||||||
$scope.edgeTypeRestriction = "successFailure";
|
$scope.edgeFlags.typeRestriction = "successFailure";
|
||||||
edgeType = "success";
|
edgeType = "success";
|
||||||
} else if (_.includes(siblingConnectionTypes, "always")) {
|
} else if (_.includes(siblingConnectionTypes, "always")) {
|
||||||
$scope.edgeTypeRestriction = "always";
|
$scope.edgeFlags.typeRestriction = "always";
|
||||||
edgeType = "always";
|
edgeType = "always";
|
||||||
|
} else {
|
||||||
|
$scope.edgeFlags.typeRestriction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.showTypeOptions = true;
|
$scope.edgeFlags.showTypeOptions = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the edgeConflict flag
|
||||||
|
resetEdgeConflict();
|
||||||
|
|
||||||
$scope.$broadcast("setEdgeType", edgeType);
|
$scope.$broadcast("setEdgeType", edgeType);
|
||||||
$scope.$broadcast("refreshWorkflowChart");
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
|
||||||
@@ -181,6 +194,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the edgeConflict flag
|
||||||
|
resetEdgeConflict();
|
||||||
|
|
||||||
$scope.$broadcast("refreshWorkflowChart");
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,6 +211,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
$scope.nodeBeingEdited.isActiveEdit = false;
|
$scope.nodeBeingEdited.isActiveEdit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the edgeConflict flag
|
||||||
|
resetEdgeConflict();
|
||||||
|
|
||||||
// Reset the form
|
// Reset the form
|
||||||
resetNodeForm();
|
resetNodeForm();
|
||||||
|
|
||||||
@@ -208,6 +227,12 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
|
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
|
||||||
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
|
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||||
$scope.cancelNodeForm();
|
$scope.cancelNodeForm();
|
||||||
|
|
||||||
|
// Refresh this object as the parent has changed
|
||||||
|
nodeToEdit = WorkflowService.searchTree({
|
||||||
|
element: $scope.treeData.data,
|
||||||
|
matchingId: nodeToEdit.id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.workflowMakerFormConfig.nodeMode = "edit";
|
$scope.workflowMakerFormConfig.nodeMode = "edit";
|
||||||
@@ -330,7 +355,30 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.showTypeOptions = (parent && parent.isStartNode) ? false : true;
|
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
|
||||||
|
tree: $scope.treeData.data,
|
||||||
|
parentId: parent.id,
|
||||||
|
childId: nodeToEdit.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parent && parent.isStartNode) {
|
||||||
|
// We don't want to give the user the option to select
|
||||||
|
// a type as this node will always be executed
|
||||||
|
$scope.edgeFlags.showTypeOptions = false;
|
||||||
|
} else {
|
||||||
|
if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) {
|
||||||
|
// This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving
|
||||||
|
$scope.edgeFlags.typeRestriction = null;
|
||||||
|
} else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure") && (nodeToEdit.edgeType === "success" || nodeToEdit.edgeType === "failure")) {
|
||||||
|
$scope.edgeFlags.typeRestriction = "successFailure";
|
||||||
|
} else if (_.includes(siblingConnectionTypes, "always") && nodeToEdit.edgeType === "always") {
|
||||||
|
$scope.edgeFlags.typeRestriction = "always";
|
||||||
|
} else {
|
||||||
|
$scope.edgeFlags.typeRestriction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.edgeFlags.showTypeOptions = true;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.$broadcast('setEdgeType', $scope.nodeBeingEdited.edgeType);
|
$scope.$broadcast('setEdgeType', $scope.nodeBeingEdited.edgeType);
|
||||||
|
|
||||||
@@ -441,6 +489,9 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
resetNodeForm();
|
resetNodeForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the edgeConflict flag
|
||||||
|
resetEdgeConflict();
|
||||||
|
|
||||||
resetDeleteNode();
|
resetDeleteNode();
|
||||||
|
|
||||||
$scope.$broadcast("refreshWorkflowChart");
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
@@ -515,6 +566,15 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resetEdgeConflict(){
|
||||||
|
$scope.edgeFlags.conflict = false;
|
||||||
|
|
||||||
|
WorkflowService.checkForEdgeConflicts({
|
||||||
|
treeData: $scope.treeData.data,
|
||||||
|
edgeFlags: $scope.edgeFlags
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="WorkflowMaker-buttonHolder">
|
<div class="WorkflowMaker-buttonHolder">
|
||||||
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> Close</button>
|
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> Close</button>
|
||||||
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate"> Save</button>
|
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="edgeFlags.conflict"> Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export default [function(){
|
|||||||
child.edgeType = "always";
|
child.edgeType = "always";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.parent = parentNode;
|
||||||
|
|
||||||
parentNode.children.push(child);
|
parentNode.children.push(child);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -81,9 +83,10 @@ export default [function(){
|
|||||||
if(params.betweenTwoNodes) {
|
if(params.betweenTwoNodes) {
|
||||||
_.forEach(parentNode.children, function(child, index) {
|
_.forEach(parentNode.children, function(child, index) {
|
||||||
if(child.id === params.parent.target.id) {
|
if(child.id === params.parent.target.id) {
|
||||||
placeholder.children.push(angular.copy(child));
|
placeholder.children.push(child);
|
||||||
parentNode.children[index] = placeholder;
|
parentNode.children[index] = placeholder;
|
||||||
placeholderRef = parentNode.children[index];
|
placeholderRef = parentNode.children[index];
|
||||||
|
child.parent = parentNode.children[index];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -102,6 +105,7 @@ export default [function(){
|
|||||||
},
|
},
|
||||||
getSiblingConnectionTypes: function(params) {
|
getSiblingConnectionTypes: function(params) {
|
||||||
// params.parentId
|
// params.parentId
|
||||||
|
// params.childId
|
||||||
// params.tree
|
// params.tree
|
||||||
|
|
||||||
let siblingConnectionTypes = {};
|
let siblingConnectionTypes = {};
|
||||||
@@ -114,7 +118,7 @@ export default [function(){
|
|||||||
if(parentNode.children && parentNode.children.length > 0) {
|
if(parentNode.children && parentNode.children.length > 0) {
|
||||||
// Loop across them and add the types as keys to siblingConnectionTypes
|
// Loop across them and add the types as keys to siblingConnectionTypes
|
||||||
_.forEach(parentNode.children, function(child) {
|
_.forEach(parentNode.children, function(child) {
|
||||||
if(!child.placeholder && child.edgeType) {
|
if(child.id !== params.childId && !child.placeholder && child.edgeType) {
|
||||||
siblingConnectionTypes[child.edgeType] = true;
|
siblingConnectionTypes[child.edgeType] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -283,6 +287,40 @@ export default [function(){
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
checkForEdgeConflicts: function(params) {
|
||||||
|
//params.treeData
|
||||||
|
//params.edgeFlags
|
||||||
|
|
||||||
|
let hasAlways = false;
|
||||||
|
let hasSuccessFailure = false;
|
||||||
|
let _this = this;
|
||||||
|
|
||||||
|
_.forEach(params.treeData.children, function(child) {
|
||||||
|
// Flip the flag to false for now - we'll set it to true later on
|
||||||
|
// if we detect a conflict
|
||||||
|
child.edgeConflict = false;
|
||||||
|
if(child.edgeType === 'always') {
|
||||||
|
hasAlways = true;
|
||||||
|
}
|
||||||
|
else if(child.edgeType === 'success' || child.edgeType === 'failure') {
|
||||||
|
hasSuccessFailure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.checkForEdgeConflicts({
|
||||||
|
treeData: child,
|
||||||
|
edgeFlags: params.edgeFlags
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if(hasAlways && hasSuccessFailure) {
|
||||||
|
// We have a conflict
|
||||||
|
_.forEach(params.treeData.children, function(child) {
|
||||||
|
child.edgeConflict = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
params.edgeFlags.conflict = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}];
|
}];
|
||||||
|
|||||||
Reference in New Issue
Block a user