Merge pull request #4389 from mabashian/4165-add-edit-node-audit

Add/Edit workflow from audit items
This commit is contained in:
Michael Abashian
2016-12-12 15:14:21 -05:00
committed by GitHub
11 changed files with 417 additions and 425 deletions

View File

@@ -170,7 +170,7 @@ export default
ngClick: 'cancelNodeForm()', ngClick: 'cancelNodeForm()',
ngShow: '!canAddWorkflowJobTemplate' ngShow: '!canAddWorkflowJobTemplate'
}, },
save: { select: {
ngClick: 'saveNodeForm()', ngClick: 'saveNodeForm()',
ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate", ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate",
ngShow: 'canAddWorkflowJobTemplate' ngShow: 'canAddWorkflowJobTemplate'

View File

@@ -16,9 +16,10 @@ export default
.factory('WorkflowFormObject', ['i18n', function(i18n) { .factory('WorkflowFormObject', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Workflow'), addTitle: i18n._('New Workflow Job Template'),
editTitle: '{{ name }}', editTitle: '{{ name }}',
name: 'workflow_job_template', name: 'workflow_job_template',
breadcrumbName: i18n._('WORKFLOW'),
base: 'workflow', base: 'workflow',
basePath: 'workflow_job_templates', basePath: 'workflow_job_templates',
// the top-most node of generated state tree // the top-most node of generated state tree

View File

@@ -1687,6 +1687,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
button.label = i18n._('Save'); button.label = i18n._('Save');
button['class'] = 'Form-saveButton'; button['class'] = 'Form-saveButton';
} }
if (btn === 'select') {
button.label = i18n._('Select');
button['class'] = 'Form-saveButton';
}
if (btn === 'cancel') { if (btn === 'cancel') {
button.label = i18n._('Cancel'); button.label = i18n._('Cancel');
button['class'] = 'Form-cancelButton'; button['class'] = 'Form-cancelButton';

View File

@@ -17,7 +17,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button ng-click="cancelForm()" class="Lookup-cancel btn btn-default">Cancel</button> <button ng-click="cancelForm()" class="Lookup-cancel btn btn-default">Cancel</button>
<button ng-click="saveForm()" class="Lookup-save btn btn-primary">Save</button> <button ng-click="saveForm()" class="Lookup-save btn btn-primary" ng-bind="list.lookupConfirmText ? list.lookupConfirmText : 'Save'"></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -401,8 +401,10 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA
} }
}, },
resolve: { resolve: {
ListDefinition: ['InventoryList', function(list) { ListDefinition: ['InventoryList', function(InventoryList) {
// mutate the provided list definition here // mutate the provided list definition here
let list = _.cloneDeep(InventoryList);
list.lookupConfirmText = 'SELECT';
return list; return list;
}], }],
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
@@ -451,8 +453,9 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA
} }
}, },
resolve: { resolve: {
ListDefinition: ['CredentialList', function(list) { ListDefinition: ['CredentialList', function(CredentialList) {
// mutate the provided list definition here let list = _.cloneDeep(CredentialList);
list.lookupConfirmText = 'SELECT';
return list; return list;
}], }],
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',

View File

@@ -33,10 +33,6 @@
$scope.parseType = 'yaml'; $scope.parseType = 'yaml';
$scope.includeWorkflowMaker = false; $scope.includeWorkflowMaker = false;
$scope.editRequests = [];
$scope.associateRequests = [];
$scope.disassociateRequests = [];
function init() { function init() {
// Select2-ify the lables input // Select2-ify the lables input
@@ -111,33 +107,6 @@
}); });
}); });
// Get the workflow nodes
TemplatesService.getWorkflowJobTemplateNodes(id)
.then(function(data){
$scope.workflowTree = WorkflowService.buildTree({
workflowNodes: data.data.results
});
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
// this workflowTree object and removing the children object for some reason (?)
// This happens on occasion and I think is a race condition (?)
if(!$scope.workflowTree.data.children) {
$scope.workflowTree.data.children = [];
}
// In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable.
// It won't get included until this the tree has been built - I'm open to better ways of doing this.
$scope.includeWorkflowMaker = true;
}, function(error){
ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!',
msg: 'Failed to get workflow job template nodes. GET returned ' +
'status: ' + error.status
});
});
// Go out and GET the workflow job temlate data needed to populate the form // Go out and GET the workflow job temlate data needed to populate the form
TemplatesService.getWorkflowJobTemplate(id) TemplatesService.getWorkflowJobTemplate(id)
.then(function(data){ .then(function(data){
@@ -180,6 +149,35 @@
$scope.url = workflowJobTemplateData.url; $scope.url = workflowJobTemplateData.url;
$scope.survey_enabled = workflowJobTemplateData.survey_enabled; $scope.survey_enabled = workflowJobTemplateData.survey_enabled;
// Get the workflow nodes
TemplatesService.getWorkflowJobTemplateNodes(id)
.then(function(data){
$scope.workflowTree = WorkflowService.buildTree({
workflowNodes: data.data.results
});
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
// this workflowTree object and removing the children object for some reason (?)
// This happens on occasion and I think is a race condition (?)
if(!$scope.workflowTree.data.children) {
$scope.workflowTree.data.children = [];
}
$scope.workflowTree.workflow_job_template_obj = $scope.workflow_job_template_obj;
// In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable.
// It won't get included until this the tree has been built - I'm open to better ways of doing this.
$scope.includeWorkflowMaker = true;
}, function(error){
ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!',
msg: 'Failed to get workflow job template nodes. GET returned ' +
'status: ' + error.status
});
});
}, function(error){ }, function(error){
ProcessErrors($scope, error.data, error.status, form, { ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!', hdr: 'Error!',
@@ -189,160 +187,6 @@
}); });
} }
function recursiveNodeUpdates(params, completionCallback) {
// params.parentId
// params.node
let generatePostUrl = function(){
let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.workflow_job_template_obj.related.workflow_nodes;
if(params.parentId) {
if(params.node.edgeType === 'success') {
base += "/success_nodes";
}
else if(params.node.edgeType === 'failure') {
base += "/failure_nodes";
}
else if(params.node.edgeType === 'always') {
base += "/always_nodes";
}
}
return base;
};
let buildSendableNodeData = function() {
// Create the node
let sendableNodeData = {
unified_job_template: params.node.unifiedJobTemplate.id
};
// Check to see if the user has provided any prompt values that are different
// from the defaults in the job template
if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) {
if(params.node.unifiedJobTemplate.ask_credential_on_launch) {
sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null;
}
if(params.node.unifiedJobTemplate.ask_inventory_on_launch) {
sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null;
}
if(params.node.unifiedJobTemplate.ask_limit_on_launch) {
sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null;
}
if(params.node.unifiedJobTemplate.ask_job_type_on_launch) {
sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null;
}
if(params.node.unifiedJobTemplate.ask_tags_on_launch) {
sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null;
}
if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) {
sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null;
}
}
return sendableNodeData;
};
let continueRecursing = function(parentId) {
$scope.totalIteratedNodes++;
if($scope.totalIteratedNodes === $scope.workflowTree.data.totalNodes) {
// We're done recursing, lets move on
completionCallback();
}
else {
if(params.node.children && params.node.children.length > 0) {
_.forEach(params.node.children, function(child) {
if(child.edgeType === "success") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
else if(child.edgeType === "failure") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
else if(child.edgeType === "always") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
});
}
}
};
if(params.node.isNew) {
TemplatesService.addWorkflowNode({
url: generatePostUrl(),
data: buildSendableNodeData()
})
.then(function(data) {
continueRecursing(data.data.id);
}, function(error) {
ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!',
msg: 'Failed to add workflow node. ' +
'POST returned status: ' +
error.status
});
});
}
else {
if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
if(params.node.edited) {
$scope.editRequests.push({
id: params.node.nodeId,
data: buildSendableNodeData()
});
}
if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep
$scope.disassociateRequests.push({
parentId: params.node.originalParentId,
nodeId: params.node.nodeId,
edge: params.node.originalEdge
});
// Can only associate if we have a parent.
// If we don't have a parent then this is a root node
// and the act of disassociating will make it a root node
if(params.parentId) {
$scope.associateRequests.push({
parentId: params.parentId,
nodeId: params.node.nodeId,
edge: params.node.edgeType
});
}
}
else if(!params.node.originalParentId && params.parentId) {
// This used to be a root node but is now not a root node
$scope.associateRequests.push({
parentId: params.parentId,
nodeId: params.node.nodeId,
edge: params.node.edgeType
});
}
}
continueRecursing(params.node.nodeId);
}
}
$scope.openWorkflowMaker = function() { $scope.openWorkflowMaker = function() {
$state.go('.workflowMaker'); $state.go('.workflowMaker');
}; };
@@ -392,231 +236,97 @@
.filter("[data-label-is-present=true]") .filter("[data-label-is-present=true]")
.map((i, val) => ({name: $(val).text()})); .map((i, val) => ({name: $(val).text()}));
$scope.totalIteratedNodes = 0; TemplatesService.updateWorkflowJobTemplate({
id: id,
data: data
}).then(function(){
// TODO: this is the only way that I could figure out to get var orgDefer = $q.defer();
// these promise arrays to play nicely. I tried to just append var associationDefer = $q.defer();
// a single promise to deletePromises but it just wasn't working var associatedLabelsDefer = $q.defer();
let editWorkflowJobTemplate = [id].map(function(id) {
return TemplatesService.updateWorkflowJobTemplate({
id: id,
data: data
});
});
if($scope.workflowTree && $scope.workflowTree.data && $scope.workflowTree.data.children && $scope.workflowTree.data.children.length > 0) { var getNext = function(data, arr, resolve) {
let completionCallback = function() { Rest.setUrl(data.next);
Rest.get()
let disassociatePromises = $scope.disassociateRequests.map(function(request) { .success(function (data) {
return TemplatesService.disassociateWorkflowNode({ if (data.next) {
parentId: request.parentId, getNext(data, arr.concat(data.results), resolve);
nodeId: request.nodeId, } else {
edge: request.edge resolve.resolve(arr.concat(data.results));
}
}); });
});
let editNodePromises = $scope.editRequests.map(function(request) {
return TemplatesService.editWorkflowNode({
id: request.id,
data: request.data
});
});
$q.all(disassociatePromises.concat(editNodePromises).concat(editWorkflowJobTemplate))
.then(function() {
let associatePromises = $scope.associateRequests.map(function(request) {
return TemplatesService.associateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
edge: request.edge
});
});
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(associatePromises.concat(deletePromises))
.then(function() {
var orgDefer = $q.defer();
var associationDefer = $q.defer();
var associatedLabelsDefer = $q.defer();
var getNext = function(data, arr, resolve) {
Rest.setUrl(data.next);
Rest.get()
.success(function (data) {
if (data.next) {
getNext(data, arr.concat(data.results), resolve);
} else {
resolve.resolve(arr.concat(data.results));
}
});
};
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
Rest.get()
.success(function(data) {
if (data.next) {
getNext(data, data.results, associatedLabelsDefer);
} else {
associatedLabelsDefer.resolve(data.results);
}
});
associatedLabelsDefer.promise.then(function (current) {
current = current.map(data => data.id);
var labelsToAdd = $scope.labels
.map(val => val.value);
var labelsToDisassociate = current
.filter(val => labelsToAdd
.indexOf(val) === -1)
.map(val => ({id: val, disassociate: true}));
var labelsToAssociate = labelsToAdd
.filter(val => current
.indexOf(val) === -1)
.map(val => ({id: val, associate: true}));
var pass = labelsToDisassociate
.concat(labelsToAssociate);
associationDefer.resolve(pass);
});
Rest.setUrl(GetBasePath("organizations"));
Rest.get()
.success(function(data) {
orgDefer.resolve(data.results[0].id);
});
orgDefer.promise.then(function(orgId) {
var toPost = [];
$scope.newLabels = $scope.newLabels
.map(function(i, val) {
val.organization = orgId;
return val;
});
$scope.newLabels.each(function(i, val) {
toPost.push(val);
});
associationDefer.promise.then(function(arr) {
toPost = toPost
.concat(arr);
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
var defers = [];
for (var i = 0; i < toPost.length; i++) {
defers.push(Rest.post(toPost[i]));
}
$q.all(defers)
.then(function() {
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
});
});
});
});
});
}; };
_.forEach($scope.workflowTree.data.children, function(child) { Rest.setUrl($scope.workflow_job_template_obj.related.labels);
recursiveNodeUpdates({
node: child
}, completionCallback);
});
}
else {
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) { Rest.get()
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); .success(function(data) {
}); if (data.next) {
getNext(data, data.results, associatedLabelsDefer);
$q.all(deletePromises.concat(editWorkflowJobTemplate)) } else {
.then(function() { associatedLabelsDefer.resolve(data.results);
var orgDefer = $q.defer(); }
var associationDefer = $q.defer();
var associatedLabelsDefer = $q.defer();
var getNext = function(data, arr, resolve) {
Rest.setUrl(data.next);
Rest.get()
.success(function (data) {
if (data.next) {
getNext(data, arr.concat(data.results), resolve);
} else {
resolve.resolve(arr.concat(data.results));
}
});
};
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
Rest.get()
.success(function(data) {
if (data.next) {
getNext(data, data.results, associatedLabelsDefer);
} else {
associatedLabelsDefer.resolve(data.results);
}
});
associatedLabelsDefer.promise.then(function (current) {
current = current.map(data => data.id);
var labelsToAdd = $scope.labels
.map(val => val.value);
var labelsToDisassociate = current
.filter(val => labelsToAdd
.indexOf(val) === -1)
.map(val => ({id: val, disassociate: true}));
var labelsToAssociate = labelsToAdd
.filter(val => current
.indexOf(val) === -1)
.map(val => ({id: val, associate: true}));
var pass = labelsToDisassociate
.concat(labelsToAssociate);
associationDefer.resolve(pass);
}); });
Rest.setUrl(GetBasePath("organizations")); associatedLabelsDefer.promise.then(function (current) {
Rest.get() current = current.map(data => data.id);
.success(function(data) { var labelsToAdd = $scope.labels
orgDefer.resolve(data.results[0].id); .map(val => val.value);
var labelsToDisassociate = current
.filter(val => labelsToAdd
.indexOf(val) === -1)
.map(val => ({id: val, disassociate: true}));
var labelsToAssociate = labelsToAdd
.filter(val => current
.indexOf(val) === -1)
.map(val => ({id: val, associate: true}));
var pass = labelsToDisassociate
.concat(labelsToAssociate);
associationDefer.resolve(pass);
});
Rest.setUrl(GetBasePath("organizations"));
Rest.get()
.success(function(data) {
orgDefer.resolve(data.results[0].id);
});
orgDefer.promise.then(function(orgId) {
var toPost = [];
$scope.newLabels = $scope.newLabels
.map(function(i, val) {
val.organization = orgId;
return val;
}); });
orgDefer.promise.then(function(orgId) { $scope.newLabels.each(function(i, val) {
var toPost = []; toPost.push(val);
$scope.newLabels = $scope.newLabels });
.map(function(i, val) {
val.organization = orgId; associationDefer.promise.then(function(arr) {
return val; toPost = toPost
.concat(arr);
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
var defers = [];
for (var i = 0; i < toPost.length; i++) {
defers.push(Rest.post(toPost[i]));
}
$q.all(defers)
.then(function() {
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
}); });
$scope.newLabels.each(function(i, val) {
toPost.push(val);
});
associationDefer.promise.then(function(arr) {
toPost = toPost
.concat(arr);
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
var defers = [];
for (var i = 0; i < toPost.length; i++) {
defers.push(Rest.post(toPost[i]));
}
$q.all(defers)
.then(function() {
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
});
});
}); });
}); });
}
}, function(error){
ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!',
msg: 'Failed to update workflow job template. PUT returned ' +
'status: ' + error.status
});
});
} catch (err) { } catch (err) {
Wait('stop'); Wait('stop');

View File

@@ -29,6 +29,11 @@
fill: @default-interface-txt; fill: @default-interface-txt;
} }
.WorkflowChart-startText {
fill: @default-bg;
cursor: default;
}
.node .rect { .node .rect {
fill: @default-secondary-bg; fill: @default-secondary-bg;
} }
@@ -97,3 +102,6 @@
width: 90px; width: 90px;
color: @default-interface-txt; color: @default-interface-txt;
} }
.WorkflowChart-activeNode {
fill: @default-link;
}

View File

@@ -84,6 +84,25 @@ export default [ '$state',
} }
} }
function rounded_rect(x, y, w, h, r, tl, tr, bl, br) {
var retval;
retval = "M" + (x + r) + "," + y;
retval += "h" + (w - 2*r);
if (tr) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r; }
else { retval += "h" + r; retval += "v" + r; }
retval += "v" + (h - 2*r);
if (br) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r; }
else { retval += "v" + r; retval += "h" + -r; }
retval += "h" + (2*r - w);
if (bl) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r; }
else { retval += "h" + -r; retval += "v" + -r; }
retval += "v" + (2*r - h);
if (tl) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r; }
else { retval += "v" + -r; retval += "h" + r; }
retval += "z";
return retval;
}
// This is the zoom function called by using the mousewheel/click and drag // This is the zoom function called by using the mousewheel/click and drag
function naturalZoom() { function naturalZoom() {
let scale = d3.event.scale, let scale = d3.event.scale,
@@ -163,20 +182,13 @@ export default [ '$state',
.attr("fill", "#5cb85c") .attr("fill", "#5cb85c")
.attr("class", "WorkflowChart-rootNode") .attr("class", "WorkflowChart-rootNode")
.call(add_node); .call(add_node);
thisNode.append("path")
.style("fill", "white")
.attr("transform", function() { return "translate(" + 30 + "," + 30 + ")"; })
.attr("d", d3.svg.symbol()
.size(120)
.type("cross")
)
.call(add_node);
thisNode.append("text") thisNode.append("text")
.attr("x", 14) .attr("x", 13)
.attr("y", 0) .attr("y", 30)
.attr("dy", ".35em") .attr("dy", ".35em")
.attr("class", "WorkflowChart-defaultText") .attr("class", "WorkflowChart-startText")
.text(function () { return "START"; }); .text(function () { return "START"; })
.call(add_node);
} }
else { else {
thisNode.append("rect") thisNode.append("rect")
@@ -184,12 +196,32 @@ export default [ '$state',
.attr("height", rectH) .attr("height", rectH)
.attr("rx", 5) .attr("rx", 5)
.attr("ry", 5) .attr("ry", 5)
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) .attr('stroke', function(d) {
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
}
}
else {
return "#D7D7D7";
}
})
.attr('stroke-width', "2px")
.attr("class", function(d) { .attr("class", function(d) {
return d.placeholder ? "rect placeholder" : "rect"; return d.placeholder ? "rect placeholder" : "rect";
}); });
thisNode.append("path")
.attr("d", rounded_rect(1, 0, 5, rectH, 5, 1, 0, 1, 0))
.attr("class", "WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; });
thisNode.append("text") thisNode.append("text")
.attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; })
.attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; })
@@ -517,8 +549,22 @@ export default [ '$state',
.attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; }); .attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; });
t.selectAll(".rect") t.selectAll(".rect")
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) .attr('stroke', function(d) {
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
}
}
else {
return "#D7D7D7";
}
})
.attr("class", function(d) { .attr("class", function(d) {
return d.placeholder ? "rect placeholder" : "rect"; return d.placeholder ? "rect placeholder" : "rect";
}); });
@@ -601,6 +647,9 @@ export default [ '$state',
t.selectAll(".WorkflowChart-conflictText") t.selectAll(".WorkflowChart-conflictText")
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; });
} }
function add_node() { function add_node() {

View File

@@ -154,7 +154,7 @@
padding-left: 20px; padding-left: 20px;
} }
.WorkflowLegend-maker--right { .WorkflowLegend-maker--right {
flex: 0 0 182px; flex: 0 0 206px;
text-align: right; text-align: right;
padding-right: 20px; padding-right: 20px;
position: relative; position: relative;
@@ -226,7 +226,7 @@
} }
.WorkflowMaker-manualControls { .WorkflowMaker-manualControls {
position: absolute; position: absolute;
left: -122px; left: -86px;
height: 60px; height: 60px;
width: 293px; width: 293px;
background-color: @default-bg; background-color: @default-bg;

View File

@@ -35,6 +35,10 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
showTypeOptions: false showTypeOptions: false
}; };
$scope.editRequests = [];
$scope.associateRequests = [];
$scope.disassociateRequests = [];
function init() { function init() {
$scope.treeDataMaster = angular.copy($scope.treeData.data); $scope.treeDataMaster = angular.copy($scope.treeData.data);
$scope.showManualControls = false; $scope.showManualControls = false;
@@ -55,6 +59,160 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
$scope.workflowMakerFormConfig.activeTab = "jobs"; $scope.workflowMakerFormConfig.activeTab = "jobs";
} }
function recursiveNodeUpdates(params, completionCallback) {
// params.parentId
// params.node
let generatePostUrl = function(){
let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.treeData.workflow_job_template_obj.related.workflow_nodes;
if(params.parentId) {
if(params.node.edgeType === 'success') {
base += "/success_nodes";
}
else if(params.node.edgeType === 'failure') {
base += "/failure_nodes";
}
else if(params.node.edgeType === 'always') {
base += "/always_nodes";
}
}
return base;
};
let buildSendableNodeData = function() {
// Create the node
let sendableNodeData = {
unified_job_template: params.node.unifiedJobTemplate.id
};
// Check to see if the user has provided any prompt values that are different
// from the defaults in the job template
if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) {
if(params.node.unifiedJobTemplate.ask_credential_on_launch) {
sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null;
}
if(params.node.unifiedJobTemplate.ask_inventory_on_launch) {
sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null;
}
if(params.node.unifiedJobTemplate.ask_limit_on_launch) {
sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null;
}
if(params.node.unifiedJobTemplate.ask_job_type_on_launch) {
sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null;
}
if(params.node.unifiedJobTemplate.ask_tags_on_launch) {
sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null;
}
if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) {
sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null;
}
}
return sendableNodeData;
};
let continueRecursing = function(parentId) {
$scope.totalIteratedNodes++;
if($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) {
// We're done recursing, lets move on
completionCallback();
}
else {
if(params.node.children && params.node.children.length > 0) {
_.forEach(params.node.children, function(child) {
if(child.edgeType === "success") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
else if(child.edgeType === "failure") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
else if(child.edgeType === "always") {
recursiveNodeUpdates({
parentId: parentId,
node: child
}, completionCallback);
}
});
}
}
};
if(params.node.isNew) {
TemplatesService.addWorkflowNode({
url: generatePostUrl(),
data: buildSendableNodeData()
})
.then(function(data) {
continueRecursing(data.data.id);
}, function(error) {
ProcessErrors($scope, error.data, error.status, form, {
hdr: 'Error!',
msg: 'Failed to add workflow node. ' +
'POST returned status: ' +
error.status
});
});
}
else {
if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
if(params.node.edited) {
$scope.editRequests.push({
id: params.node.nodeId,
data: buildSendableNodeData()
});
}
if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep
$scope.disassociateRequests.push({
parentId: params.node.originalParentId,
nodeId: params.node.nodeId,
edge: params.node.originalEdge
});
// Can only associate if we have a parent.
// If we don't have a parent then this is a root node
// and the act of disassociating will make it a root node
if(params.parentId) {
$scope.associateRequests.push({
parentId: params.parentId,
nodeId: params.node.nodeId,
edge: params.node.edgeType
});
}
}
else if(!params.node.originalParentId && params.parentId) {
// This used to be a root node but is now not a root node
$scope.associateRequests.push({
parentId: params.parentId,
nodeId: params.node.nodeId,
edge: params.node.edgeType
});
}
}
continueRecursing(params.node.nodeId);
}
}
$scope.lookUpInventory = function(){ $scope.lookUpInventory = function(){
$state.go('.inventory'); $state.go('.inventory');
}; };
@@ -70,7 +228,66 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
}; };
$scope.saveWorkflowMaker = function() { $scope.saveWorkflowMaker = function() {
$scope.closeDialog();
$scope.totalIteratedNodes = 0;
if($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) {
let completionCallback = function() {
let disassociatePromises = $scope.disassociateRequests.map(function(request) {
return TemplatesService.disassociateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
edge: request.edge
});
});
let editNodePromises = $scope.editRequests.map(function(request) {
return TemplatesService.editWorkflowNode({
id: request.id,
data: request.data
});
});
$q.all(disassociatePromises.concat(editNodePromises))
.then(function() {
let associatePromises = $scope.associateRequests.map(function(request) {
return TemplatesService.associateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
edge: request.edge
});
});
let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(associatePromises.concat(deletePromises))
.then(function() {
$scope.closeDialog();
});
});
};
_.forEach($scope.treeData.data.children, function(child) {
recursiveNodeUpdates({
node: child
}, completionCallback);
});
}
else {
let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(deletePromises)
.then(function() {
$scope.closeDialog();
});
}
}; };
/* ADD NODE FUNCTIONS */ /* ADD NODE FUNCTIONS */
@@ -575,7 +792,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
edgeFlags: $scope.edgeFlags edgeFlags: $scope.edgeFlags
}); });
} }
$scope.toggleManualControls = function() { $scope.toggleManualControls = function() {
$scope.showManualControls = !$scope.showManualControls; $scope.showManualControls = !$scope.showManualControls;
}; };

View File

@@ -47,10 +47,10 @@ describe('Controller: WorkflowMaker', () => {
})); }));
describe('scope.saveWorkflowMaker()', () => { describe('scope.closeWorkflowMaker()', () => {
it('should close the dialog', ()=>{ it('should close the dialog', ()=>{
scope.saveWorkflowMaker(); scope.closeWorkflowMaker();
expect(scope.closeDialog).toHaveBeenCalled(); expect(scope.closeDialog).toHaveBeenCalled();
}); });