1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Completed work necessary to support editing workflow links and nodes separately. Added hover and tooltip to links

This commit is contained in:
mabashian 2018-10-24 15:32:28 -04:00
parent 87d6253176
commit 29b4979736
12 changed files with 1174 additions and 1059 deletions

View File

@ -111,6 +111,8 @@ function TemplatesStrings (BaseString) {
JOBS: t.s('JOBS'),
PLEASE_CLICK_THE_START_BUTTON: t.s('Please click the start button to build your workflow.'),
PLEASE_HOVER_OVER_A_TEMPLATE: t.s('Please hover over a template for additional options.'),
EDIT_LINK_TOOLTIP: t.s('Click to edit link'),
VIEW_LINK_TOOLTIP: t.s('Click to view link'),
RUN: t.s('RUN'),
CHECK: t.s('CHECK'),
SELECT: t.s('SELECT'),
@ -122,7 +124,8 @@ function TemplatesStrings (BaseString) {
INVENTORY_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden by the parent workflow inventory.'),
INVENTORY_PROMPT_WILL_OVERRIDE: t.s('The inventory of this node will be overridden if a parent workflow inventory is provided at launch.'),
INVENTORY_PROMPT_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden if a parent workflow inventory is provided at launch.'),
EDIT_LINK: ({ parentName, childName }) => t.s('EDIT LINK | {{parentName}} to {{childName}}', { parentName, childName })
EDIT_LINK: ({ parentName, childName }) => t.s('EDIT LINK | {{parentName}} to {{childName}}', { parentName, childName }),
VIEW_LINK: ({ parentName, childName }) => t.s('VIEW LINK | {{parentName}} to {{childName}}', { parentName, childName })
}
}

View File

@ -1,43 +1,88 @@
.link circle,
.link polygon,
.link .linkCross,
.node circle,
.node .linkIcon,
.node .WorkflowChart-hoverPath {
.WorkflowChart-node {
font-size: 12px;
font-family: 'Open Sans', sans-serif, 'FontAwesome';
}
.WorkflowChart-link {
fill: none;
stroke-width: 2px;
}
.WorkflowChart-linkOverlay {
fill: @default-interface-txt;
}
.WorkflowChart-link--active.WorkflowChart-linkOverlay,
.WorkflowChart-linkHovering .WorkflowChart-linkOverlay {
cursor: pointer;
opacity: 1;
fill: @cgrey;
}
.WorkflowChart-linkHovering .WorkflowChart-linkPath {
cursor: pointer;
}
.WorkflowChart-link circle,
.WorkflowChart-link polygon,
.WorkflowChart-link .WorkflowChart-betweenNodesIcon,
.WorkflowChart-node .WorkflowChart-nodeAddCircle,
.WorkflowChart-node .WorkflowChart-nodeRemoveCircle,
.WorkflowChart-node .WorkflowChart-nodeAddIcon,
.WorkflowChart-node .WorkflowChart-nodeRemoveIcon {
opacity: 0;
}
.node .addCircle, .link .addCircle {
.WorkflowChart-node .WorkflowChart-addCircle, .WorkflowChart-link .WorkflowChart-addCircle {
fill: @default-succ;
}
.addCircle.addHovering {
.WorkflowChart-addCircle.WorkflowChart-addHovering {
fill: @default-succ-hov;
}
.node .removeCircle {
.WorkflowChart-node .WorkflowChart-nodeRemoveCircle {
fill: @default-err;
}
.removeCircle.removeHovering {
.WorkflowChart-nodeRemoveCircle.removeHovering {
fill: @default-err-hov;
}
.node .linkCircle {
fill: @default-link;
.WorkflowChart-node .WorkflowChart-rect {
fill: @default-secondary-bg;
}
.node .linkIcon {
color: @default-bg;
.WorkflowChart-rect.WorkflowChart-placeholder {
stroke-dasharray: 3;
}
.linkCircle.removeHovering {
fill: @default-link-hov;
.WorkflowChart-node .WorkflowChart-transparentRect {
fill: @default-bg;
opacity: 0;
}
.node {
font-size: 12px;
font-family: 'Open Sans', sans-serif, 'FontAwesome';
.WorkflowChart-alwaysShowAdd circle,
.WorkflowChart-alwaysShowAdd path,
.WorkflowChart-alwaysShowAdd .WorkflowChart-betweenNodesIcon,
.WorkflowChart-nodeHovering .WorkflowChart-nodeAddCircle,
.WorkflowChart-nodeHovering .WorkflowChart-nodeAddIcon,
.WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveCircle,
.WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveIcon,
.WorkflowChart-addHovering circle,
.WorkflowChart-addHovering path,
.WorkflowChart-addHovering .WorkflowChart-betweenNodesIcon {
cursor: pointer;
opacity: 1;
}
.WorkflowChart-link.WorkflowChart-placeholder {
stroke-dasharray: 3;
}
.WorkflowChart-svg {
border-bottom-left-radius: 5px;
width: 100%;
}
.WorkflowChart-defaultText {
@ -49,76 +94,36 @@
cursor: default;
}
.node .rect {
fill: @default-secondary-bg;
}
.rect.placeholder {
stroke-dasharray: 3;
}
.node .transparentRect {
fill: @default-bg;
opacity: 0;
}
.WorkflowChart-alwaysShowAdd circle,
.WorkflowChart-alwaysShowAdd path,
.WorkflowChart-alwaysShowAdd .linkCross,
.hovering .addCircle,
.hovering .removeCircle,
.addHovering .betweenNodesCircle,
.hovering .linkCircle,
.hovering .linkIcon,
.hovering .WorkflowChart-hoverPath,
.addHovering .linkCross {
cursor: pointer;
opacity: 1;
}
.link {
fill: none;
stroke-width: 2px;
}
.link.placeholder {
stroke-dasharray: 3;
}
.WorkflowChart-svg {
border-bottom-left-radius: 5px;
width: 100%;
}
.WorkflowResults-rightSide .WorkflowChart-svg {
background-color: @f6grey;
border: 1px solid @d7grey;
border-top: 0px;
border-bottom-right-radius: 5px;
}
.WorkflowChart-nodeTypeCircle {
fill: @default-icon;
}
.WorkflowChart-nodeTypeLetter {
fill: @default-bg;
}
.workflowChart-nodeStatus--running {
.WorkflowChart-nodeStatus--running {
fill: @default-icon;
}
.workflowChart-nodeStatus--success {
.WorkflowChart-nodeStatus--success {
fill: @default-succ;
}
.workflowChart-nodeStatus--failed, .workflowChart-nodeStatus--canceled {
.WorkflowChart-nodeStatus--failed, .WorkflowChart-nodeStatus--canceled {
fill: @default-err;
}
.WorkflowChart-detailsLink {
fill: @default-link;
cursor: pointer;
font-size: 10px;
}
.WorkflowChart-incompleteIcon {
color: @default-warning;
}
.WorkflowChart-deletedText {
width: 90px;
color: @default-interface-txt;
@ -126,6 +131,7 @@
.WorkflowChart-activeNode {
fill: @default-link;
}
.WorkflowChart-elapsedHolder {
background-color: @b7grey;
color: @default-bg;
@ -134,40 +140,47 @@
padding: 1px 3px;
border-radius: 4px;
}
.WorkflowChart-nameText {
font-size: 10px;
}
.WorkflowChart-tooltip {
pointer-events: none;
text-align: center;
}
.WorkflowChart-tooltipContents {
padding: 10px;
background-color: #707070;
color: #FFFFFF;
background-color: @default-interface-txt;
color: @default-bg;
border-radius: 4px;
word-wrap: break-word;
max-width: 325px;
font-size: 10px;
}
.WorkflowChart-tooltipArrow {
.WorkflowChart-tooltipArrow--down {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #707070;
border-top: 10px solid @default-interface-txt;
margin: auto;
}
.WorkflowChart-tooltipArrow--right {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid @default-interface-txt;
margin: auto;
position: relative;
right: -55px;
top: -34px;
}
.WorkflowChart-dashedNode {
stroke-dasharray: 5,5;
}
.linkOverlay {
fill: @default-interface-txt;
}
.linkActiveEdit.linkOverlay,
.overlayHovering .linkOverlay {
cursor: pointer;
opacity: 0.4;
}
.overlayHovering .linkPath {
cursor: pointer;
}

View File

@ -239,14 +239,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
let nodes = tree.nodes(scope.treeData),
links = tree.links(nodes);
let node = svgGroup.selectAll("g.node")
let node = svgGroup.selectAll("g.WorkflowChart-node")
.data(nodes, function(d) {
d.y = d.depth * 240;
return d.id || (d.id = ++i);
});
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("class", "WorkflowChart-node")
.attr("id", function(d){return "node-" + d.id;})
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
@ -308,7 +308,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
})
.attr('stroke-width', "2px")
.attr("class", function(d) {
let classString = d.placeholder ? "rect placeholder" : "rect";
let classString = d.placeholder ? "WorkflowChart-rect WorkflowChart-placeholder" : "WorkflowChart-rect";
classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : "";
return classString;
});
@ -398,7 +398,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
thisNode.append("rect")
.attr("width", nodeW)
.attr("height", nodeH)
.attr("class", "transparentRect")
.attr("class", "WorkflowChart-transparentRect")
.call(edit_node)
.on("mouseover", function(d) {
if(!d.isStartNode) {
@ -409,13 +409,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
// As such, we need to move the nodes after the links so that when the tooltip renders it shows up on top
// of the links and not underneath them. I tried rendering the links before the nodes but that lead to
// some weird link animation that I didn't care to try to fix.
svgGroup.selectAll("g.node").each(function() {
svgGroup.selectAll("g.WorkflowChart-node").each(function() {
this.parentNode.appendChild(this);
});
// After the nodes have been properly placed after the links, we need to make sure that the node that
// the user is hovering over is at the very end of the list. This way the tooltip will appear on top
// of all other nodes.
svgGroup.selectAll("g.node").sort(function (a) {
svgGroup.selectAll("g.WorkflowChart-node").sort(function (a) {
return (a.id !== d.id) ? -1 : 1;
});
// Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place
@ -437,14 +437,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
});
}
d3.select("#node-" + d.id)
.classed("hovering", true);
.classed("WorkflowChart-nodeHovering", true);
}
})
.on("mouseout", function(d){
$('.WorkflowChart-tooltip').remove();
if(!d.isStartNode) {
d3.select("#node-" + d.id)
.classed("hovering", false);
.classed("WorkflowChart-nodeHovering", false);
}
});
thisNode.append("text")
@ -461,23 +461,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.attr("id", function(d){return "node-" + d.id + "-add";})
.attr("cx", nodeW)
.attr("r", 10)
.attr("class", "addCircle nodeCircle")
.attr("class", "WorkflowChart-addCircle WorkflowChart-nodeAddCircle")
.style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
.call(add_node)
.on("mouseover", function(d) {
d3.select("#node-" + d.id)
.classed("hovering", true);
.classed("WorkflowChart-nodeHovering", true);
d3.select("#node-" + d.id + "-add")
.classed("addHovering", true);
.classed("WorkflowChart-addHovering", true);
})
.on("mouseout", function(d){
d3.select("#node-" + d.id)
.classed("hovering", false);
.classed("WorkflowChart-nodeHovering", false);
d3.select("#node-" + d.id + "-add")
.classed("addHovering", false);
.classed("WorkflowChart-addHovering", false);
});
thisNode.append("path")
.attr("class", "nodeAddCross WorkflowChart-hoverPath")
.attr("class", "WorkflowChart-nodeAddIcon")
.style("fill", "white")
.attr("transform", function() { return "translate(" + nodeW + "," + 0 + ")"; })
.attr("d", d3.svg.symbol()
@ -488,38 +488,38 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.call(add_node)
.on("mouseover", function(d) {
d3.select("#node-" + d.id)
.classed("hovering", true);
.classed("WorkflowChart-nodeHovering", true);
d3.select("#node-" + d.id + "-add")
.classed("addHovering", true);
.classed("WorkflowChart-addHovering", true);
})
.on("mouseout", function(d){
d3.select("#node-" + d.id)
.classed("hovering", false);
.classed("WorkflowChart-nodeHovering", false);
d3.select("#node-" + d.id + "-add")
.classed("addHovering", false);
.classed("WorkflowChart-addHovering", false);
});
thisNode.append("circle")
.attr("id", function(d){return "node-" + d.id + "-remove";})
.attr("cx", nodeW)
.attr("cy", nodeH)
.attr("r", 10)
.attr("class", "removeCircle")
.attr("class", "WorkflowChart-nodeRemoveCircle")
.style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; })
.call(remove_node)
.on("mouseover", function(d) {
d3.select("#node-" + d.id)
.classed("hovering", true);
.classed("WorkflowChart-nodeHovering", true);
d3.select("#node-" + d.id + "-remove")
.classed("removeHovering", true);
})
.on("mouseout", function(d){
d3.select("#node-" + d.id)
.classed("hovering", false);
.classed("WorkflowChart-nodeHovering", false);
d3.select("#node-" + d.id + "-remove")
.classed("removeHovering", false);
});
thisNode.append("path")
.attr("class", "nodeRemoveCross WorkflowChart-hoverPath")
.attr("class", "WorkflowChart-nodeRemoveIcon")
.style("fill", "white")
.attr("transform", function() { return "translate(" + nodeW + "," + nodeH + ") rotate(-45)"; })
.attr("d", d3.svg.symbol()
@ -530,60 +530,16 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.call(remove_node)
.on("mouseover", function(d) {
d3.select("#node-" + d.id)
.classed("hovering", true);
.classed("WorkflowChart-nodeHovering", true);
d3.select("#node-" + d.id + "-remove")
.classed("removeHovering", true);
})
.on("mouseout", function(d){
d3.select("#node-" + d.id)
.classed("hovering", false);
.classed("WorkflowChart-nodeHovering", false);
d3.select("#node-" + d.id + "-remove")
.classed("removeHovering", false);
});
// thisNode.append("circle")
// .attr("id", function(d){return "node-" + d.id + "-link";})
// .attr("cx", nodeW)
// .attr("cy", nodeH/2)
// .attr("r", 10)
// .attr("class", "linkCircle nodeCircle")
// .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
// .call(link_node)
// .on("mouseover", function(d) {
// d3.select("#node-" + d.id)
// .classed("hovering", true);
// d3.select("#node-" + d.id + "-link")
// .classed("addHovering", true);
// })
// .on("mouseout", function(d){
// d3.select("#node-" + d.id)
// .classed("hovering", false);
// d3.select("#node-" + d.id + "-link")
// .classed("addHovering", false);
// });
// // TODO: clean up the placement of this icon... this works but it's not
// // clean
// thisNode.append("foreignObject")
// .attr("x", nodeW - 6)
// .attr("y", nodeH/2 - 9)
// .style("font-size","14px")
// .html(function () {
// return `<span class="fa fa-link" />`;
// })
// .attr("class", "linkIcon")
// .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
// .call(link_node)
// .on("mouseover", function(d) {
// d3.select("#node-" + d.id)
// .classed("hovering", true);
// d3.select("#node-" + d.id + "-link")
// .classed("addHovering", true);
// })
// .on("mouseout", function(d){
// d3.select("#node-" + d.id)
// .classed("hovering", false);
// d3.select("#node-" + d.id + "-link")
// .classed("addHovering", false);
// });
thisNode.append("circle")
.attr("class", function(d) {
@ -593,25 +549,25 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
if(d.job){
switch(d.job.status) {
case "pending":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "waiting":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "running":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "successful":
statusClass += "workflowChart-nodeStatus--success";
statusClass += "WorkflowChart-nodeStatus--success";
break;
case "failed":
statusClass += "workflowChart-nodeStatus--failed";
statusClass += "WorkflowChart-nodeStatus--failed";
break;
case "error":
statusClass += "workflowChart-nodeStatus--failed";
statusClass += "WorkflowChart-nodeStatus--failed";
break;
case "canceled":
statusClass += "workflowChart-nodeStatus--canceled";
statusClass += "WorkflowChart-nodeStatus--canceled";
break;
}
}
@ -652,63 +608,149 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
graphLoaded = true;
let link = svgGroup.selectAll("g.link")
let link = svgGroup.selectAll("g.WorkflowChart-link")
.data(links, function(d) {
return d.source.id + "-" + d.target.id;
});
let linkEnter = link.enter().append("g")
.attr("class", "link")
.attr("class", "WorkflowChart-link")
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
linkEnter.append("polygon", "g")
.attr("class", function(d) {
let linkClasses = ["linkOverlay"];
let linkClasses = ["WorkflowChart-linkOverlay"];
if (d.source.isLinkEditParent && d.target.isLinkEditChild) {
linkClasses.push("linkActiveEdit");
linkClasses.push("WorkflowChart-link--active");
}
return linkClasses.join(' ');
})
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
.attr("points",function(d) {
const pt1 = [d.source.y + nodeW, d.source.x + 10 + nodeH/2].join(",");
const pt2 = [d.target.y,d.target.x + 10 + nodeH/2].join(",");
const pt3 = [d.target.y,d.target.x - 10 + nodeH/2].join(",");
const pt4 = [d.source.y + nodeW,d.source.x - 10 + nodeH/2].join(",");
let x1 = d.source.y + nodeW;
let y1 = d.source.x + nodeH / 2;
let x2 = d.target.y;
let y2 = d.target.x + nodeH / 2;
let slope = (y2 - y1)/(x2-x1);
let yIntercept = y1 - slope*x1;
let orthogonalDistance = 8;
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
return [pt1, pt2, pt3, pt4].join(" ");
})
.call(edit_link)
.on("mouseover", function(d) {
if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details') {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("overlayHovering", true);
.classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
arrowClass = 'WorkflowChart-tooltipArrow--down';
} else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
arrowClass = 'WorkflowChart-tooltipArrow--right';
}
let edgeTypeLabel;
switch(d.target.edgeType) {
case "always":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS');
break;
case "success":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS');
break;
case "failure":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE');
break;
}
let linkInstructionText = _.get(scope, 'workflowJobTemplateObj.summary_fields.user_capabilities.edit') ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP');
linkEnter.append("foreignObject")
.attr("x", xPos)
.attr("y", yPos)
.attr("width", 100)
.attr("height", 60)
.attr("class", "WorkflowChart-tooltip")
.html(function(){
return `<div class='WorkflowChart-tooltipContents'><div>${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}</div><div>${linkInstructionText}</div></div><div class='${arrowClass}'></div>`;
});
}
})
.on("mouseout", function(d){
if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("overlayHovering", false);
.classed("WorkflowChart-linkHovering", false);
}
$('.WorkflowChart-tooltip').remove();
});
// Add entering links in the parents old position.
linkEnter.append("path", "g")
.attr("class", function(d) {
return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath";
return (d.source.placeholder || d.target.placeholder) ? "WorkflowChart-linkPath WorkflowChart-placeholder" : "WorkflowChart-linkPath";
})
.attr("d", lineData)
.call(edit_link)
.on("mouseover", function(d) {
if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
.on("mouseenter", function(d) {
if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details') {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("overlayHovering", true);
.classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
arrowClass = 'WorkflowChart-tooltipArrow--down';
} else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
arrowClass = 'WorkflowChart-tooltipArrow--right';
}
let edgeTypeLabel;
switch(d.target.edgeType) {
case "always":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS');
break;
case "success":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS');
break;
case "failure":
edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE');
break;
}
let linkInstructionText = _.get(scope, 'workflowJobTemplateObj.summary_fields.user_capabilities.edit') ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP');
linkEnter.append("foreignObject")
.attr("x", xPos)
.attr("y", yPos)
.attr("width", 100)
.attr("height", 60)
.attr("class", "WorkflowChart-tooltip")
.html(function(){
return `<div class='WorkflowChart-tooltipContents'><div>${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}</div><div>${linkInstructionText}</div></div><div class='${arrowClass}'></div>`;
});
}
})
.on("mouseout", function(d){
.on("mouseleave", function(d){
if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("overlayHovering", false);
.classed("WorkflowChart-linkHovering", false);
}
$('.WorkflowChart-tooltip').remove();
})
.attr('stroke', function(d) {
if(d.target.edgeType) {
@ -736,20 +778,20 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
})
.attr("r", 10)
.attr("class", "addCircle betweenNodesCircle")
.attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes")
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; })
.call(add_node_between)
.on("mouseover", function(d) {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("addHovering", true);
.classed("WorkflowChart-addHovering", true);
})
.on("mouseout", function(d){
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("addHovering", false);
.classed("WorkflowChart-addHovering", false);
});
linkEnter.append("path")
.attr("class", "linkCross")
.attr("class", "WorkflowChart-betweenNodesIcon")
.style("fill", "white")
.attr("transform", function(d) {
let translate;
@ -769,11 +811,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.call(add_node_between)
.on("mouseover", function(d) {
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("addHovering", true);
.classed("WorkflowChart-addHovering", true);
})
.on("mouseout", function(d){
d3.select("#link-" + d.source.id + "-" + d.target.id)
.classed("addHovering", false);
.classed("WorkflowChart-addHovering", false);
});
link.exit().remove();
@ -781,21 +823,21 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
// Transition nodes and links to their new positions.
let t = baseSvg.transition();
t.selectAll(".nodeCircle")
t.selectAll(".WorkflowChart-nodeAddCircle")
.style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; });
t.selectAll(".nodeAddCross")
t.selectAll(".WorkflowChart-nodeAddIcon")
.style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; });
t.selectAll(".removeCircle")
t.selectAll(".WorkflowChart-nodeRemoveCircle")
.style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; });
t.selectAll(".nodeRemoveCross")
t.selectAll(".WorkflowChart-nodeRemoveIcon")
.style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; });
t.selectAll(".linkPath")
t.selectAll(".WorkflowChart-linkPath")
.attr("class", function(d) {
return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath";
return (d.source.placeholder || d.target.placeholder) ? "WorkflowChart-linkPath WorkflowChart-placeholder" : "WorkflowChart-linkPath";
})
.attr("d", lineData)
.attr('stroke', function(d) {
@ -815,7 +857,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}
});
t.selectAll(".betweenNodesCircle")
t.selectAll(".WorkflowChart-circleBetweenNodes")
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; })
.attr("cx", function(d) {
return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2;
})
@ -823,23 +866,32 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
});
t.selectAll(".linkOverlay")
t.selectAll(".WorkflowChart-linkOverlay")
.attr("class", function(d) {
let linkClasses = ["linkOverlay"];
let linkClasses = ["WorkflowChart-linkOverlay"];
if (d.source.isLinkEditParent && d.target.isLinkEditChild) {
linkClasses.push("linkActiveEdit");
linkClasses.push("WorkflowChart-link--active");
}
return linkClasses.join(' ');
})
.attr("points",function(d) {
const pt1 = [d.source.y + nodeW, d.source.x + 10 + nodeH/2].join(",");
const pt2 = [d.target.y,d.target.x + 10 + nodeH/2].join(",");
const pt3 = [d.target.y,d.target.x - 10 + nodeH/2].join(",");
const pt4 = [d.source.y + nodeW,d.source.x - 10 + nodeH/2].join(",");
let x1 = d.source.y + nodeW;
let y1 = d.source.x + nodeH / 2;
let x2 = d.target.y;
let y2 = d.target.x + nodeH / 2;
let slope = (y2 - y1)/(x2-x1);
let yIntercept = y1 - slope*x1;
let orthogonalDistance = 8;
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
return [pt1, pt2, pt3, pt4].join(" ");
});
t.selectAll(".linkCross")
t.selectAll(".WorkflowChart-betweenNodesIcon")
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; })
.attr("transform", function(d) {
let translate;
@ -852,7 +904,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return translate;
});
t.selectAll(".rect")
t.selectAll(".WorkflowChart-rect")
.attr('stroke', function(d) {
if(d.job && d.job.status) {
if(d.job.status === "successful"){
@ -870,12 +922,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}
})
.attr("class", function(d) {
let classString = d.placeholder ? "rect placeholder" : "rect";
let classString = d.placeholder ? "WorkflowChart-rect WorkflowChart-placeholder" : "WorkflowChart-rect";
classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : "";
return classString;
});
t.selectAll(".node")
t.selectAll(".WorkflowChart-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 + ")"; });
@ -937,25 +989,25 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
if(d.job){
switch(d.job.status) {
case "pending":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "waiting":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "running":
statusClass += "workflowChart-nodeStatus--running";
statusClass += "WorkflowChart-nodeStatus--running";
break;
case "successful":
statusClass += "workflowChart-nodeStatus--success";
statusClass += "WorkflowChart-nodeStatus--success";
break;
case "failed":
statusClass += "workflowChart-nodeStatus--failed";
statusClass += "WorkflowChart-nodeStatus--failed";
break;
case "error":
statusClass += "workflowChart-nodeStatus--failed";
statusClass += "WorkflowChart-nodeStatus--failed";
break;
case "canceled":
statusClass += "workflowChart-nodeStatus--canceled";
statusClass += "WorkflowChart-nodeStatus--canceled";
break;
}
}
@ -1058,11 +1110,10 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function edit_link() {
this.on("click", function(d) {
if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details'){
// What if the node is new? it won't have a nodeId right?
if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details'){
scope.editLink({
parentId: d.source.nodeId,
childId: d.target.nodeId
parentId: d.source.id,
childId: d.target.id
});
}
});

View File

@ -11,6 +11,7 @@ export default ['templateUrl',
return {
scope: {
linkConfig: '<',
readOnly: '<',
cancel: '&',
select: '&'
},

View File

@ -1,4 +1,4 @@
<div class="WorkflowMaker-formTitle">{{:: strings.get('workflow_maker.EDIT_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) }}</div>
<div class="WorkflowMaker-formTitle">{{readOnly ? strings.get('workflow_maker.VIEW_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) : strings.get('workflow_maker.EDIT_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) }}</div>
<div class="WorkflowMaker-form">
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
<label for="edgeType" class="Form-inputLabelContainer">
@ -13,13 +13,15 @@
class="form-control Form-dropDown"
name="edgeType"
tabindex="-1"
ng-disabled="readOnly"
aria-hidden="true">
</select>
</div>
</div>
<div class="buttons Form-buttons" id="workflow_maker_controls">
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_link_btn" ng-click="select({parentId: linkConfig.parent.id, childId: linkConfig.child.id, edgeType: edgeType.value})" ng-disabled="!edgeType"> {{:: strings.get('workflow_maker.SELECT') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_link_btn" ng-show="readOnly" ng-click="cancel()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_link_btn" ng-show="!readOnly" ng-click="select({edgeType: edgeType.value})" ng-disabled="!edgeType"> {{:: strings.get('SAVE') }}</button>
</div>
</div>
</div>

View File

@ -4,8 +4,635 @@
* All Rights Reserved
*************************************************/
export default ['$scope',
function($scope) {
console.log('inside wnf controller');
export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService', 'Rest', '$q',
'WorkflowService', 'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet',
'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList',
function($scope, TemplatesService, JobTemplate, PromptService, Rest, $q,
WorkflowService, TemplatesStrings, CreateSelect2, Empty, generateList, qs,
GetBasePath, TemplateList, ProjectList, InventorySourcesList
) {
let promptWatcher, credentialsWatcher, surveyQuestionWatcher, listPromises = [];
$scope.strings = TemplatesStrings;
let templateList = _.cloneDeep(TemplateList);
delete templateList.actions;
delete templateList.fields.type;
delete templateList.fields.description;
delete templateList.fields.smart_status;
delete templateList.fields.labels;
delete templateList.fieldActions;
templateList.fields.name.columnClass = "col-md-8";
templateList.disableRow = "{{ readOnly }}";
templateList.disableRowValue = 'readOnly';
templateList.fields.info = {
ngInclude: "'/static/partials/job-template-details.html'",
type: 'template',
columnClass: 'col-md-3',
label: '',
nosort: true
};
templateList.maxVisiblePages = 5;
templateList.searchBarFullWidth = true;
$scope.templateList = templateList;
let inventorySourceList = _.cloneDeep(InventorySourcesList);
inventorySourceList.maxVisiblePages = 5;
inventorySourceList.searchBarFullWidth = true;
inventorySourceList.disableRow = "{{ readOnly }}";
inventorySourceList.disableRowValue = 'readOnly';
$scope.inventorySourceList = inventorySourceList;
let projectList = _.cloneDeep(ProjectList);
delete projectList.fields.status;
delete projectList.fields.scm_type;
delete projectList.fields.last_updated;
projectList.fields.name.columnClass = "col-md-11";
projectList.maxVisiblePages = 5;
projectList.searchBarFullWidth = true;
projectList.disableRow = "{{ readOnly }}";
projectList.disableRowValue = 'readOnly';
$scope.projectList = projectList;
$scope.$watch('node', (newNode, oldNode) => {
if (oldNode.id !== newNode.id) {
setupNodeForm();
}
});
$scope.$watchGroup(['templates', 'projects', 'inventory_sources', 'activeTab'], () => {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.templates[i].checked = 1;
}
else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.projects[i].checked = 1;
}
else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) {
$scope.inventory_sources[i].checked = 1;
}
else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
});
const checkCredentialsForRequiredPasswords = () => {
let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => {
if ((credential.passwords_needed &&
credential.passwords_needed.length > 0) ||
(_.has(credential, 'inputs.vault_password') &&
credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
});
$scope.credentialRequiresPassword = credentialRequiresPassword;
};
const watchForPromptChanges = () => {
let promptDataToWatch = [
'promptData.prompts.inventory.value',
'promptData.prompts.verbosity.value',
'missingSurveyValue'
];
promptWatcher = $scope.$watchGroup(promptDataToWatch, function() {
let missingPromptValue = false;
if ($scope.missingSurveyValue) {
missingPromptValue = true;
} else if (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) {
missingPromptValue = true;
}
$scope.promptModalMissingReqFields = missingPromptValue;
});
if ($scope.promptData.launchConf.ask_credential_on_launch && $scope.credentialRequiresPassword) {
credentialsWatcher = $scope.$watch('promptData.prompts.credentials', () => {
checkCredentialsForRequiredPasswords();
});
}
};
const finishConfiguringEdit = () => {
let jobTemplate = new JobTemplate();
console.log($scope.node);
if (!_.isEmpty($scope.node.promptData)) {
$scope.promptData = _.cloneDeep($scope.node.promptData);
const launchConf = $scope.promptData.launchConf;
if (!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
}
$scope.nodeFormDataLoaded = true;
} else if (
_.get($scope, 'node.unifiedJobTemplate.unified_job_type') === 'job_template' ||
_.get($scope, 'node.unifiedJobTemplate.type') === 'job_template'
) {
let promises = [jobTemplate.optionsLaunch($scope.node.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.node.unifiedJobTemplate.id)];
if (_.has($scope, 'node.originalNodeObj.related.credentials')) {
Rest.setUrl($scope.node.originalNodeObj.related.credentials);
promises.push(Rest.get());
}
$q.all(promises)
.then((responses) => {
let launchOptions = responses[0].data,
launchConf = responses[1].data,
workflowNodeCredentials = responses[2] ? responses[2].data.results : [];
let prompts = PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data,
currentValues: $scope.node.originalNodeObj
});
let defaultCredsWithoutOverrides = [];
prompts.credentials.previousOverrides = _.cloneDeep(workflowNodeCredentials);
const credentialHasScheduleOverride = (templateDefaultCred) => {
let credentialHasOverride = false;
workflowNodeCredentials.forEach((scheduleCred) => {
if (templateDefaultCred.credential_type === scheduleCred.credential_type) {
if (
(!templateDefaultCred.vault_id && !scheduleCred.inputs.vault_id) ||
(templateDefaultCred.vault_id && scheduleCred.inputs.vault_id && templateDefaultCred.vault_id === scheduleCred.inputs.vault_id)
) {
credentialHasOverride = true;
}
}
});
return credentialHasOverride;
};
if (_.has(launchConf, 'defaults.credentials')) {
launchConf.defaults.credentials.forEach((defaultCred) => {
if (!credentialHasScheduleOverride(defaultCred)) {
defaultCredsWithoutOverrides.push(defaultCred);
}
});
}
prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides);
if ((!$scope.node.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.node.unifiedJobTemplate.project) {
$scope.selectedTemplateInvalid = true;
} else {
$scope.selectedTemplateInvalid = false;
}
let credentialRequiresPassword = false;
prompts.credentials.value.forEach((credential) => {
if(credential.inputs) {
if ((credential.inputs.password && credential.inputs.password === "ASK") ||
(credential.inputs.become_password && credential.inputs.become_password === "ASK") ||
(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") ||
(credential.inputs.vault_password && credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
} else if (credential.passwords_needed && credential.passwords_needed.length > 0) {
credentialRequiresPassword = true;
}
});
$scope.credentialRequiresPassword = credentialRequiresPassword;
if (!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
$scope.nodeFormDataLoaded = true;
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
if (responses[1].data.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions($scope.node.unifiedJobTemplate.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec,
extra_data: _.cloneDeep($scope.node.originalNodeObj.extra_data)
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data);
$scope.node.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
surveyQuestions: surveyQuestionRes.data.spec,
template: $scope.node.unifiedJobTemplate.id
};
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if (question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
checkCredentialsForRequiredPasswords();
watchForPromptChanges();
$scope.nodeFormDataLoaded = true;
});
} else {
$scope.node.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
template: $scope.node.unifiedJobTemplate.id
};
checkCredentialsForRequiredPasswords();
watchForPromptChanges();
$scope.nodeFormDataLoaded = true;
}
}
});
} else {
$scope.nodeFormDataLoaded = true;
}
if (_.get($scope, 'node.unifiedJobTemplate')) {
if (_.get($scope, 'node.unifiedJobTemplate.type') === "job_template") {
$scope.activeTab = "jobs";
}
$scope.selectedTemplate = $scope.node.unifiedJobTemplate;
if ($scope.selectedTemplate.unified_job_type) {
switch ($scope.selectedTemplate.unified_job_type) {
case "job":
$scope.activeTab = "jobs";
break;
case "project_update":
$scope.activeTab = "project_syncs";
break;
case "inventory_update":
$scope.activeTab = "inventory_syncs";
break;
}
} else if ($scope.selectedTemplate.type) {
switch ($scope.selectedTemplate.type) {
case "job_template":
$scope.activeTab = "jobs";
break;
case "project":
$scope.activeTab = "project_syncs";
break;
case "inventory_source":
$scope.activeTab = "inventory_syncs";
break;
}
}
} else {
$scope.activeTab = "jobs";
}
if ($scope.mode === 'add') {
const alwaysOption = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: 'always'
};
const successOption = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: 'success'
};
const failureOption = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: 'failure'
};
$scope.edgeTypeOptions = [alwaysOption];
switch($scope.node.isRoot) {
case true:
$scope.edgeType = alwaysOption;
break;
case false:
$scope.edgeType = successOption;
$scope.edgeTypeOptions.push(successOption, failureOption);
break;
}
CreateSelect2({
element: '#workflow_node_edge_3',
multiple: false
});
$scope.nodeFormDataLoaded = true;
}
};
// Determine whether or not we need to go out and GET this nodes unified job template
// in order to determine whether or not prompt fields are needed
$scope.openPromptModal = function() {
$scope.promptData.triggerModalOpen = true;
};
$scope.toggle_row = function(selectedRow) {
if (!$scope.readOnly) {
// TODO: make this more concise
switch($scope.activeTab) {
case 'jobs':
$scope.templates.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.templates[i].checked = 1;
} else {
$scope.templates[i].checked = 0;
}
});
break;
case 'project_syncs':
$scope.projects.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.projects[i].checked = 1;
} else {
$scope.projects[i].checked = 0;
}
});
break;
case 'inventory_syncs':
$scope.inventory_sources.forEach(function(row, i) {
if (row.id === selectedRow.id) {
$scope.inventory_sources[i].checked = 1;
} else {
$scope.inventory_sources[i].checked = 0;
}
});
break;
}
templateManuallySelected(selectedRow);
}
};
const templateManuallySelected = (selectedTemplate) => {
if (promptWatcher) {
promptWatcher();
}
if (surveyQuestionWatcher) {
surveyQuestionWatcher();
}
if (credentialsWatcher) {
credentialsWatcher();
}
$scope.promptData = null;
if (selectedTemplate.type === "job_template") {
let jobTemplate = new JobTemplate();
$q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)])
.then((responses) => {
let launchConf = responses[1].data;
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
$scope.selectedTemplateInvalid = true;
} else {
$scope.selectedTemplateInvalid = false;
}
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
$scope.credentialRequiresPassword = true;
} else {
$scope.credentialRequiresPassword = false;
}
$scope.selectedTemplate = angular.copy(selectedTemplate);
if (!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
if (launchConf.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions(selectedTemplate.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
surveyQuestions: processed.surveyQuestions,
template: selectedTemplate.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if (question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
watchForPromptChanges();
});
} else {
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
template: selectedTemplate.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
watchForPromptChanges();
}
}
});
} else {
$scope.selectedTemplate = angular.copy(selectedTemplate);
$scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
}
};
const setupNodeForm = () => {
$scope.nodeFormDataLoaded = false;
$scope.template_queryset = {
page_size: '5',
order_by: 'name',
type: 'workflow_job_template,job_template'
};
$scope.templates = [];
$scope.template_dataset = {};
// Go out and GET the list contents for each of the tabs
listPromises.push(
qs.search(GetBasePath('unified_job_templates'), $scope.template_queryset)
.then(function(res) {
$scope.template_dataset = res.data;
$scope.templates = $scope.template_dataset.results;
})
);
$scope.project_queryset = {
page_size: '5',
order_by: 'name'
};
$scope.projects = [];
$scope.project_dataset = {};
listPromises.push(
qs.search(GetBasePath('projects'), $scope.project_queryset)
.then(function(res) {
$scope.project_dataset = res.data;
$scope.projects = $scope.project_dataset.results;
})
);
$scope.inventory_source_dataset = {
page_size: '5',
order_by: 'name',
not__source: ''
}
$scope.inventory_sources = [];
$scope.inventory_source_dataset = {};
listPromises.push(
qs.search(GetBasePath('inventory_sources'), $scope.inventory_source_dataset)
.then(function(res) {
$scope.inventory_source_dataset = res.data;
$scope.inventory_sources = $scope.inventory_source_dataset.results;
})
);
$q.all(listPromises)
.then(() => {
if (!$scope.node.isNew && !$scope.node.edited && $scope.node.unifiedJobTemplate && $scope.node.unifiedJobTemplate.unified_job_type && $scope.node.unifiedJobTemplate.unified_job_type === 'job') {
// This is a node that we got back from the api with an incomplete
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.node.unifiedJobTemplate.id)
.then(function(data) {
$scope.node.unifiedJobTemplate = _.clone(data.data.results[0]);
finishConfiguringEdit();
}, function(error) {
ProcessErrors($scope, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to get unified job template. GET returned ' +
'status: ' + error.status
});
});
} else {
finishConfiguringEdit();
}
});
}
setupNodeForm();
}
];

View File

@ -9,13 +9,16 @@ import workflowNodeFormController from './workflow-node-form.controller';
export default ['templateUrl',
function(templateUrl) {
return {
scope: {},
scope: {
mode: '<',
node: '=',
cancel: '&',
select: '&',
readOnly: '<'
},
restrict: 'E',
templateUrl: templateUrl('templates/workflows/workflow-maker/forms/workflow-node-form'),
controller: workflowNodeFormController,
link: function(scope) {
console.log('inside link function for workflow node form');
}
controller: workflowNodeFormController
};
}
];

View File

@ -1,17 +1,111 @@
<div class="Form-tabHolder">
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'jobs'}" ng-click="toggleFormTab('jobs')">{{strings.get('workflow_maker.JOBS')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'project_sync'}" ng-click="toggleFormTab('project_sync')">{{strings.get('workflow_maker.PROJECT_SYNC')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'inventory_sync'}" ng-click="toggleFormTab('inventory_sync')">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</div>
</div>
<div class="WorkflowMaker-formLists">
<div id="workflow-jobs-list" ui-view="jobTemplateList" ng-show="workflowMakerFormConfig.activeTab === 'jobs'"></div>
<div id="workflow-project-sync-list" ui-view="projectSyncList" ng-show="workflowMakerFormConfig.activeTab === 'project_sync'"></div>
<div id="workflow-inventory-sync-list" ui-view="inventorySyncList" ng-show="workflowMakerFormConfig.activeTab === 'inventory_sync'"></div>
</div>
<span ng-show="selectedTemplate &&
((selectedTemplate.type === 'job_template' && workflowMakerFormConfig.activeTab === 'jobs') ||
(selectedTemplate.type === 'project' && workflowMakerFormConfig.activeTab === 'project_sync') ||
(selectedTemplate.type === 'inventory_source' && workflowMakerFormConfig.activeTab === 'inventory_sync'))">
<div ng-show="nodeFormDataLoaded">
<div class="WorkflowMaker-formTitle ng-binding">{{mode === 'edit' ? node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}</div>
<div class="Form-tabHolder">
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'jobs'}" ng-click="activeTab = 'jobs'">{{strings.get('workflow_maker.JOBS')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'project_syncs'}" ng-click="activeTab = 'project_syncs'">{{strings.get('workflow_maker.PROJECT_SYNC')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'inventory_syncs'}" ng-click="activeTab = 'inventory_syncs'">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</div>
</div>
<div class="WorkflowMaker-formLists">
<div id="workflow-jobs-list" ng-show="activeTab === 'jobs'">
<div ng-hide="templates.length === 0 && (searchTags | isEmpty)">
<smart-search django-model="templates" base-path="unified_job_templates" iterator="template" dataset="template_dataset" list="templateList" collection="templates" default-params="template_default_params" query-set="template_queryset" search-bar-full-width="true" search-tags="searchTags">
</smart-search>
</div>
<div class="row" ng-show="templates.length === 0 && !(searchTags | isEmpty)">
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
</div>
<div class="List-noItems" ng-show="templates.length === 0 && (searchTags | isEmpty)">PLEASE ADD ITEMS TO THIS LIST</div>
<div class="list-table-container" ng-show="templates.length > 0">
<table id="templates_table" class="List-table" is-extended="false">
<thead>
<tr class="List-tableHeaderRow">
<th class="List-tableHeader select-column List-staticColumn--smallStatus" translate=""></th>
<th base-path="unified_job_templates" collection="templates" dataset="template_dataset" column-sort="" column-field="name" column-iterator="template" column-no-sort="undefined" column-label="Name" column-custom-class="" query-set="template_queryset">
</th>
<th class="List-tableHeader--info" base-path="unified_job_templates" collection="templates" dataset="template_dataset" column-sort="" column-field="info" column-iterator="template" column-no-sort="true" column-label="" column-custom-class="" query-set="template_queryset">
</th>
</tr>
</thead>
<tbody>
<tr ng-class="[template.success_class, {'List-tableRow--selected' : $stateParams['template_id'] == template.id}, {'List-tableRow--disabled': !template.summary_fields.user_capabilities.edit}]" id="{{ template.id }}" class="List-tableRow template_class" disable-row="{{ !template.summary_fields.user_capabilities.edit }}" ng-repeat="template in templates">
<td class="List-tableCell">
<input type="radio" ng-model="template.checked" ng-value="1" ng-false-value="0" name="check_template_{{template.id}}" ng-click="toggle_row(template)" ng-disabled="!template.summary_fields.user_capabilities.edit">
</td>
<td class="List-tableCell name-column col-md-8" ng-click="toggle_row(template)">
{{ template.name }}</td>
<td class="col-md-3" ng-include="'/static/partials/job-template-details.html'"></td>
</tr>
</tbody>
</table>
</div>
<paginate base-path="unified_job_templates" collection="templates" dataset="template_dataset" iterator="template" query-set="template_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
</div>
<div id="workflow-project-sync-list" ng-show="activeTab === 'project_syncs'">
<div ng-hide="projects.length === 0 && (searchTags | isEmpty)">
<smart-search django-model="projects" base-path="projects" iterator="project" dataset="project_dataset" list="projectList" collection="projects" default-params="project_default_params" query-set="project_queryset" search-bar-full-width="true" search-tags="searchTags">
</smart-search>
</div>
<div class="row" ng-show="projects.length === 0 && !(searchTags | isEmpty)">
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
</div>
<div class="List-noItems" ng-show="projects.length === 0 && (searchTags | isEmpty)">No Projects Have Been Created</div>
<div class="list-table-container" ng-show="projects.length > 0">
<table id="projects_table" class="List-table" is-extended="false">
<thead>
<tr class="List-tableHeaderRow">
<th class="List-tableHeader select-column List-staticColumn--smallStatus" translate=""></th>
<th base-path="projects" collection="projects" dataset="project_dataset" column-sort="" column-field="name" column-iterator="project" column-no-sort="undefined" column-label="Name" column-custom-class="col-md-8" query-set="project_queryset">
</th>
</tr>
</thead>
<tbody>
<tr ng-class="[project.success_class, {'List-tableRow--selected' : $stateParams['project_id'] == project.id}]" id="{{ project.id }}" class="List-tableRow project_class" ng-repeat="project in projects">
<td class="List-tableCell">
<input type="radio" ng-model="project.checked" ng-value="1" ng-false-value="0" name="check_project_{{project.id}}" ng-click="toggle_row(project)" ng-disabled="undefined">
</td>
<td class="List-tableCell name-column col-md-8" ng-click="toggle_row(project)">
{{ project.name }}</td>
</tr>
</tbody>
</table>
</div>
<paginate base-path="projects" collection="projects" dataset="project_dataset" iterator="project" query-set="project_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
</div>
<div id="workflow-inventory-sync-list" ng-show="activeTab === 'inventory_syncs'">
<div ng-hide="inventory_sources.length === 0 && (searchTags | isEmpty)">
<smart-search django-model="inventory_sources" base-path="inventory_sources" iterator="inventory_source" dataset="inventory_source_dataset" list="inventorySourceList" collection="inventory_sources" default-params="inventory_source_default_params" query-set="inventory_source_queryset" search-bar-full-width="true" search-tags="searchTags">
</smart-search>
</div>
<div class="row" ng-show="inventory_sources.length === 0 && !(searchTags | isEmpty)">
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
</div>
<div class="List-noItems" ng-show="inventory_sources.length === 0 && (searchTags | isEmpty)">PLEASE ADD ITEMS TO THIS LIST</div>
<div class="list-table-container" ng-show="inventory_sources.length > 0">
<table id="workflow_inventory_sources_table" class="List-table" is-extended="false">
<thead>
<tr class="List-tableHeaderRow">
<th class="List-tableHeader select-column List-staticColumn--smallStatus" translate=""></th>
<th base-path="inventory_sources" collection="inventory_sources" dataset="inventory_source_dataset" column-sort="" column-field="name" column-iterator="inventory_source" column-no-sort="undefined" column-label="Name" column-custom-class="" query-set="inventory_source_queryset">
</th>
</tr>
</thead>
<tbody>
<tr ng-class="[inventory_source.success_class, {'List-tableRow--selected' : $stateParams['inventory_source_id'] == inventory_source.id}]" id="{{ inventory_source.id }}" class="List-tableRow inventory_source_class" ng-repeat="inventory_source in inventory_sources">
<td class="List-tableCell">
<input type="radio" ng-model="inventory_source.checked" ng-value="1" ng-false-value="0" name="check_inventory_source_{{inventory_source.id}}" ng-click="toggle_row(inventory_source)" ng-disabled="undefined">
</td>
<td class="List-tableCell name-column col-md-11" ng-click="toggle_row(inventory_source)">
<span aw-tool-tip="Inventory: {{inventory_source.summary_fields.inventory.name}}" data-placement="top">{{ inventory_source.name }}</span></td>
</tr>
</tbody>
</table>
</div>
<paginate base-path="inventory_sources" collection="inventory_sources" dataset="inventory_source_dataset" iterator="inventory_source" query-set="inventory_source_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
</div>
</div>
<div ng-if="selectedTemplate && selectedTemplateInvalid">
<div class="WorkflowMaker-invalidJobTemplateWarning">
<span class="fa fa-warning"></span>
@ -24,28 +118,29 @@
<span>{{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}</span>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="selectedTemplate && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch)">
<label for="verbosity" class="Form-inputLabelContainer">
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="mode === 'add'">
<label for="edgeType" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: strings.get('workflow_maker.RUN') }}</span>
</label>
<div>
<select
id="workflow_node_edge"
id="workflow_node_edge_3"
ng-options="v as v.label for v in edgeTypeOptions track by v.value"
ng-model="edgeType"
class="form-control Form-dropDown"
name="edgeType"
tabindex="-1"
ng-disabled="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"
ng-disabled="readOnly"
aria-hidden="true">
</select>
</div>
</div>
<div class="buttons Form-buttons" id="workflow_maker_controls">
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_btn" ng-show="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch)" ng-click="confirmNodeForm()" ng-disabled="!selectedTemplate || promptModalMissingReqFields || credentialRequiresPassword"> {{:: strings.get('workflow_maker.SELECT') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_btn" ng-show="readOnly" ng-click="cancel()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_btn" ng-show="!readOnly" ng-click="select({selectedTemplate, promptData, edgeType})" ng-disabled="!selectedTemplate || promptModalMissingReqFields || credentialRequiresPassword || selectedTemplateInvalid"> {{:: strings.get('workflow_maker.SELECT') }}</button>
</div>
</span>
<prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="readOnly"></prompt>
</div>

View File

@ -13,22 +13,26 @@
display: flex;
height: 34px;
}
.WorkflowMaker-title {
align-items: center;
flex: 1 0 auto;
display: flex;
height: 34px;
}
.WorkflowMaker-titleText {
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
}
.WorkflowMaker-exitHolder {
justify-content: flex-end;
display: flex;
}
.WorkflowMaker-exit{
cursor:pointer;
padding:0px;
@ -40,9 +44,11 @@
transition: color 0.2s;
line-height:1;
}
.WorkflowMaker-exit:hover{
color:@default-icon;
}
.WorkflowMaker-contentHolder {
display: flex;
border: 1px solid @b7grey;
@ -50,11 +56,13 @@
height: ~"calc(100% - 85px)";
overflow: hidden;
}
.WorkflowMaker-contentLeft {
flex: 1;
flex-direction: column;
height: 100%;
}
.WorkflowMaker-contentRight {
flex: 0 0 400px;
border-left: 1px solid @b7grey;
@ -63,12 +71,14 @@
height: 100%;
overflow-y: scroll;
}
.WorkflowMaker-buttonHolder {
height: 30px;
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.WorkflowMaker-saveButton{
background-color: @submit-button-bg;
color: @submit-button-text;
@ -117,45 +127,55 @@
justify-content: center;
border-radius: 4px;
}
.WorkflowMaker-deleteModal {
height: 200px;
width: 600px;
background-color: @default-bg;
border-radius: 5px;
}
.WorkflowMaker-formTitle {
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
}
.WorkflowMaker-formHelp {
color: @default-interface-txt;
}
.WorkflowMaker-formLists {
margin-bottom: 20px;
.SmartSearch-searchTermContainer {
width: 100%;
}
}
.WorkflowMaker-formTitle {
display: flex;
color: @default-interface-txt;
margin-right: 10px;
}
.WorkflowMaker-formLabel {
font-weight: normal;
}
.WorkflowMaker-formElement {
margin-bottom: 10px;
}
.WorkflowMaker-chart {
display: flex;
width: 100%;
}
.WorkflowMaker-totalJobs {
margin-right: 5px;
}
.WorkflowLegend-maker {
display: flex;
height: 40px;
@ -164,33 +184,39 @@
background: @default-bg;
border-bottom: 1px solid @b7grey;
}
.WorkflowLegend-maker--left {
flex: 1 0 auto;
}
.WorkflowLegend-maker--right {
flex: 0 0 217px;
text-align: right;
padding-right: 20px;
position: relative;
}
.WorkflowLegend-onSuccessLegend {
height: 4px;
width: 20px;
background-color: @submit-button-bg;
margin: 18px 5px 18px 0px;
}
.WorkflowLegend-onFailLegend {
height: 4px;
width: 20px;
background-color: @default-err;
margin: 18px 5px 18px 0px;
}
.WorkflowLegend-alwaysLegend {
height: 4px;
width: 20px;
background-color: @default-link;
margin: 18px 5px 18px 0px;
}
.WorkflowLegend-letterCircle{
border-radius: 50%;
width: 20px;
@ -201,6 +227,7 @@
margin: 10px 5px 10px 0px;
line-height: 20px;
}
.WorkflowLegend-details {
align-items: center;
display: flex;
@ -215,6 +242,7 @@
display: block;
flex: 1 0 auto;
}
.WorkflowLegend-details--right {
flex: 0 0 44px;
text-align: right;
@ -229,15 +257,18 @@
font-size: 1.2em;
margin-left: 15px;
}
.Key-menuIcon:hover,
.WorkflowMaker-manualControlsIcon:hover {
color: @default-link-hov;
cursor: pointer;
}
.Key-menuIcon--active,
.WorkflowMaker-manualControlsIcon--active {
color: @default-link-hov;
}
.WorkflowMaker-manualControls {
position: absolute;
left: -106px;
@ -251,6 +282,7 @@
margin-left: -1px;
border-right: 0;
}
.WorkflowLegend-manualControls {
position: absolute;
left: -272px;
@ -262,13 +294,16 @@
border: 1px solid @d7grey;
border-bottom-left-radius: 5px;
}
.WorkflowMaker-formTab {
margin-right: 10px;
}
.WorkflowMaker-preventBodyScrolling {
height: 100%;
overflow: hidden;
}
.WorkflowMaker-invalidJobTemplateWarning {
margin-bottom: 5px;
color: @default-err;
@ -281,15 +316,18 @@
background-color: @default-bg;
border: 1px solid @default-list-header-bg;
}
.Key-listItem {
display: flex;
padding: 0;
margin: 5px 0 0 0;
}
.Key-listItemContent {
margin: 0;
line-height: 20px;
}
.Key-listItemContent--circle {
line-height: 28px;
}
@ -300,27 +338,34 @@
margin: 9px 5px 9px 0px;
outline: none;
}
.Key-heading {
font-weight: 700;
margin: 0 0 10px;
line-height: 0;
padding: 0;
}
.Key-icon--success {
background-color: @submit-button-bg;
}
.Key-icon--fail {
background-color: @default-err;
}
.Key-icon--always {
background-color: @default-link;
}
.Key-icon--warning {
background: @default-warning;
}
.Key-icon--default {
background: @default-icon;
}
.Key-icon--circle {
border-radius: 50%;
width: 20px;
@ -330,6 +375,7 @@
line-height: 20px;
margin: 4px 5px 5px 0px;
}
.Key-details {
display: flex;
height: 40px;

View File

@ -11,27 +11,11 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
ProcessErrors, CreateSelect2, $q, JobTemplate, WorkflowJobTemplate,
Empty, PromptService, Rest, TemplatesStrings, $timeout, $state) {
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
$scope.strings = TemplatesStrings;
// TODO: I don't think this needs to be on scope but changing it will require changes to
// all the prompt places
$scope.preventCredsWithPasswords = true;
$scope.workflowMakerFormConfig = {
nodeMode: "idle",
activeTab: "jobs",
formIsValid: false
};
$scope.job_type_options = [{
label: $scope.strings.get('workflow_maker.RUN'),
value: "run"
}, {
label: $scope.strings.get('workflow_maker.CHECK'),
value: "check"
}];
$scope.edgeTypeOptions = createEdgeTypeOptions();
let editRequests = [];
let associateRequests = [];
let disassociateRequests = [];
@ -41,32 +25,10 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
$scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`;
function createEdgeTypeOptions() {
return ([{
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: 'always'
},
{
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: 'success'
},
{
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: 'failure'
}
]);
}
function resetNodeForm() {
$scope.workflowMakerFormConfig.nodeMode = "idle";
delete $scope.selectedTemplate;
delete $scope.placeholderNode;
delete $scope.betweenTwoNodes;
$scope.nodeBeingEdited = null;
$scope.workflowMakerFormConfig.activeTab = "jobs";
$scope.$broadcast('clearWorkflowLists');
}
$scope.formState = {
'showNodeForm': false,
'showLinkForm': false
};
function recursiveNodeUpdates(params, completionCallback) {
// params.parentId
@ -301,62 +263,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
}
}
let updateEdgeDropdownOptions = (edgeTypeValue) => {
// Not passing an edgeTypeValue will include all by default
if (edgeTypeValue) {
$scope.edgeTypeOptions = _.filter(createEdgeTypeOptions(), {
'value': edgeTypeValue
});
} else {
$scope.edgeTypeOptions = createEdgeTypeOptions();
}
CreateSelect2({
element: '#workflow_node_edge',
multiple: false
});
};
let checkCredentialsForRequiredPasswords = () => {
let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => {
if ((credential.passwords_needed &&
credential.passwords_needed.length > 0) ||
(_.has(credential, 'inputs.vault_password') &&
credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
});
$scope.credentialRequiresPassword = credentialRequiresPassword;
};
let watchForPromptChanges = () => {
let promptDataToWatch = [
'promptData.prompts.inventory.value',
'promptData.prompts.verbosity.value',
'missingSurveyValue'
];
promptWatcher = $scope.$watchGroup(promptDataToWatch, function () {
let missingPromptValue = false;
if ($scope.missingSurveyValue) {
missingPromptValue = true;
} else if ($scope.selectedTemplate.type === 'job_template' && (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id)) {
missingPromptValue = true;
}
$scope.promptModalMissingReqFields = missingPromptValue;
});
if ($scope.promptData.launchConf.ask_credential_on_launch && $scope.credentialRequiresPassword) {
credentialsWatcher = $scope.$watch('promptData.prompts.credentials', () => {
checkCredentialsForRequiredPasswords();
});
}
};
$scope.closeWorkflowMaker = function () {
$scope.closeWorkflowMaker = function() {
// Revert the data to the master which was created when the dialog was opened
$scope.treeData.data = angular.copy($scope.treeDataMaster);
$scope.closeDialog();
@ -442,19 +349,15 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.startAddNode = function (parent, betweenTwoNodes) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
if ($scope.nodeBeingWorkedOn) {
$scope.cancelNodeForm();
}
if ($scope.linkBeingEdited) {
if ($scope.linkBeingWorkedOn) {
$scope.cancelLinkForm();
}
$scope.workflowMakerFormConfig.nodeMode = "add";
$scope.addParent = parent;
$scope.betweenTwoNodes = betweenTwoNodes;
$scope.placeholderNode = WorkflowService.addPlaceholderNode({
$scope.nodeBeingWorkedOn = WorkflowService.addPlaceholderNode({
parent: parent,
betweenTwoNodes: betweenTwoNodes,
tree: $scope.treeData.data,
@ -463,443 +366,96 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.treeData.nextIndex++;
// Set the default to success
let edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) {
// This node will always be executed
updateEdgeDropdownOptions('always');
edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
} else {
updateEdgeDropdownOptions();
}
$scope.edgeType = edgeType;
$scope.$broadcast("refreshWorkflowChart");
$scope.nodeFormMode = "add";
$scope.formState.showNodeForm = true;
};
$scope.confirmNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
$scope.placeholderNode.unifiedJobTemplate = $scope.selectedTemplate;
$scope.placeholderNode.edgeType = $scope.edgeType.value;
if ($scope.placeholderNode.unifiedJobTemplate.type === 'job_template' ||
$scope.placeholderNode.unifiedJobTemplate.type === 'workflow_job_template') {
$scope.placeholderNode.promptData = _.cloneDeep($scope.promptData);
$scope.confirmNodeForm = function(selectedTemplate, promptData, edgeType) {
if ($scope.nodeFormMode === "add") {
if (selectedTemplate && edgeType && edgeType.value) {
$scope.nodeBeingWorkedOn.unifiedJobTemplate = selectedTemplate;
$scope.nodeBeingWorkedOn.edgeType = edgeType.value;
if ($scope.nodeBeingWorkedOn.unifiedJobTemplate.type === 'job_template') {
$scope.nodeBeingWorkedOn.promptData = _.cloneDeep(promptData);
}
$scope.placeholderNode.canEdit = true;
$scope.nodeBeingWorkedOn.canEdit = true;
delete $scope.placeholderNode.placeholder;
resetNodeForm();
delete $scope.nodeBeingWorkedOn.placeholder;
// Increment the total node counter
$scope.treeData.data.totalNodes++;
}
} else if ($scope.workflowMakerFormConfig.nodeMode === "edit") {
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
$scope.nodeBeingEdited.unifiedJobTemplate = $scope.selectedTemplate;
$scope.nodeBeingEdited.edgeType = $scope.edgeType.value;
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template' || $scope.nodeBeingEdited.unifiedJobTemplate.type === 'workflow_job_template') {
$scope.nodeBeingEdited.promptData = _.cloneDeep($scope.promptData);
} else if ($scope.nodeFormMode === "edit") {
if (selectedTemplate) {
$scope.nodeBeingWorkedOn.unifiedJobTemplate = selectedTemplate;
if ($scope.nodeBeingWorkedOn.unifiedJobTemplate.type === 'job_template') {
$scope.nodeBeingWorkedOn.promptData = _.cloneDeep(promptData);
}
$scope.nodeBeingEdited.isActiveEdit = false;
$scope.nodeBeingWorkedOn.isActiveEdit = false;
$scope.nodeBeingEdited.edited = true;
resetNodeForm();
$scope.nodeBeingWorkedOn.edited = true;
}
}
if (promptWatcher) {
promptWatcher();
}
if (surveyQuestionWatcher) {
surveyQuestionWatcher();
}
if (credentialsWatcher) {
credentialsWatcher();
}
$scope.promptData = null;
$scope.formState.showNodeForm = false;
$scope.nodeFormMode = null;
$scope.nodeBeingWorkedOn = null;
$scope.$broadcast("refreshWorkflowChart");
};
$scope.cancelNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
$scope.cancelNodeForm = function() {
if ($scope.nodeFormMode === "add") {
// Remove the placeholder node from the tree
WorkflowService.removeNodeFromTree({
tree: $scope.treeData.data,
nodeToBeDeleted: $scope.placeholderNode
nodeToBeDeleted: $scope.nodeBeingWorkedOn
});
} else if ($scope.workflowMakerFormConfig.nodeMode === "edit") {
$scope.nodeBeingEdited.isActiveEdit = false;
} else if ($scope.nodeFormMode === "edit") {
$scope.nodeBeingWorkedOn.isActiveEdit = false;
}
if (promptWatcher) {
promptWatcher();
}
if (surveyQuestionWatcher) {
surveyQuestionWatcher();
}
if (credentialsWatcher) {
credentialsWatcher();
}
$scope.promptData = null;
$scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false;
// Reset the form
resetNodeForm();
$scope.formState.showNodeForm = false;
$scope.nodeBeingWorkedOn = null;
$scope.nodeFormMode = null;
$scope.$broadcast("refreshWorkflowChart");
};
/* EDIT NODE FUNCTIONS */
$scope.startEditNode = function (nodeToEdit) {
$scope.editNodeHelpMessage = null;
if ($scope.linkBeingEdited) {
$scope.startEditNode = function(nodeToEdit) {
if ($scope.linkBeingWorkedOn) {
$scope.cancelLinkForm();
}
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
if (!$scope.nodeBeingWorkedOn || ($scope.nodeBeingWorkedOn && $scope.nodeBeingWorkedOn.id !== nodeToEdit.id)) {
if ($scope.nodeBeingWorkedOn) {
$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.nodeFormMode = "edit";
$scope.formState.showNodeForm = true;
let parent = WorkflowService.searchTree({
element: $scope.treeData.data,
matchingId: nodeToEdit.parent.id
});
$scope.nodeBeingEdited = WorkflowService.searchTree({
$scope.nodeBeingWorkedOn = WorkflowService.searchTree({
element: parent,
matchingId: nodeToEdit.id
});
$scope.nodeBeingEdited.isActiveEdit = true;
let finishConfiguringEdit = function () {
let templateType = $scope.nodeBeingEdited.unifiedJobTemplate.type;
let jobTemplate = templateType === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
if (!_.isEmpty($scope.nodeBeingEdited.promptData)) {
$scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData);
const launchConf = $scope.promptData.launchConf;
if (!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template' && launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
}
} else if (
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job' ||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template' ||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'workflow_job' ||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'workflow_job_template'
) {
let promises = [jobTemplate.optionsLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id)];
if (_.has($scope, 'nodeBeingEdited.originalNodeObj.related.credentials')) {
Rest.setUrl($scope.nodeBeingEdited.originalNodeObj.related.credentials);
promises.push(Rest.get());
}
$q.all(promises)
.then((responses) => {
let launchOptions = responses[0].data,
launchConf = responses[1].data,
workflowNodeCredentials = responses[2] ? responses[2].data.results : [];
let prompts = PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data,
currentValues: $scope.nodeBeingEdited.originalNodeObj
});
let defaultCredsWithoutOverrides = [];
prompts.credentials.previousOverrides = _.cloneDeep(workflowNodeCredentials);
const credentialHasScheduleOverride = (templateDefaultCred) => {
let credentialHasOverride = false;
workflowNodeCredentials.forEach((scheduleCred) => {
if (templateDefaultCred.credential_type === scheduleCred.credential_type) {
if (
(!templateDefaultCred.vault_id && !scheduleCred.inputs.vault_id) ||
(templateDefaultCred.vault_id && scheduleCred.inputs.vault_id && templateDefaultCred.vault_id === scheduleCred.inputs.vault_id)
) {
credentialHasOverride = true;
}
}
});
return credentialHasOverride;
};
if (_.has(launchConf, 'defaults.credentials')) {
launchConf.defaults.credentials.forEach((defaultCred) => {
if (!credentialHasScheduleOverride(defaultCred)) {
defaultCredsWithoutOverrides.push(defaultCred);
}
});
}
prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides);
if ($scope.nodeBeingEdited.unifiedJobTemplate.unified_job_template === 'job') {
if ((!$scope.nodeBeingEdited.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.nodeBeingEdited.unifiedJobTemplate.project) {
$scope.selectedTemplateInvalid = true;
} else {
$scope.selectedTemplateInvalid = false;
}
} else {
$scope.selectedTemplateInvalid = false;
}
let credentialRequiresPassword = false;
prompts.credentials.value.forEach((credential) => {
if (credential.inputs) {
if ((credential.inputs.password && credential.inputs.password === "ASK") ||
(credential.inputs.become_password && credential.inputs.become_password === "ASK") ||
(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") ||
(credential.inputs.vault_password && credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
} else if (credential.passwords_needed && credential.passwords_needed.length > 0) {
credentialRequiresPassword = true;
}
});
$scope.credentialRequiresPassword = credentialRequiresPassword;
if (!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
if (responses[1].data.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions($scope.nodeBeingEdited.unifiedJobTemplate.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec,
extra_data: _.cloneDeep($scope.nodeBeingEdited.originalNodeObj.extra_data)
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data);
$scope.nodeBeingEdited.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
surveyQuestions: surveyQuestionRes.data.spec,
templateType: $scope.nodeBeingEdited.unifiedJobTemplate.type,
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
};
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if (question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
checkCredentialsForRequiredPasswords();
watchForPromptChanges();
});
} else {
$scope.nodeBeingEdited.promptData = $scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
templateType: $scope.nodeBeingEdited.unifiedJobTemplate.type,
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
};
checkCredentialsForRequiredPasswords();
watchForPromptChanges();
}
}
});
}
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) {
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === "job_template" ||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === "workflow_job_template") {
$scope.workflowMakerFormConfig.activeTab = "jobs";
}
$scope.selectedTemplate = $scope.nodeBeingEdited.unifiedJobTemplate;
if ($scope.selectedTemplate.unified_job_type) {
switch ($scope.selectedTemplate.unified_job_type) {
case "job":
case "workflow_job":
$scope.workflowMakerFormConfig.activeTab = "jobs";
break;
case "project_update":
$scope.workflowMakerFormConfig.activeTab = "project_sync";
break;
case "inventory_update":
$scope.workflowMakerFormConfig.activeTab = "inventory_sync";
break;
}
} else if ($scope.selectedTemplate.type) {
switch ($scope.selectedTemplate.type) {
case "job_template":
case "workflow_job_template":
$scope.workflowMakerFormConfig.activeTab = "jobs";
break;
case "project":
$scope.workflowMakerFormConfig.activeTab = "project_sync";
break;
case "inventory_source":
$scope.workflowMakerFormConfig.activeTab = "inventory_sync";
break;
}
}
} else {
$scope.workflowMakerFormConfig.activeTab = "jobs";
}
let edgeDropdownOptions = null;
// Select RUN dropdown option
switch ($scope.nodeBeingEdited.edgeType) {
case "always":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
if ($scope.nodeBeingEdited.isRoot) {
edgeDropdownOptions = 'always';
}
break;
case "success":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
break;
case "failure":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: "failure"
};
break;
}
$timeout(updateEdgeDropdownOptions(edgeDropdownOptions));
$scope.$broadcast("refreshWorkflowChart");
};
// Determine whether or not we need to go out and GET this nodes unified job template
// in order to determine whether or not prompt fields are needed
if (!$scope.nodeBeingEdited.isNew && !$scope.nodeBeingEdited.edited &&
(_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job' ||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'workflow_job')) {
// This is a node that we got back from the api with an incomplete
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
.then(function (data) {
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
finishConfiguringEdit();
}, function ({
data,
status,
config
}) {
ProcessErrors($scope, data, status, null, {
hdr: $scope.strings.get('error.HEADER'),
msg: $scope.strings.get('error.CALL', {
path: `${config.url}`,
action: `${config.method}`,
status
})
});
});
} else {
finishConfiguringEdit();
}
$scope.nodeBeingWorkedOn.isActiveEdit = true;
}
};
$scope.$broadcast("refreshWorkflowChart");
}
/* EDIT LINK FUNCTIONS */
@ -907,18 +463,17 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
const setupLinkEdit = () => {
const parentNode = WorkflowService.searchTree({
element: $scope.treeData.data,
matchingId: parentId,
byNodeId: true
matchingId: parentId
});
parentNode.isLinkEditParent = true;
// Loop across children looking for childId
const childNode = _.find(parentNode.children, {'nodeId': childId});
const childNode = _.find(parentNode.children, {'id': childId});
childNode.isLinkEditChild = true;
$scope.linkBeingEdited = {
$scope.linkBeingWorkedOn = {
parent: parentNode,
child: childNode
}
@ -934,19 +489,19 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
},
edgeType: childNode.edgeType
}
$scope.editLink = true;
$scope.formState.showLinkForm = true;
$scope.$broadcast("refreshWorkflowChart");
}
if ($scope.nodeBeingEdited || $scope.placeholderNode) {
if ($scope.nodeBeingWorkedOn) {
$scope.cancelNodeForm();
}
if ($scope.linkBeingEdited) {
if ($scope.linkBeingEdited.parent.nodeId !== parentId || $scope.linkBeingEdited.child.nodeId !== childId) {
$scope.linkBeingEdited.parent.isLinkEditParent = false;
$scope.linkBeingEdited.child.isLinkEditChild = false;
if ($scope.linkBeingWorkedOn) {
if ($scope.linkBeingWorkedOn.parent.nodeId !== parentId || $scope.linkBeingWorkedOn.child.nodeId !== childId) {
$scope.linkBeingWorkedOn.parent.isLinkEditParent = false;
$scope.linkBeingWorkedOn.child.isLinkEditChild = false;
setupLinkEdit()
}
} else {
@ -955,34 +510,20 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
};
$scope.confirmLinkForm = (parentId, childId, edgeType) => {
$scope.linkBeingEdited.parent.isLinkEditParent = false;
$scope.linkBeingEdited.child.isLinkEditChild = false;
const parentNode = WorkflowService.searchTree({
element: $scope.treeData.data,
matchingId: parentId,
byNodeId: true
});
// Loop across children looking for childId
const childNode = _.find(parentNode.children, {'nodeId': childId});
childNode.edgeType = edgeType;
$scope.linkBeingEdited = null;
$scope.editLink = false;
$scope.confirmLinkForm = (newEdgeType) => {
$scope.linkBeingWorkedOn.parent.isLinkEditParent = false;
$scope.linkBeingWorkedOn.child.isLinkEditChild = false;
$scope.linkBeingWorkedOn.child.edgeType = newEdgeType;
$scope.linkBeingWorkedOn = null;
$scope.formState.showLinkForm = false;
$scope.$broadcast("refreshWorkflowChart");
}
$scope.cancelLinkForm = () => {
$scope.linkBeingEdited.parent.isLinkEditParent = false;
$scope.linkBeingEdited.child.isLinkEditChild = false;
$scope.linkBeingEdited = null;
$scope.editLink = false;
$scope.linkBeingWorkedOn.parent.isLinkEditParent = false;
$scope.linkBeingWorkedOn.child.isLinkEditChild = false;
$scope.linkBeingWorkedOn = null;
$scope.formState.showLinkForm = false;
$scope.$broadcast("refreshWorkflowChart");
};
@ -1005,7 +546,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.confirmDeleteNode = function () {
if ($scope.nodeToBeDeleted) {
if ($scope.linkBeingEdited) {
if ($scope.linkBeingWorkedOn) {
$scope.cancelLinkForm();
}
@ -1020,225 +561,16 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.treeData.data.deletedNodes.push($scope.nodeToBeDeleted.nodeId);
}
if ($scope.nodeToBeDeleted.isActiveEdit) {
resetNodeForm();
}
resetDeleteNode();
$scope.$broadcast("refreshWorkflowChart");
if ($scope.placeholderNode) {
let edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
if ($scope.placeholderNode.isRoot) {
updateEdgeDropdownOptions('always');
edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
} else {
updateEdgeDropdownOptions();
}
$scope.edgeType = edgeType;
} else if ($scope.nodeBeingEdited) {
switch ($scope.nodeBeingEdited.edgeType) {
case "always":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
if ($scope.nodeBeingEdited.isRoot) {
updateEdgeDropdownOptions('always');
} else {
updateEdgeDropdownOptions();
}
break;
case "success":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
updateEdgeDropdownOptions();
break;
case "failure":
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: "failure"
};
updateEdgeDropdownOptions();
break;
}
}
$scope.treeData.data.totalNodes--;
}
};
$scope.toggleFormTab = function (tab) {
if ($scope.workflowMakerFormConfig.activeTab !== tab) {
$scope.workflowMakerFormConfig.activeTab = tab;
}
};
function getEditNodeHelpMessage(workflowTemplate, selectedTemplate) {
if (selectedTemplate.type === "workflow_job_template") {
if (workflowTemplate.inventory) {
if (selectedTemplate.ask_inventory_on_launch) {
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
}
}
if (workflowTemplate.ask_inventory_on_launch) {
if (selectedTemplate.ask_inventory_on_launch) {
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
}
}
}
if (selectedTemplate.type === "job_template") {
if (workflowTemplate.inventory) {
if (selectedTemplate.ask_inventory_on_launch) {
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
}
return $scope.strings.get('workflow_maker.INVENTORY_WILL_NOT_OVERRIDE');
}
if (workflowTemplate.ask_inventory_on_launch) {
if (selectedTemplate.ask_inventory_on_launch) {
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
}
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_NOT_OVERRIDE');
}
}
return null;
}
$scope.templateManuallySelected = function (selectedTemplate) {
if (promptWatcher) {
promptWatcher();
}
if (surveyQuestionWatcher) {
surveyQuestionWatcher();
}
if (credentialsWatcher) {
credentialsWatcher();
}
$scope.promptData = null;
$scope.editNodeHelpMessage = getEditNodeHelpMessage($scope.treeData.workflow_job_template_obj, selectedTemplate);
if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") {
let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
$q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)])
.then((responses) => {
const launchConf = jobTemplate.getLaunchConf();
if (selectedTemplate.type === 'job_template') {
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
$scope.selectedTemplateInvalid = true;
} else {
$scope.selectedTemplateInvalid = false;
}
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
$scope.credentialRequiresPassword = true;
} else {
$scope.credentialRequiresPassword = false;
}
}
$scope.selectedTemplate = angular.copy(selectedTemplate);
if (jobTemplate.canLaunchWithoutPrompt()) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
if (['job_template', 'workflow_job_template'].includes(selectedTemplate.type)) {
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
$scope.promptModalMissingReqFields = true;
} else {
$scope.promptModalMissingReqFields = false;
}
} else {
$scope.promptModalMissingReqFields = false;
}
if (launchConf.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions(selectedTemplate.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.promptData = {
launchConf,
launchOptions: responses[0].data,
surveyQuestions: processed.surveyQuestions,
template: selectedTemplate.id,
templateType: selectedTemplate.type,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if (question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
watchForPromptChanges();
});
} else {
$scope.promptData = {
launchConf,
launchOptions: responses[0].data,
template: selectedTemplate.id,
templateType: selectedTemplate.type,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
watchForPromptChanges();
}
}
});
} else {
$scope.selectedTemplate = angular.copy(selectedTemplate);
$scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
}
};
$scope.toggleManualControls = function () {
$scope.toggleManualControls = function() {
$scope.showManualControls = !$scope.showManualControls;
};
@ -1268,10 +600,6 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.$broadcast('zoomToFitChart');
};
$scope.openPromptModal = function () {
$scope.promptData.triggerModalOpen = true;
};
let allNodes = [];
let page = 1;
@ -1310,11 +638,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
// This is the last page
buildTreeFromNodes();
}
}, function ({
data,
status,
config
}) {
}, function ({ data, status, config }) {
ProcessErrors($scope, data, status, null, {
hdr: $scope.strings.get('error.HEADER'),
msg: $scope.strings.get('error.CALL', {
@ -1327,7 +651,5 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
};
getNodes();
updateEdgeDropdownOptions();
}
];

View File

@ -84,71 +84,16 @@
<workflow-chart ng-if="modalOpen" tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" edit-link="startEditLink(parentId, childId)" delete-node="startDeleteNode(nodeToDelete)" workflow-zoomed="workflowZoomed(zoom)" can-add-workflow-job-template="canAddWorkflowJobTemplate" workflow-job-template-obj="workflowJobTemplateObj" mode="edit" class="WorkflowMaker-chart"></workflow-chart>
</div>
<div class="WorkflowMaker-contentRight">
<span ng-show="!editLink">
<div class="WorkflowMaker-formTitle">{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : strings.get('workflow_maker.EDIT_TEMPLATE')) : strings.get('workflow_maker.ADD_A_TEMPLATE')}}</div>
<div class="WorkflowMaker-formHelp" ng-show="workflowMakerFormConfig.nodeMode === 'idle'" ng-bind="treeData.data.totalNodes === 0 ? strings.get('workflow_maker.PLEASE_CLICK_THE_START_BUTTON') : strings.get('workflow_maker.PLEASE_HOVER_OVER_A_TEMPLATE')"></div>
<div class="WorkflowMaker-form" ng-show="workflowMakerFormConfig.nodeMode === 'add' || workflowMakerFormConfig.nodeMode === 'edit'">
<div class="Form-tabHolder">
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'jobs'}" ng-click="toggleFormTab('jobs')">{{strings.get('workflow_maker.JOBS')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'project_sync'}" ng-click="toggleFormTab('project_sync')">{{strings.get('workflow_maker.PROJECT_SYNC')}}</div>
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'inventory_sync'}" ng-click="toggleFormTab('inventory_sync')">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</div>
</div>
<div class="WorkflowMaker-formLists">
<div id="workflow-jobs-list" ui-view="jobTemplateList" ng-show="workflowMakerFormConfig.activeTab === 'jobs'"></div>
<div id="workflow-project-sync-list" ui-view="projectSyncList" ng-show="workflowMakerFormConfig.activeTab === 'project_sync'"></div>
<div id="workflow-inventory-sync-list" ui-view="inventorySyncList" ng-show="workflowMakerFormConfig.activeTab === 'inventory_sync'"></div>
</div>
<span ng-show="selectedTemplate &&
((selectedTemplate.type === 'job_template' && workflowMakerFormConfig.activeTab === 'jobs') ||
(selectedTemplate.type === 'project' && workflowMakerFormConfig.activeTab === 'project_sync') ||
(selectedTemplate.type === 'inventory_source' && workflowMakerFormConfig.activeTab === 'inventory_sync'))">
<div ng-if="selectedTemplate && selectedTemplateInvalid">
<div class="WorkflowMaker-invalidJobTemplateWarning">
<span class="fa fa-warning"></span>
<span>{{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }}</span>
</div>
</div>
<div ng-if="selectedTemplate && credentialRequiresPassword">
<div class="WorkflowMaker-invalidJobTemplateWarning">
<span class="fa fa-warning"></span>
<span>{{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}</span>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="selectedTemplate && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch) && workflowMakerFormConfig.nodeMode === 'add'">
<label for="verbosity" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: strings.get('workflow_maker.RUN') }}</span>
</label>
<div>
<select
id="workflow_node_edge"
ng-options="v as v.label for v in edgeTypeOptions track by v.value"
ng-model="edgeType"
class="form-control Form-dropDown"
name="edgeType"
tabindex="-1"
ng-disabled="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"
aria-hidden="true">
</select>
</div>
</div>
<div class="buttons Form-buttons" id="workflow_maker_controls">
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> {{:: strings.get('CANCEL') }}</button>
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_btn" ng-show="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch)" ng-click="confirmNodeForm()" ng-disabled="!selectedTemplate || promptModalMissingReqFields || credentialRequiresPassword"> {{:: strings.get('workflow_maker.SELECT') }}</button>
</div>
</span>
</div>
<span ng-if="formState.showNodeForm">
<workflow-node-form mode="nodeFormMode" node="nodeBeingWorkedOn" select="confirmNodeForm(selectedTemplate, promptData, edgeType)" cancel="cancelNodeForm()" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"/>
</span>
<span ng-if="editLink">
<workflow-link-form link-config="linkConfig" select="confirmLinkForm(parentId, childId, edgeType)" cancel="cancelLinkForm()"/>
<span ng-if="formState.showLinkForm">
<workflow-link-form link-config="linkConfig" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit" select="confirmLinkForm(edgeType)" cancel="cancelLinkForm()"/>
</span>
</div>
</div>
<div class="WorkflowMaker-buttonHolder">
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="workflowMakerFormConfig.nodeMode === 'add'"> {{:: strings.get('SAVE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="showNodeForm || showLinkForm"> {{:: strings.get('SAVE') }}</button>
</div>
<prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)"></prompt>
</div>

View File

@ -157,3 +157,10 @@
border-radius: 5px;
font-size: 11px;
}
.WorkflowResults-rightSide .WorkflowChart-svg {
background-color: @f6grey;
border: 1px solid @d7grey;
border-top: 0px;
border-bottom-right-radius: 5px;
}