Merge pull request #2373 from marshmalien/always_nodes_ui

Display WF always nodes in conjunction with success and failure

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot]
2018-10-12 22:43:32 +00:00
committed by GitHub
11 changed files with 444 additions and 434 deletions

View File

@@ -3490,10 +3490,6 @@ class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, Enforce
return getattr(parent, self.relationship).all()
def is_valid_relation(self, parent, sub, created=False):
mutex_list = ('success_nodes', 'failure_nodes') if self.relationship == 'always_nodes' else ('always_nodes',)
for relation in mutex_list:
if getattr(parent, relation).all().exists():
return {'Error': _('Cannot associate {0} when {1} have been associated.').format(self.relationship, relation)}
if created:
return None

View File

@@ -7,6 +7,7 @@ from awx.main.models.workflow import WorkflowJob, WorkflowJobNode, WorkflowJobTe
from awx.main.models.jobs import JobTemplate, Job
from awx.main.models.projects import ProjectUpdate
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.versioning import reverse
# Django
from django.test import TransactionTestCase
@@ -196,9 +197,15 @@ class TestWorkflowJobTemplate:
assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple parent relationship not allowed.'}
# test mutex validation
test_view.relationship = 'failure_nodes'
node_assoc_1 = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
assert (test_view.is_valid_relation(nodes[2], node_assoc_1) ==
{'Error': 'Cannot associate failure_nodes when always_nodes have been associated.'})
def test_always_success_failure_creation(self, wfjt, admin, get):
wfjt_node = wfjt.workflow_job_template_nodes.all()[1]
node = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
wfjt_node.always_nodes.add(node)
assert len(node.get_parent_nodes()) == 1
url = reverse('api:workflow_job_template_node_list') + str(wfjt_node.id) + '/'
resp = get(url, admin)
assert node.id in resp.data['always_nodes']
def test_wfjt_unique_together_with_org(self, organization):
wfjt1 = WorkflowJobTemplate(name='foo', organization=organization)

View File

@@ -112,7 +112,6 @@ function TemplatesStrings (BaseString) {
RUN: t.s('RUN'),
CHECK: t.s('CHECK'),
SELECT: t.s('SELECT'),
EDGE_CONFLICT: t.s('EDGE CONFLICT'),
DELETED: t.s('DELETED'),
START: t.s('START'),
DETAILS: t.s('DETAILS'),

View File

@@ -102,13 +102,6 @@
width: 90px;
color: @default-interface-txt;
}
.WorkflowChart-conflictIcon {
color: @default-err;
}
.WorkflowChart-conflictText {
width: 90px;
color: @default-interface-txt;
}
.WorkflowChart-activeNode {
fill: @default-link;
}

View File

@@ -327,16 +327,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
}).each(wrap);
thisNode.append("foreignObject")
.attr("x", 54)
.attr("y", 45)
.style("font-size","0.7em")
.attr("class", "WorkflowChart-conflictText")
.html(function () {
return `<span class=\"WorkflowChart-conflictIcon\">\uf06a</span><span> ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}</span>`;
})
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
thisNode.append("foreignObject")
.attr("x", 62)
.attr("y", 22)
@@ -831,9 +821,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
t.selectAll(".WorkflowChart-deletedText")
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
t.selectAll(".WorkflowChart-conflictText")
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; });

View File

@@ -4,13 +4,12 @@
* All Rights Reserved
*************************************************/
export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
'$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
export default ['$scope', 'WorkflowService', 'TemplatesService',
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
'i18n',
function($scope, WorkflowService, GetBasePath, TemplatesService,
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) {
function ($scope, WorkflowService, TemplatesService,
ProcessErrors, CreateSelect2, $q, JobTemplate,
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
@@ -31,24 +30,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
value: "check"
}];
$scope.edgeFlags = {
conflict: false
};
$scope.edgeTypeOptions = [
{
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'
}
];
$scope.edgeTypeOptions = createEdgeTypeOptions();
let editRequests = [];
let associateRequests = [];
@@ -59,6 +41,22 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', '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;
@@ -74,7 +72,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// params.parentId
// params.node
let buildSendableNodeData = function() {
let buildSendableNodeData = function () {
// Create the node
let sendableNodeData = {
unified_job_template: params.node.unifiedJobTemplate.id,
@@ -122,7 +120,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
return sendableNodeData;
};
let continueRecursing = function(parentId) {
let continueRecursing = function (parentId) {
$scope.totalIteratedNodes++;
if ($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) {
@@ -130,7 +128,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
completionCallback();
} else {
if (params.node.children && params.node.children.length > 0) {
_.forEach(params.node.children, function(child) {
_.forEach(params.node.children, function (child) {
if (child.edgeType === "success") {
recursiveNodeUpdates({
parentId: parentId,
@@ -155,49 +153,51 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
if (params.node.isNew) {
TemplatesService.addWorkflowNode({
url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes,
data: buildSendableNodeData()
})
.then(function(data) {
url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes,
data: buildSendableNodeData()
})
.then(function (data) {
if (!params.node.isRoot) {
associateRequests.push({
parentId: params.parentId,
nodeId: data.data.id,
edge: params.node.edgeType
if (!params.node.isRoot) {
associateRequests.push({
parentId: params.parentId,
nodeId: data.data.id,
edge: params.node.edgeType
});
}
if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
// This finds the credentials that were selected in the prompt but don't occur
// in the template defaults
let credentialsToPost = params.node.promptData.prompts.credentials.value.filter(function (credFromPrompt) {
let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []);
return !defaultCreds.some(function (defaultCred) {
return credFromPrompt.id === defaultCred.id;
});
});
credentialsToPost.forEach((credentialToPost) => {
credentialRequests.push({
id: data.data.id,
data: {
id: credentialToPost.id
}
});
});
}
params.node.isNew = false;
continueRecursing(data.data.id);
}, function ({ data, config, status }) {
ProcessErrors($scope, data, status, null, {
hdr: $scope.strings.get('error.HEADER'),
msg: $scope.strings.get('error.CALL', {
path: `${config.url}`,
action: `${config.method}`,
status
})
});
}
if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){
// This finds the credentials that were selected in the prompt but don't occur
// in the template defaults
let credentialsToPost = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) {
let defaultCreds = params.node.promptData.launchConf.defaults.credentials ? params.node.promptData.launchConf.defaults.credentials : [];
return !defaultCreds.some(function(defaultCred) {
return credFromPrompt.id === defaultCred.id;
});
});
credentialsToPost.forEach((credentialToPost) => {
credentialRequests.push({
id: data.data.id,
data: {
id: credentialToPost.id
}
});
});
}
params.node.isNew = false;
continueRecursing(data.data.id);
}, function(error) {
ProcessErrors($scope, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to add workflow node. ' +
'POST returned status: ' +
error.status
});
});
} else {
if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
@@ -208,56 +208,56 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
data: buildSendableNodeData()
});
if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){
let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) {
let defaultCreds = params.node.promptData.launchConf.defaults.credentials ? params.node.promptData.launchConf.defaults.credentials : [];
return !defaultCreds.some(function(defaultCred) {
return credFromPrompt.id === defaultCred.id;
});
});
if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function (credFromPrompt) {
let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []);
return !defaultCreds.some(function (defaultCred) {
return credFromPrompt.id === defaultCred.id;
});
});
let credentialsToAdd = credentialsNotInPriorCredentials.filter(function(credNotInPrior) {
let previousOverrides = params.node.promptData.prompts.credentials.previousOverrides ? params.node.promptData.prompts.credentials.previousOverrides : [];
return !previousOverrides.some(function(priorCred) {
return credNotInPrior.id === priorCred.id;
});
});
let credentialsToAdd = credentialsNotInPriorCredentials.filter(function (credNotInPrior) {
let previousOverrides = _.get(params, 'node.promptData.prompts.credentials.previousOverrides', []);
return !previousOverrides.some(function (priorCred) {
return credNotInPrior.id === priorCred.id;
});
});
let credentialsToRemove = [];
let credentialsToRemove = [];
if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) {
credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function(priorCred) {
return !credentialsNotInPriorCredentials.some(function(credNotInPrior) {
return priorCred.id === credNotInPrior.id;
});
});
}
if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) {
credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function (priorCred) {
return !credentialsNotInPriorCredentials.some(function (credNotInPrior) {
return priorCred.id === credNotInPrior.id;
});
});
}
credentialsToAdd.forEach((credentialToAdd) => {
credentialRequests.push({
id: params.node.nodeId,
data: {
id: credentialToAdd.id
}
});
});
credentialsToAdd.forEach((credentialToAdd) => {
credentialRequests.push({
id: params.node.nodeId,
data: {
id: credentialToAdd.id
}
});
});
credentialsToRemove.forEach((credentialToRemove) => {
credentialRequests.push({
id: params.node.nodeId,
data: {
id: credentialToRemove.id,
disassociate: true
}
});
});
}
credentialsToRemove.forEach((credentialToRemove) => {
credentialRequests.push({
id: params.node.nodeId,
data: {
id: credentialToRemove.id,
disassociate: true
}
});
});
}
}
if (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType)) {
let parentIsDeleted = false;
_.forEach($scope.treeData.data.deletedNodes, function(deletedNode) {
_.forEach($scope.treeData.data.deletedNodes, function (deletedNode) {
if (deletedNode === params.node.originalParentId) {
parentIsDeleted = true;
}
@@ -297,44 +297,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
}
let updateEdgeDropdownOptions = (optionsToInclude) => {
// Not passing optionsToInclude will include all by default
if (!optionsToInclude) {
$scope.edgeTypeOptions = [
{
label: i18n._('Always'),
value: 'always'
},
{
label: i18n._('On Success'),
value: 'success'
},
{
label: i18n._('On Failure'),
value: 'failure'
}
];
} else {
$scope.edgeTypeOptions = [];
let updateEdgeDropdownOptions = (edgeTypeValue) => {
// Not passing an edgeTypeValue will include all by default
optionsToInclude.forEach((optionToInclude) => {
if (optionToInclude === "always") {
$scope.edgeTypeOptions.push({
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: 'always'
});
} else if (optionToInclude === "success") {
$scope.edgeTypeOptions.push({
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: 'success'
});
} else if (optionToInclude === "failure") {
$scope.edgeTypeOptions.push({
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: 'failure'
});
}
if (edgeTypeValue) {
$scope.edgeTypeOptions = _.filter(createEdgeTypeOptions(), {
'value': edgeTypeValue
});
} else {
$scope.edgeTypeOptions = createEdgeTypeOptions();
}
CreateSelect2({
@@ -347,9 +318,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => {
if ((credential.passwords_needed &&
credential.passwords_needed.length > 0) ||
credential.passwords_needed.length > 0) ||
(_.has(credential, 'inputs.vault_password') &&
credential.inputs.vault_password === "ASK")
credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
@@ -364,7 +335,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
'missingSurveyValue'
];
promptWatcher = $scope.$watchGroup(promptDataToWatch, function() {
promptWatcher = $scope.$watchGroup(promptDataToWatch, function () {
let missingPromptValue = false;
if ($scope.missingSurveyValue) {
missingPromptValue = true;
@@ -381,20 +352,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
};
$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();
};
$scope.saveWorkflowMaker = function() {
$scope.saveWorkflowMaker = function () {
$scope.totalIteratedNodes = 0;
if ($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) {
let completionCallback = function() {
let completionCallback = function () {
let disassociatePromises = disassociateRequests.map(function(request) {
let disassociatePromises = disassociateRequests.map(function (request) {
return TemplatesService.disassociateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
@@ -402,67 +373,68 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
});
});
let editNodePromises = editRequests.map(function(request) {
let editNodePromises = editRequests.map(function (request) {
return TemplatesService.editWorkflowNode({
id: request.id,
data: request.data
});
});
let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
let deletePromises = $scope.treeData.data.deletedNodes.map(function (nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(disassociatePromises.concat(editNodePromises, deletePromises))
.then(function() {
.then(function () {
let credentialPromises = credentialRequests.map(function(request) {
return TemplatesService.postWorkflowNodeCredential({
id: request.id,
data: request.data
let credentialPromises = credentialRequests.map(function (request) {
return TemplatesService.postWorkflowNodeCredential({
id: request.id,
data: request.data
});
});
});
let associatePromises = associateRequests.map(function(request) {
return TemplatesService.associateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
edge: request.edge
let associatePromises = associateRequests.map(function (request) {
return TemplatesService.associateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
edge: request.edge
});
});
});
$q.all(associatePromises.concat(credentialPromises))
.then(function() {
$scope.closeDialog();
}).catch(({data, status}) => {
return $q.all(associatePromises.concat(credentialPromises))
.then(function () {
$scope.closeDialog();
});
}).catch(({
data,
status
}) => {
ProcessErrors($scope, data, status, null, {});
});
}).catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {});
});
};
_.forEach($scope.treeData.data.children, function(child) {
_.forEach($scope.treeData.data.children, function (child) {
recursiveNodeUpdates({
node: child
}, completionCallback);
});
} else {
let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
let deletePromises = $scope.treeData.data.deletedNodes.map(function (nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(deletePromises)
.then(function() {
$scope.closeDialog();
});
.then(function () {
$scope.closeDialog();
});
}
};
/* ADD NODE FUNCTIONS */
$scope.startAddNode = function(parent, betweenTwoNodes) {
$scope.startAddNode = function (parent, betweenTwoNodes) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
$scope.cancelNodeForm();
@@ -481,41 +453,29 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.treeData.nextIndex++;
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
tree: $scope.treeData.data,
parentId: betweenTwoNodes ? parent.source.id : parent.id,
childId: $scope.placeholderNode.id
});
// Set the default to success
let edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"};
let edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) {
// We don't want to give the user the option to select
// a type as this node will always be executed
updateEdgeDropdownOptions(["always"]);
edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"};
// This node will always be executed
updateEdgeDropdownOptions('always');
edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
} else {
if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
updateEdgeDropdownOptions(["success", "failure"]);
edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"};
} else if (_.includes(siblingConnectionTypes, "always")) {
updateEdgeDropdownOptions(["always"]);
edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"};
} else {
updateEdgeDropdownOptions();
}
updateEdgeDropdownOptions();
}
// Reset the edgeConflict flag
resetEdgeConflict();
$scope.edgeType = edgeType;
$scope.$broadcast("refreshWorkflowChart");
};
$scope.confirmNodeForm = function() {
$scope.confirmNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
@@ -565,13 +525,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.promptData = null;
// Reset the edgeConflict flag
resetEdgeConflict();
$scope.$broadcast("refreshWorkflowChart");
};
$scope.cancelNodeForm = function() {
$scope.cancelNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
// Remove the placeholder node from the tree
WorkflowService.removeNodeFromTree({
@@ -598,9 +555,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false;
// Reset the edgeConflict flag
resetEdgeConflict();
// Reset the form
resetNodeForm();
@@ -609,7 +563,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
/* EDIT NODE FUNCTIONS */
$scope.startEditNode = function(nodeToEdit) {
$scope.startEditNode = function (nodeToEdit) {
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
@@ -636,7 +590,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.nodeBeingEdited.isActiveEdit = true;
let finishConfiguringEdit = function() {
let finishConfiguringEdit = function () {
let jobTemplate = new JobTemplate();
@@ -656,8 +610,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -729,7 +683,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false;
prompts.credentials.value.forEach((credential) => {
if(credential.inputs) {
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") ||
@@ -756,8 +710,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -816,7 +770,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
watchForPromptChanges();
}
}
});
});
}
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) {
@@ -855,32 +809,30 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.workflowMakerFormConfig.activeTab = "jobs";
}
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
tree: $scope.treeData.data,
parentId: parent.id,
childId: nodeToEdit.id
});
let edgeDropdownOptions = null;
let edgeDropdownOptions = null;
switch($scope.nodeBeingEdited.edgeType) {
// Select RUN dropdown option
switch ($scope.nodeBeingEdited.edgeType) {
case "always":
$scope.edgeType = {label: i18n._("Always"), value: "always"};
if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) {
edgeDropdownOptions = ["always"];
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
if ($scope.nodeBeingEdited.isRoot) {
edgeDropdownOptions = 'always';
}
break;
case "success":
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
edgeDropdownOptions = ["success", "failure"];
}
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
break;
case "failure":
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
edgeDropdownOptions = ["success", "failure"];
}
$scope.edgeType = {
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
value: "failure"
};
break;
}
@@ -897,15 +849,18 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
.then(function(data) {
.then(function (data) {
$scope.nodeBeingEdited.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
});
}, 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();
@@ -922,16 +877,16 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.deleteOverlayVisible = false;
}
$scope.startDeleteNode = function(nodeToDelete) {
$scope.startDeleteNode = function (nodeToDelete) {
$scope.nodeToBeDeleted = nodeToDelete;
$scope.deleteOverlayVisible = true;
};
$scope.cancelDeleteNode = function() {
$scope.cancelDeleteNode = function () {
resetDeleteNode();
};
$scope.confirmDeleteNode = function() {
$scope.confirmDeleteNode = function () {
if ($scope.nodeToBeDeleted) {
// TODO: turn this into a promise so that we can handle errors
@@ -949,95 +904,56 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
resetNodeForm();
}
// Reset the edgeConflict flag
resetEdgeConflict();
resetDeleteNode();
$scope.$broadcast("refreshWorkflowChart");
if ($scope.placeholderNode) {
let edgeType = {label: "On Success", value: "success"};
let edgeType = {
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
value: "success"
};
if ($scope.placeholderNode.isRoot) {
updateEdgeDropdownOptions(["always"]);
edgeType = {label: "Always", value: "always"};
} else {
// we need to update the possible edges based on any new siblings
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
tree: $scope.treeData.data,
parentId: $scope.placeholderNode.parent.id,
childId: $scope.placeholderNode.id
});
if (
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
!_.includes(siblingConnectionTypes, "always")
) {
updateEdgeDropdownOptions(["success", "failure"]);
} else if (
_.includes(siblingConnectionTypes, "always") &&
!_.includes(siblingConnectionTypes, "success") &&
!_.includes(siblingConnectionTypes, "failure")
) {
updateEdgeDropdownOptions(["always"]);
edgeType = {label: "Always", value: "always"};
} else {
updateEdgeDropdownOptions();
}
}
$scope.edgeType = edgeType;
} else if ($scope.nodeBeingEdited) {
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
tree: $scope.treeData.data,
parentId: $scope.nodeBeingEdited.parent.id,
childId: $scope.nodeBeingEdited.id
});
if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
updateEdgeDropdownOptions(["success", "failure"]);
} else if (_.includes(siblingConnectionTypes, "always") && $scope.nodeBeingEdited.edgeType === "always") {
updateEdgeDropdownOptions(["always"]);
updateEdgeDropdownOptions('always');
edgeType = {
label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
} else {
updateEdgeDropdownOptions();
}
switch($scope.nodeBeingEdited.edgeType) {
case "always":
$scope.edgeType = {label: i18n._("Always"), value: "always"};
if (
_.includes(siblingConnectionTypes, "always") &&
!_.includes(siblingConnectionTypes, "success") &&
!_.includes(siblingConnectionTypes, "failure")
) {
updateEdgeDropdownOptions(["always"]);
} else {
updateEdgeDropdownOptions();
}
break;
case "success":
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
if (
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
!_.includes(siblingConnectionTypes, "always")
) {
updateEdgeDropdownOptions(["success", "failure"]);
} else {
updateEdgeDropdownOptions();
}
break;
case "failure":
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
if (
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
!_.includes(siblingConnectionTypes, "always")
) {
updateEdgeDropdownOptions(["success", "failure"]);
} else {
updateEdgeDropdownOptions();
}
break;
}
$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--;
@@ -1045,13 +961,13 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
};
$scope.toggleFormTab = function(tab) {
$scope.toggleFormTab = function (tab) {
if ($scope.workflowMakerFormConfig.activeTab !== tab) {
$scope.workflowMakerFormConfig.activeTab = tab;
}
};
$scope.templateManuallySelected = function(selectedTemplate) {
$scope.templateManuallySelected = function (selectedTemplate) {
if (promptWatcher) {
promptWatcher();
@@ -1100,8 +1016,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
$scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -1168,56 +1084,47 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
};
function resetEdgeConflict(){
$scope.edgeFlags.conflict = false;
WorkflowService.checkForEdgeConflicts({
treeData: $scope.treeData.data,
edgeFlags: $scope.edgeFlags
});
}
$scope.toggleManualControls = function() {
$scope.toggleManualControls = function () {
$scope.showManualControls = !$scope.showManualControls;
};
$scope.panChart = function(direction) {
$scope.panChart = function (direction) {
$scope.$broadcast('panWorkflowChart', {
direction: direction
});
};
$scope.zoomChart = function(zoom) {
$scope.zoomChart = function (zoom) {
$scope.$broadcast('zoomWorkflowChart', {
zoom: zoom
});
};
$scope.resetChart = function() {
$scope.resetChart = function () {
$scope.$broadcast('resetWorkflowChart');
};
$scope.workflowZoomed = function(zoom) {
$scope.workflowZoomed = function (zoom) {
$scope.$broadcast('workflowZoomed', {
zoom: zoom
});
};
$scope.zoomToFitChart = function() {
$scope.zoomToFitChart = function () {
$scope.$broadcast('zoomToFitChart');
};
$scope.openPromptModal = function() {
$scope.openPromptModal = function () {
$scope.promptData.triggerModalOpen = true;
};
let allNodes = [];
let page = 1;
let buildTreeFromNodes = function(){
let buildTreeFromNodes = function () {
WorkflowService.buildTree({
workflowNodes: allNodes
}).then(function(data){
}).then(function (data) {
$scope.treeData = data;
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
@@ -1234,33 +1141,35 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
});
};
let getNodes = function(){
let getNodes = function () {
// Get the workflow nodes
TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page)
.then(function(data){
for(var i=0; i<data.data.results.length; i++) {
allNodes.push(data.data.results[i]);
}
if (data.data.next) {
// Get the next page
page++;
getNodes();
} else {
// This is the last page
buildTreeFromNodes();
}
}, function(error){
ProcessErrors($scope, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to get workflow job template nodes. GET returned ' +
'status: ' + error.status
.then(function (data) {
for (var i = 0; i < data.data.results.length; i++) {
allNodes.push(data.data.results[i]);
}
if (data.data.next) {
// Get the next page
page++;
getNodes();
} else {
// This is the last page
buildTreeFromNodes();
}
}, 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
})
});
});
});
};
getNodes();
updateEdgeDropdownOptions();
}
];

View File

@@ -139,7 +139,7 @@
</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="edgeFlags.conflict || 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="workflowMakerFormConfig.nodeMode === 'add'"> {{:: 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

@@ -290,39 +290,5 @@ export default ['$q', function($q){
}
},
checkForEdgeConflicts: function(params) {
//params.treeData
//params.edgeFlags
let hasAlways = false;
let hasSuccessFailure = false;
let _this = this;
_.forEach(params.treeData.children, function(child) {
// Flip the flag to false for now - we'll set it to true later on
// if we detect a conflict
child.edgeConflict = false;
if(child.edgeType === 'always') {
hasAlways = true;
}
else if(child.edgeType === 'success' || child.edgeType === 'failure') {
hasSuccessFailure = true;
}
_this.checkForEdgeConflicts({
treeData: child,
edgeFlags: params.edgeFlags
});
});
if(hasAlways && hasSuccessFailure) {
// We have a conflict
_.forEach(params.treeData.children, function(child) {
child.edgeConflict = true;
});
params.edgeFlags.conflict = true;
}
}
};
}];

View File

@@ -0,0 +1,8 @@
exports.command = function findThenClick (selector) {
this.waitForElementPresent(selector, () => {
this.moveToElement(selector, 0, 0, () => {
this.click(selector);
});
});
return this;
};

View File

@@ -0,0 +1,144 @@
import {
getInventorySource,
getJobTemplate,
getProject,
getWorkflowTemplate
} from '../fixtures';
let data;
const spinny = "//*[contains(@class, 'spinny')]";
const workflowTemplateNavTab = "//at-side-nav-item[contains(@name, 'TEMPLATES')]";
const workflowSelector = "//a[contains(text(), 'test-actions-workflow-template')]";
const workflowVisualizerBtn = "//button[contains(@id, 'workflow_job_template_workflow_visualizer_btn')]";
const rootNode = "//*[@id='node-2']";
const childNode = "//*[@id='node-3']";
const newChildNode = "//*[@id='node-5']";
const leafNode = "//*[@id='node-6']";
const nodeAdd = "//*[contains(@class, 'nodeAddCross')]";
const nodeRemove = "//*[contains(@class, 'nodeRemoveCross')]";
// one of the jobs or projects or inventories
const testActionsProject = "//td[contains(text(), 'test-actions-project')]";
const testActionsJob = "//td[contains(text(), 'test-actions-job')]";
// dropdown bar which lets you select edge type
const edgeTypeDropdownBar = "//span[contains(@id, 'select2-workflow_node_edge-container')]";
const alwaysDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='Always']";
const successDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='On Success']";
const failureDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='On Failure']";
const selectButton = "//*[@id='workflow_maker_select_btn']";
const deleteConfirmation = "//button[@ng-click='confirmDeleteNode()']";
module.exports = {
before: (client, done) => {
const resources = [
getInventorySource('test-actions'),
getJobTemplate('test-actions'),
getProject('test-actions'),
getWorkflowTemplate('test-actions'),
];
Promise.all(resources)
.then(([source, template, project, workflow]) => {
data = { source, template, project, workflow };
done();
});
client
.login()
.waitForAngular()
.resizeWindow(1200, 1000)
.useXpath()
.findThenClick(workflowTemplateNavTab)
.pause(1500)
.waitForElementNotVisible(spinny)
.findThenClick(workflowSelector)
.findThenClick(workflowVisualizerBtn);
},
'verify that workflow visualizer root node can only be set to always': client => {
client
.useXpath()
.findThenClick(rootNode)
.findThenClick(testActionsProject)
.findThenClick(edgeTypeDropdownBar)
.waitForElementNotPresent(successDropdown)
.waitForElementNotPresent(failureDropdown)
.waitForElementPresent(alwaysDropdown);
},
'verify that a non-root node can be set to always/success/failure': client => {
client
.useXpath()
.findThenClick(childNode)
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(edgeTypeDropdownBar)
.waitForElementPresent(successDropdown)
.waitForElementPresent(failureDropdown)
.waitForElementPresent(alwaysDropdown)
.findThenClick(edgeTypeDropdownBar);
},
'verify that a sibling node can be any edge type': client => {
client
.useXpath()
.moveToElement(childNode, 0, 0, () => {
client.pause(500);
client.waitForElementNotVisible(spinny);
// Concatenating the xpaths lets us click the proper node
client.click(childNode + nodeAdd);
})
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(testActionsJob)
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(edgeTypeDropdownBar)
.waitForElementPresent(successDropdown)
.waitForElementPresent(failureDropdown)
.waitForElementPresent(alwaysDropdown)
.findThenClick(alwaysDropdown)
.click(selectButton);
},
'Verify node-shifting behavior upon deletion': client => {
client
.findThenClick(newChildNode)
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(edgeTypeDropdownBar)
.findThenClick(successDropdown)
.click(selectButton)
.moveToElement(newChildNode, 0, 0, () => {
client.pause(500);
client.waitForElementNotVisible(spinny);
client.click(newChildNode + nodeAdd);
})
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(testActionsJob)
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(edgeTypeDropdownBar)
.waitForElementPresent(successDropdown)
.waitForElementPresent(failureDropdown)
.waitForElementPresent(alwaysDropdown)
.findThenClick(alwaysDropdown)
.click(selectButton)
.moveToElement(newChildNode, 0, 0, () => {
client.pause(500);
client.waitForElementNotVisible(spinny);
client.click(newChildNode + nodeRemove);
})
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(deleteConfirmation)
.findThenClick(leafNode)
.pause(1000)
.waitForElementNotVisible(spinny)
.findThenClick(edgeTypeDropdownBar)
.waitForElementPresent(successDropdown)
.waitForElementPresent(failureDropdown)
.waitForElementPresent(alwaysDropdown);
},
after: client => {
client.end();
}
};