mirror of
https://github.com/ansible/awx.git
synced 2026-01-17 12:41:19 -03:30
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:
commit
73f54b2237
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"; });
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
}
|
||||
];
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
8
awx/ui/test/e2e/commands/findThenClick.js
Normal file
8
awx/ui/test/e2e/commands/findThenClick.js
Normal file
@ -0,0 +1,8 @@
|
||||
exports.command = function findThenClick (selector) {
|
||||
this.waitForElementPresent(selector, () => {
|
||||
this.moveToElement(selector, 0, 0, () => {
|
||||
this.click(selector);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
};
|
||||
144
awx/ui/test/e2e/tests/test-workflow-visualizer.js
Normal file
144
awx/ui/test/e2e/tests/test-workflow-visualizer.js
Normal 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();
|
||||
}
|
||||
};
|
||||
@ -13,7 +13,7 @@ The CRUD operations against a workflow job template and its corresponding workfl
|
||||
### Workflow Nodes
|
||||
Workflow Nodes are containers of workflow spawned job resources and function as nodes of workflow decision trees. Like that of workflow itself, the two types of workflow nodes are workflow job template nodes and workflow job nodes.
|
||||
|
||||
Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parnent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.
|
||||
Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.
|
||||
|
||||
#### Workflow Node Launch Configuration
|
||||
|
||||
@ -30,12 +30,13 @@ the launch configurations on workflow nodes.
|
||||
The tree-graph structure of a workflow is enforced by associating workflow job template nodes via endpoints `/workflow_job_template_nodes/\d+/*_nodes/`, where `*` has options `success`, `failure` and `always`. However there are restrictions that must be enforced when setting up new connections. Here are the three restrictions that will raise validation error when break:
|
||||
* Cycle restriction: According to tree definition, no cycle is allowed.
|
||||
* Convergent restriction: Different paths should not come into the same node, in other words, a node cannot have multiple parents.
|
||||
* Mutex restriction: A node cannot have all three types of child nodes. It contains either always nodes only, or any type other than always nodes.
|
||||
|
||||
> Note: A node can now have all three types of child nodes.
|
||||
|
||||
### Workflow Run Details
|
||||
A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creats workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete.
|
||||
A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creates workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete.
|
||||
|
||||
As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expaneded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwritting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node.
|
||||
As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expanded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwriting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node.
|
||||
|
||||
Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing.
|
||||
|
||||
@ -84,7 +85,7 @@ Artifact support starts in Ansible and is carried through in Tower. The `set_sta
|
||||
* Verify that workflow job template nodes can be created under, or (dis)associated with workflow job templates.
|
||||
* Verify that only the permitted types of job template types can be associated with a workflow job template node. Currently the permitted types are *job templates, inventory sources and projects*.
|
||||
* Verify that workflow job template nodes under the same workflow job template can be associated to form parent-child relationship of decision trees. In specific, one node takes another as its child node by POSTing another node's id to one of the three endpoints: `/success_nodes/`, `/failure_nodes/` and `/always_nodes/`.
|
||||
* Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent) and mutex('always' XOR the rest).
|
||||
* Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent).
|
||||
* Verify that a workflow job template can be successfully copied and the created workflow job template does not miss any field that should be copied or intentionally modified.
|
||||
* Verify that if a user has no access to any of the related resources of a workflow job template node, that node will not be copied and will have `null` as placeholder.
|
||||
* Verify that `artifacts` is populated when `set_stats` is used in Ansible >= v2.2.1.0-0.3.rc3.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user