mirror of
https://github.com/ansible/awx.git
synced 2026-05-16 05:47:38 -02:30
Add and edit workflow job templates from the UI
This commit is contained in:
@@ -184,6 +184,7 @@
|
|||||||
.Form-formGroup--fullWidth {
|
.Form-formGroup--fullWidth {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
padding-right: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-formGroup--checkbox{
|
.Form-formGroup--checkbox{
|
||||||
@@ -553,19 +554,24 @@ input[type='radio']:checked:before {
|
|||||||
color: @btn-txt;
|
color: @btn-txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-surveyButton {
|
.Form-primaryButton {
|
||||||
background-color: @default-link;
|
background-color: @default-link;
|
||||||
color: @default-bg;
|
color: @default-bg;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
padding-left:15px;
|
padding-left:15px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-surveyButton:hover{
|
.Form-primaryButton:hover {
|
||||||
background-color: @default-link-hov;
|
background-color: @default-link-hov;
|
||||||
color: @default-bg;
|
color: @default-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Form-primaryButton.Form-tab--disabled:hover {
|
||||||
|
background-color: @default-link;
|
||||||
|
}
|
||||||
|
|
||||||
.Form-formGroup--singleColumn {
|
.Form-formGroup--singleColumn {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
|
|||||||
@@ -357,6 +357,33 @@ table, tbody {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.List-dropdownButton {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.List-dropdownSuccess {
|
||||||
|
background-color: @submit-button-bg;
|
||||||
|
color: @submit-button-text;
|
||||||
|
border-color: @submit-button-bg-hov;
|
||||||
|
}
|
||||||
|
|
||||||
|
.List-dropdownSuccess:hover,
|
||||||
|
.List-dropdownSuccess:focus {
|
||||||
|
color: @submit-button-text;
|
||||||
|
background-color: @submit-button-bg-hov;
|
||||||
|
}
|
||||||
|
|
||||||
|
.List-dropdownCarat {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-top: 4px dashed;
|
||||||
|
border-top: 4px solid\9;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
.List-searchWidget + .List-searchWidget {
|
.List-searchWidget + .List-searchWidget {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ var tower = angular.module('Tower', [
|
|||||||
'ActivityStreamHelper',
|
'ActivityStreamHelper',
|
||||||
'gettext',
|
'gettext',
|
||||||
'I18N',
|
'I18N',
|
||||||
|
'WorkflowFormDefinition',
|
||||||
|
'InventorySourcesListDefinition',
|
||||||
|
'WorkflowMakerFormDefinition'
|
||||||
])
|
])
|
||||||
|
|
||||||
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
|
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
export default
|
export default
|
||||||
[ 'InitiatePlaybookRun',
|
[ 'InitiatePlaybookRun',
|
||||||
'templateUrl',
|
'templateUrl',
|
||||||
'$location',
|
'$state',
|
||||||
function JobTemplatesList(InitiatePlaybookRun, templateUrl, $location) {
|
function JobTemplatesList(InitiatePlaybookRun, templateUrl, $state) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: link,
|
link: link,
|
||||||
@@ -47,7 +47,7 @@ export default
|
|||||||
};
|
};
|
||||||
|
|
||||||
scope.editJobTemplate = function (jobTemplateId) {
|
scope.editJobTemplate = function (jobTemplateId) {
|
||||||
$location.path( '/job_templates/' + jobTemplateId);
|
$state.go('templates.editJobTemplate', {id: jobTemplateId});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h3 class="DashboardList-headerText">
|
<h3 class="DashboardList-headerText">
|
||||||
<translate>RECENTLY USED JOB TEMPLATES</translate>
|
<translate>RECENTLY USED JOB TEMPLATES</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<a href="/#/job_templates" class="DashboardList-viewAll">
|
<a href="/#/templates" class="DashboardList-viewAll">
|
||||||
<translate>VIEW ALL</translate>
|
<translate>VIEW ALL</translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
ng-class-even="'List-tableRow--evenRow'"
|
ng-class-even="'List-tableRow--evenRow'"
|
||||||
ng-repeat = "job_template in job_templates">
|
ng-repeat = "job_template in job_templates">
|
||||||
<td class="DashboardList-nameCell">
|
<td class="DashboardList-nameCell">
|
||||||
<a href="#/job_templates/{{ job_template.id }}" class="DashboardList-nameContainer">
|
<a href="#/templates/{{ job_template.id }}" class="DashboardList-nameContainer">
|
||||||
{{ job_template.name }}
|
{{ job_template.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="DashboardList-container">
|
<div class="DashboardList-container">
|
||||||
<p class="DashboardList-noJobs" translate>No job templates were recently used.<br />
|
<p class="DashboardList-noJobs">No job templates were recently used.<br />
|
||||||
You can create a job template <a href="#/job_templates/add">here</a>.</p>
|
You can create a job template <a href="#/templates/add">here</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import ProjectStatus from "./forms/ProjectStatus";
|
|||||||
import Projects from "./forms/Projects";
|
import Projects from "./forms/Projects";
|
||||||
import Teams from "./forms/Teams";
|
import Teams from "./forms/Teams";
|
||||||
import Users from "./forms/Users";
|
import Users from "./forms/Users";
|
||||||
|
import WorkflowMaker from "./forms/WorkflowMaker";
|
||||||
|
import Workflows from "./forms/Workflows";
|
||||||
|
|
||||||
|
|
||||||
export
|
export
|
||||||
@@ -46,5 +48,7 @@ export
|
|||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
Projects,
|
Projects,
|
||||||
Teams,
|
Teams,
|
||||||
Users
|
Users,
|
||||||
|
WorkflowMaker,
|
||||||
|
Workflows
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ export default
|
|||||||
addTitle: i18n._('New Job Template'),
|
addTitle: i18n._('New Job Template'),
|
||||||
editTitle: '{{ name }}',
|
editTitle: '{{ name }}',
|
||||||
name: 'job_template',
|
name: 'job_template',
|
||||||
|
breadcrumbName: 'JOB TEMPLATE',
|
||||||
basePath: 'job_templates',
|
basePath: 'job_templates',
|
||||||
// the top-most node of generated state tree
|
// the top-most node of generated state tree
|
||||||
stateTree: 'jobTemplates',
|
stateTree: 'templates',
|
||||||
tabs: true,
|
tabs: true,
|
||||||
|
activeEditState: 'templates.editJobTemplate',
|
||||||
// (optional) array of supporting templates to ng-include inside generated html
|
// (optional) array of supporting templates to ng-include inside generated html
|
||||||
include: ['/static/partials/survey-maker-modal.html'],
|
include: ['/static/partials/survey-maker-modal.html'],
|
||||||
|
|
||||||
|
|||||||
154
awx/ui/client/src/forms/WorkflowMaker.js
Normal file
154
awx/ui/client/src/forms/WorkflowMaker.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name forms.function:JobTemplate
|
||||||
|
* @description This form is for adding/editing a Job Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('WorkflowMakerFormDefinition', [])
|
||||||
|
|
||||||
|
.value ('WorkflowMakerFormObject', {
|
||||||
|
|
||||||
|
addTitle: '',
|
||||||
|
editTitle: '',
|
||||||
|
name: 'workflow_maker',
|
||||||
|
base: 'job_templates',
|
||||||
|
tabs: false,
|
||||||
|
cancelButton: false,
|
||||||
|
showHeader: false,
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
credential: {
|
||||||
|
label: 'Credential',
|
||||||
|
type: 'lookup',
|
||||||
|
sourceModel: 'credential',
|
||||||
|
sourceField: 'name',
|
||||||
|
ngClick: 'lookUpCredential()',
|
||||||
|
requiredErrorMsg: "Please select a Credential.",
|
||||||
|
column: 1,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " +
|
||||||
|
" the username and SSH key or password that Ansible will need to log into the remote hosts.</p>",
|
||||||
|
dataTitle: 'Credential',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_credential_on_launch",
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: 'selectedTemplate.ask_credential_on_launch'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inventory: {
|
||||||
|
label: 'Inventory',
|
||||||
|
type: 'lookup',
|
||||||
|
sourceModel: 'inventory',
|
||||||
|
sourceField: 'name',
|
||||||
|
ngClick: 'lookUpInventory()',
|
||||||
|
requiredErrorMsg: "Please select an Inventory.",
|
||||||
|
column: 1,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>Select the inventory containing the hosts you want this job to manage.</p>",
|
||||||
|
dataTitle: 'Inventory',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_inventory_on_launch",
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: 'selectedTemplate.ask_inventory_on_launch'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
job_type: {
|
||||||
|
label: 'Job Type',
|
||||||
|
type: 'select',
|
||||||
|
ngOptions: 'type.label for type in job_type_options track by type.value',
|
||||||
|
"default": 0,
|
||||||
|
column: 1,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>When this template is submitted as a job, setting the type to <em>run</em> will execute the playbook, running tasks " +
|
||||||
|
" on the selected hosts.</p> <p>Setting the type to <em>check</em> will not execute the playbook. Instead, <code>ansible</code> will check playbook " +
|
||||||
|
" syntax, test environment setup and report problems.</p> <p>Setting the type to <em>scan</em> will execute the playbook and store any " +
|
||||||
|
" scanned facts for use with Tower's System Tracking feature.</p>",
|
||||||
|
dataTitle: 'Job Type',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_job_type_on_launch",
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: 'selectedTemplate.ask_job_type_on_launch'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
label: 'Limit',
|
||||||
|
type: 'text',
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
column: 1,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. " +
|
||||||
|
"Multiple patterns can be separated by ; : or ,</p><p>For more information and examples see " +
|
||||||
|
"<a href=\"http://docs.ansible.com/intro_patterns.html\" target=\"_blank\">the Patterns topic at docs.ansible.com</a>.</p>",
|
||||||
|
dataTitle: 'Limit',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_limit_on_launch"
|
||||||
|
},
|
||||||
|
job_tags: {
|
||||||
|
label: 'Job Tags',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 5,
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
'elementClass': 'Form-textInput',
|
||||||
|
column: 2,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>Provide a comma separated list of tags.</p>\n" +
|
||||||
|
"<p>Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.</p>" +
|
||||||
|
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>",
|
||||||
|
dataTitle: "Job Tags",
|
||||||
|
dataPlacement: "right",
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_tags_on_launch"
|
||||||
|
},
|
||||||
|
skip_tags: {
|
||||||
|
label: 'Skip Tags',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 5,
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
'elementClass': 'Form-textInput',
|
||||||
|
column: 2,
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
awPopOver: "<p>Provide a comma separated list of tags.</p>\n" +
|
||||||
|
"<p>Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task.</p>" +
|
||||||
|
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>",
|
||||||
|
dataTitle: "Skip Tags",
|
||||||
|
dataPlacement: "right",
|
||||||
|
dataContainer: "body",
|
||||||
|
ngShow: "selectedTemplate.ask_skip_tags_on_launch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
ngClick: 'cancelNodeForm()'
|
||||||
|
},
|
||||||
|
save: {
|
||||||
|
ngClick: 'confirmNodeForm()',
|
||||||
|
ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.factory('WorkflowMakerForm', ['WorkflowMakerFormObject', 'NotificationsList', function(WorkflowMakerFormObject, NotificationsList) {
|
||||||
|
return function() {
|
||||||
|
var itm;
|
||||||
|
for (itm in WorkflowMakerFormObject.related) {
|
||||||
|
if (WorkflowMakerFormObject.related[itm].include === "NotificationsList") {
|
||||||
|
WorkflowMakerFormObject.related[itm] = NotificationsList;
|
||||||
|
WorkflowMakerFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WorkflowMakerFormObject;
|
||||||
|
};
|
||||||
|
}]);
|
||||||
203
awx/ui/client/src/forms/Workflows.js
Normal file
203
awx/ui/client/src/forms/Workflows.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name forms.function:Workflow
|
||||||
|
* @description This form is for adding/editing a Workflow
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('WorkflowFormDefinition', [])
|
||||||
|
|
||||||
|
.value ('WorkflowFormObject', {
|
||||||
|
|
||||||
|
addTitle: 'New Workflow',
|
||||||
|
editTitle: '{{ name }}',
|
||||||
|
name: 'workflow_job_template',
|
||||||
|
base: 'workflow',
|
||||||
|
basePath: 'workflow_job_templates',
|
||||||
|
// the top-most node of generated state tree
|
||||||
|
stateTree: 'templates',
|
||||||
|
activeEditState: 'templates.editWorkflowJobTemplate',
|
||||||
|
tabs: true,
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
name: {
|
||||||
|
label: 'Name',
|
||||||
|
type: 'text',
|
||||||
|
addRequired: true,
|
||||||
|
editRequired: true,
|
||||||
|
column: 1
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
label: 'Description',
|
||||||
|
type: 'text',
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
column: 1
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
label: 'Organization',
|
||||||
|
type: 'lookup',
|
||||||
|
sourceModel: 'organization',
|
||||||
|
basePath: 'organizations',
|
||||||
|
list: 'OrganizationList',
|
||||||
|
sourceField: 'name',
|
||||||
|
dataTitle: 'Organization',
|
||||||
|
dataContainer: 'body',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
column: 1
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
label: 'Labels',
|
||||||
|
type: 'select',
|
||||||
|
class: 'Form-formGroup--fullWidth',
|
||||||
|
ngOptions: 'label.label for label in labelOptions track by label.value',
|
||||||
|
multiSelect: true,
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
dataTitle: 'Labels',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
awPopOver: "<p>Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.</p>",
|
||||||
|
dataContainer: 'body'
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
label: 'Extra Variables',
|
||||||
|
type: 'textarea',
|
||||||
|
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||||
|
rows: 6,
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false,
|
||||||
|
"default": "---",
|
||||||
|
column: 2,
|
||||||
|
awPopOver: "<p>Pass extra command line variables to the playbook. This is the <code>-e</code> or <code>--extra-vars</code> command line parameter " +
|
||||||
|
"for <code>ansible-playbook</code>. Provide key/value pairs using either YAML or JSON.</p>" +
|
||||||
|
"JSON:<br />\n" +
|
||||||
|
"<blockquote>{<br /> \"somevar\": \"somevalue\",<br /> \"password\": \"magic\"<br /> }</blockquote>\n" +
|
||||||
|
"YAML:<br />\n" +
|
||||||
|
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n",
|
||||||
|
dataTitle: 'Extra Variables',
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buttons: { //for now always generates <button> tags
|
||||||
|
cancel: {
|
||||||
|
ngClick: 'formCancel()'
|
||||||
|
},
|
||||||
|
save: {
|
||||||
|
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||||
|
ngDisabled: "workflow_form.$invalid || can_edit!==true"//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
related: {
|
||||||
|
permissions: {
|
||||||
|
awToolTip: 'Please save before assigning permissions',
|
||||||
|
dataPlacement: 'top',
|
||||||
|
basePath: 'job_templates/:id/access_list/',
|
||||||
|
type: 'collection',
|
||||||
|
title: 'Permissions',
|
||||||
|
iterator: 'permission',
|
||||||
|
index: false,
|
||||||
|
open: false,
|
||||||
|
searchType: 'select',
|
||||||
|
actions: {
|
||||||
|
add: {
|
||||||
|
ngClick: "addPermission",
|
||||||
|
label: 'Add',
|
||||||
|
awToolTip: 'Add a permission',
|
||||||
|
actionClass: 'btn List-buttonSubmit',
|
||||||
|
buttonContent: '+ ADD',
|
||||||
|
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
username: {
|
||||||
|
key: true,
|
||||||
|
label: 'User',
|
||||||
|
linkBase: 'users',
|
||||||
|
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
label: 'Role',
|
||||||
|
type: 'role',
|
||||||
|
noSort: true,
|
||||||
|
class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4',
|
||||||
|
searchable: false
|
||||||
|
},
|
||||||
|
team_roles: {
|
||||||
|
label: 'Team Roles',
|
||||||
|
type: 'team_roles',
|
||||||
|
noSort: true,
|
||||||
|
class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4',
|
||||||
|
searchable: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
include: "NotificationsList"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
relatedButtons: {
|
||||||
|
add_survey: {
|
||||||
|
ngClick: 'addSurvey()',
|
||||||
|
ngShow: '!survey_exists',
|
||||||
|
awFeature: 'surveys',
|
||||||
|
awToolTip: 'Please save before adding a survey',
|
||||||
|
dataPlacement: 'top',
|
||||||
|
label: 'Add Survey',
|
||||||
|
class: 'Form-primaryButton'
|
||||||
|
},
|
||||||
|
edit_survey: {
|
||||||
|
ngClick: 'editSurvey()',
|
||||||
|
awFeature: 'surveys',
|
||||||
|
ngShow: 'survey_exists',
|
||||||
|
label: 'Edit Survey',
|
||||||
|
class: 'Form-primaryButton'
|
||||||
|
},
|
||||||
|
workflow_editor: {
|
||||||
|
ngClick: 'openWorkflowMaker()',
|
||||||
|
awToolTip: 'Please save before defining the workflow graph',
|
||||||
|
dataPlacement: 'top',
|
||||||
|
label: 'Workflow Editor',
|
||||||
|
class: 'Form-primaryButton'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
relatedSets: function(urls) {
|
||||||
|
return {
|
||||||
|
permissions: {
|
||||||
|
iterator: 'permission',
|
||||||
|
url: urls.access_list
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
iterator: 'notification',
|
||||||
|
url: '/api/v1/notification_templates/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.factory('WorkflowForm', ['WorkflowFormObject', 'NotificationsList',
|
||||||
|
function(WorkflowFormObject, NotificationsList) {
|
||||||
|
return function() {
|
||||||
|
var itm;
|
||||||
|
|
||||||
|
for (itm in WorkflowFormObject.related) {
|
||||||
|
if (WorkflowFormObject.related[itm].include === "NotificationsList") {
|
||||||
|
WorkflowFormObject.related[itm] = NotificationsList;
|
||||||
|
WorkflowFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WorkflowFormObject;
|
||||||
|
};
|
||||||
|
}]);
|
||||||
@@ -14,7 +14,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
|||||||
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
||||||
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
||||||
ParseVariableString, Prompt, InitiatePlaybookRun,
|
ParseVariableString, Prompt, InitiatePlaybookRun,
|
||||||
deleteJobTemplate, $state, $filter) {
|
JobTemplateService, $state, $filter) {
|
||||||
|
|
||||||
// Inject dynamic view
|
// Inject dynamic view
|
||||||
var defaultUrl = GetBasePath('inventory'),
|
var defaultUrl = GetBasePath('inventory'),
|
||||||
@@ -141,25 +141,23 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
|||||||
$location.path($location.path() + '/job_templates/' + this.scan_job_template.id);
|
$location.path($location.path() + '/job_templates/' + this.scan_job_template.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteScanJob = function() {
|
$scope.deleteScanJob = function () {
|
||||||
var id = this.scan_job_template.id,
|
var id = this.scan_job_template.id ,
|
||||||
action = function() {
|
action = function () {
|
||||||
$('#prompt-modal').modal('hide');
|
$('#prompt-modal').modal('hide');
|
||||||
Wait('start');
|
Wait('start');
|
||||||
deleteJobTemplate(id)
|
JobTemplateService.deleteJobTemplate(id)
|
||||||
.success(function() {
|
.success(function () {
|
||||||
$('#prompt-modal').modal('hide');
|
$('#prompt-modal').modal('hide');
|
||||||
// @issue: OLD SEARCH
|
// @issue: OLD SEARCH
|
||||||
// $scope.search(form.related.scan_job_templates.iterator);
|
// $scope.search(form.related.scan_job_templates.iterator);
|
||||||
})
|
})
|
||||||
.error(function(data) {
|
.error(function (data) {
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
ProcessErrors($scope, data, status, null, {
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
hdr: 'Error!',
|
msg: 'DELETE returned status: ' + status });
|
||||||
msg: 'DELETE returned status: ' + status
|
});
|
||||||
});
|
};
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Prompt({
|
Prompt({
|
||||||
hdr: 'Delete',
|
hdr: 'Delete',
|
||||||
@@ -176,5 +174,5 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
|||||||
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
||||||
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
||||||
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
||||||
'deleteJobTemplate', '$state', '$filter', InventoriesEdit,
|
'JobTemplateService', '$state', '$filter', InventoriesEdit,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ export default
|
|||||||
scope.job_template_name = data.name;
|
scope.job_template_name = data.name;
|
||||||
scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||||
scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||||
scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
scope.job_template_url = '/#/templates/' + data.unified_job_template;
|
||||||
scope.inventory_url = (scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
scope.inventory_url = (scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||||
scope.project_url = (scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
scope.project_url = (scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||||
scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export default
|
|||||||
var scope = params.scope.$new(),
|
var scope = params.scope.$new(),
|
||||||
id = params.id,
|
id = params.id,
|
||||||
relaunch = params.relaunch || false,
|
relaunch = params.relaunch || false,
|
||||||
system_job = params.system_job || false;
|
job_type = params.job_type;
|
||||||
scope.job_template_id = id;
|
scope.job_template_id = id;
|
||||||
|
|
||||||
var el = $compile( "<submit-job data-submit-job-id=" + id + " data-submit-job-system=" + system_job + " data-submit-job-relaunch=" + relaunch + "></submit-job>" )( scope );
|
var el = $compile( "<submit-job data-submit-job-id=" + id + " submit-job-type=" + job_type + " data-submit-job-relaunch=" + relaunch + "></submit-job>" )( scope );
|
||||||
$('#content-container').remove('submit-job').append( el );
|
$('#content-container').remove('submit-job').append( el );
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,12 @@ export default
|
|||||||
// jobs, and jobDetails $states.
|
// jobs, and jobDetails $states.
|
||||||
|
|
||||||
if (!$scope.submitJobRelaunch) {
|
if (!$scope.submitJobRelaunch) {
|
||||||
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
|
if($scope.submitJobType && $scope.submitJobType === 'job_template') {
|
||||||
|
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
|
||||||
|
}
|
||||||
|
else if($scope.submitJobType && $scope.submitJobType === 'workflow_job_template') {
|
||||||
|
launch_url = GetBasePath('workflow_job_templates') + $scope.submitJobId + '/launch/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
launch_url = GetBasePath('jobs') + $scope.submitJobId + '/relaunch/';
|
launch_url = GetBasePath('jobs') + $scope.submitJobId + '/relaunch/';
|
||||||
@@ -191,7 +196,7 @@ export default
|
|||||||
updateRequiredPasswords();
|
updateRequiredPasswords();
|
||||||
}
|
}
|
||||||
|
|
||||||
if( ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
if( ($scope.submitJobType === 'workflow_job_template' && !$scope.survey_enabled) || ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
||||||
// The job can be launched if
|
// The job can be launched if
|
||||||
// a) It's a relaunch and no passwords are needed
|
// a) It's a relaunch and no passwords are needed
|
||||||
// or
|
// or
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2', 'ParseT
|
|||||||
return {
|
return {
|
||||||
scope: {
|
scope: {
|
||||||
submitJobId: '=',
|
submitJobId: '=',
|
||||||
submitJobSystem: '=',
|
submitJobType: '@',
|
||||||
submitJobRelaunch: '='
|
submitJobRelaunch: '='
|
||||||
},
|
},
|
||||||
templateUrl: templateUrl('job-submission/job-submission'),
|
templateUrl: templateUrl('job-submission/job-submission'),
|
||||||
|
|||||||
@@ -280,7 +280,7 @@
|
|||||||
|
|
||||||
|
|
||||||
function saveCompleted(id) {
|
function saveCompleted(id) {
|
||||||
$state.go('jobTemplates.edit', {job_template_id: id}, {reload: true});
|
$state.go('templates.editJobTemplate', {id: id}, {reload: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.removeTemplateSaveSuccess) {
|
if ($scope.removeTemplateSaveSuccess) {
|
||||||
@@ -501,14 +501,13 @@
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
console.log(err)
|
|
||||||
Alert("Error", "Error parsing extra variables. " +
|
Alert("Error", "Error parsing extra variables. " +
|
||||||
"Parser returned: " + err);
|
"Parser returned: " + err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formCancel = function () {
|
$scope.formCancel = function () {
|
||||||
$state.go('jobTemplates');
|
$state.transitionTo('templates');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
import controller from './job-templates-add.controller';
|
import controller from './job-template-add.controller';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobTemplatesAdd', [])
|
angular.module('jobTemplateAdd', [])
|
||||||
.controller('JobTemplatesAdd', controller);
|
.controller('JobTemplateAdd', controller);
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
import controller from './job-templates-edit.controller';
|
import controller from './workflow-add.controller';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobTemplatesEdit', [])
|
angular.module('workflowAdd', [])
|
||||||
.controller('JobTemplatesEdit', controller);
|
.controller('WorkflowAdd', controller);
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ '$scope', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors', 'ClearScope',
|
||||||
|
'Wait', '$state', 'CreateSelect2', 'JobTemplateService', 'ToJSON',
|
||||||
|
'ParseTypeChange', 'OrganizationList', '$q', 'Rest', 'GetBasePath',
|
||||||
|
function(
|
||||||
|
$scope, WorkflowForm, GenerateForm, Alert, ProcessErrors, ClearScope,
|
||||||
|
Wait, $state, CreateSelect2, JobTemplateService, ToJSON,
|
||||||
|
ParseTypeChange, OrganizationList, $q, Rest, GetBasePath
|
||||||
|
) {
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath('workflow_job_templates'));
|
||||||
|
Rest.options()
|
||||||
|
.success(function(data) {
|
||||||
|
if (!data.actions.POST) {
|
||||||
|
$state.go("^");
|
||||||
|
Alert('Permission Error', 'You do not have permission to add a workflow job template.', 'alert-info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ClearScope();
|
||||||
|
// Inject dynamic view
|
||||||
|
let form = WorkflowForm(),
|
||||||
|
generator = GenerateForm;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$scope.parseType = 'yaml';
|
||||||
|
$scope.can_edit = true;
|
||||||
|
// apply form definition's default field values
|
||||||
|
GenerateForm.applyDefaults(form, $scope);
|
||||||
|
|
||||||
|
// Make the variables textarea look pretty
|
||||||
|
ParseTypeChange({
|
||||||
|
scope: $scope,
|
||||||
|
field_id: 'workflow_job_template_variables',
|
||||||
|
onChange: function() {
|
||||||
|
// Make sure the form controller knows there was a change
|
||||||
|
$scope[form.name + '_form'].$setDirty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Go out and grab the possible labels
|
||||||
|
JobTemplateService.getLabelOptions()
|
||||||
|
.then(function(data){
|
||||||
|
$scope.labelOptions = data;
|
||||||
|
// select2-ify the labels input
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#workflow_job_template_labels',
|
||||||
|
multiple: true,
|
||||||
|
addNew: true
|
||||||
|
});
|
||||||
|
}, function(error){
|
||||||
|
ProcessErrors($scope, error.data, error.status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get labels. GET returned ' +
|
||||||
|
'status: ' + error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.formSave = function () {
|
||||||
|
let fld, data = {};
|
||||||
|
|
||||||
|
generator.clearApiErrors($scope);
|
||||||
|
|
||||||
|
Wait('start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (fld in form.fields) {
|
||||||
|
data[fld] = $scope[fld];
|
||||||
|
}
|
||||||
|
|
||||||
|
data.extra_vars = ToJSON($scope.parseType,
|
||||||
|
$scope.variables, true);
|
||||||
|
|
||||||
|
// The idea here is that we want to find the new option elements that also have a label that exists in the dom
|
||||||
|
$("#workflow_job_template_labels > option").filter("[data-select2-tag=true]").each(function(optionIndex, option) {
|
||||||
|
$("#workflow_job_template_labels").siblings(".select2").first().find(".select2-selection__choice").each(function(labelIndex, label) {
|
||||||
|
if($(option).text() === $(label).attr('title')) {
|
||||||
|
// Mark that the option has a label present so that we can filter by that down below
|
||||||
|
$(option).attr('data-label-is-present', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.newLabels = $("#workflow_job_template_labels > option")
|
||||||
|
.filter("[data-select2-tag=true]")
|
||||||
|
.filter("[data-label-is-present=true]")
|
||||||
|
.map((i, val) => ({name: $(val).text()}));
|
||||||
|
|
||||||
|
JobTemplateService.createWorkflowJobTemplate(data)
|
||||||
|
.then(function(data) {
|
||||||
|
|
||||||
|
let orgDefer = $q.defer();
|
||||||
|
let associationDefer = $q.defer();
|
||||||
|
|
||||||
|
Rest.setUrl(data.data.related.labels);
|
||||||
|
|
||||||
|
let currentLabels = Rest.get()
|
||||||
|
.then(function(data) {
|
||||||
|
return data.data.results
|
||||||
|
.map(val => val.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentLabels.then(function (current) {
|
||||||
|
let labelsToAdd = ($scope.labels || [])
|
||||||
|
.map(val => val.value);
|
||||||
|
let labelsToDisassociate = current
|
||||||
|
.filter(val => labelsToAdd
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, disassociate: true}));
|
||||||
|
let labelsToAssociate = labelsToAdd
|
||||||
|
.filter(val => current
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, associate: true}));
|
||||||
|
let pass = labelsToDisassociate
|
||||||
|
.concat(labelsToAssociate);
|
||||||
|
associationDefer.resolve(pass);
|
||||||
|
});
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath("organizations"));
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
orgDefer.resolve(data.results[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
orgDefer.promise.then(function(orgId) {
|
||||||
|
let toPost = [];
|
||||||
|
$scope.newLabels = $scope.newLabels
|
||||||
|
.map(function(i, val) {
|
||||||
|
val.organization = orgId;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.newLabels.each(function(i, val) {
|
||||||
|
toPost.push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
associationDefer.promise.then(function(arr) {
|
||||||
|
toPost = toPost
|
||||||
|
.concat(arr);
|
||||||
|
|
||||||
|
Rest.setUrl(data.data.related.labels);
|
||||||
|
|
||||||
|
let defers = [];
|
||||||
|
for (let i = 0; i < toPost.length; i++) {
|
||||||
|
defers.push(Rest.post(toPost[i]));
|
||||||
|
}
|
||||||
|
$q.all(defers)
|
||||||
|
.then(function() {
|
||||||
|
// If we follow the same pattern as job templates then the survey logic will go here
|
||||||
|
|
||||||
|
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: data.data.id}, {reload: true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function (error) {
|
||||||
|
ProcessErrors($scope, error.data, error.status, form,
|
||||||
|
{
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to add new workflow. ' +
|
||||||
|
'POST returned status: ' +
|
||||||
|
error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Wait('stop');
|
||||||
|
Alert("Error", "Error parsing extra variables. " +
|
||||||
|
"Parser returned: " + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formCancel = function () {
|
||||||
|
$state.transitionTo('templates');
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="tab-pane" id="workflow_create">
|
||||||
|
<div ui-view></div>
|
||||||
|
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||||
|
</div>
|
||||||
@@ -19,7 +19,12 @@
|
|||||||
jobTemplateCopyService.set(res)
|
jobTemplateCopyService.set(res)
|
||||||
.success(function(res){
|
.success(function(res){
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
$state.go('jobTemplates.edit', {id: res.id}, {reload: true});
|
if(res.type && res.type === 'job_template') {
|
||||||
|
$state.go('templates.editJobTemplate', {id: res.id}, {reload: true});
|
||||||
|
}
|
||||||
|
else if(res.type && res.type === 'workflow') {
|
||||||
|
// TODO: direct the user to the edit state for workflows
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.error(function(res, status){
|
.error(function(res, status){
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'jobTemplates.copy',
|
name: 'templates.copy',
|
||||||
route: '/:id/copy',
|
route: '/:id/copy',
|
||||||
controller: 'jobTemplateCopyController'
|
controller: 'jobTemplateCopyController'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
/*************************************************
|
|
||||||
* Copyright (c) 2015 Ansible, Inc.
|
|
||||||
*
|
|
||||||
* All Rights Reserved
|
|
||||||
*************************************************/
|
|
||||||
|
|
||||||
export default ['Rest', 'GetBasePath', function(Rest, GetBasePath){
|
|
||||||
return {
|
|
||||||
deleteJobTemplate: function(id){
|
|
||||||
var url = GetBasePath('job_templates');
|
|
||||||
|
|
||||||
url = url + id;
|
|
||||||
|
|
||||||
Rest.setUrl(url);
|
|
||||||
return Rest.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}];
|
|
||||||
11
awx/ui/client/src/job-templates/edit-job-template/main.js
Normal file
11
awx/ui/client/src/job-templates/edit-job-template/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import controller from './job-template-edit.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('jobTemplateEdit', [])
|
||||||
|
.controller('JobTemplateEdit', controller);
|
||||||
11
awx/ui/client/src/job-templates/edit-workflow/main.js
Normal file
11
awx/ui/client/src/job-templates/edit-workflow/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import controller from './workflow-edit.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('workflowEdit', [])
|
||||||
|
.controller('WorkflowEdit', controller);
|
||||||
@@ -0,0 +1,762 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ '$scope', '$stateParams', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
||||||
|
'ClearScope', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty',
|
||||||
|
'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString',
|
||||||
|
'JobTemplateService', 'OrganizationList', 'Rest',
|
||||||
|
function(
|
||||||
|
$scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
||||||
|
ClearScope, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||||
|
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||||
|
JobTemplateService, OrganizationList, Rest
|
||||||
|
) {
|
||||||
|
|
||||||
|
ClearScope();
|
||||||
|
|
||||||
|
$scope.$watch('workflow_job_template_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||||
|
if (val === false) {
|
||||||
|
$scope.canAdd = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject dynamic view
|
||||||
|
let form = WorkflowForm(),
|
||||||
|
generator = GenerateForm,
|
||||||
|
id = $stateParams.workflow_job_template_id;
|
||||||
|
|
||||||
|
$scope.mode = 'edit';
|
||||||
|
$scope.parseType = 'yaml';
|
||||||
|
$scope.includeWorkflowMaker = false;
|
||||||
|
|
||||||
|
// What is this used for? Permissions?
|
||||||
|
$scope.can_edit = true;
|
||||||
|
|
||||||
|
$scope.editRequests = [];
|
||||||
|
$scope.associateRequests = [];
|
||||||
|
$scope.disassociateRequests = [];
|
||||||
|
|
||||||
|
$scope.workflowTree = {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
canDelete: false,
|
||||||
|
canEdit: false,
|
||||||
|
canAddTo: true,
|
||||||
|
isStartNode: true,
|
||||||
|
unifiedJobTemplate: {
|
||||||
|
name: "Workflow Launch"
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
deletedNodes: [],
|
||||||
|
totalNodes: 0
|
||||||
|
},
|
||||||
|
nextIndex: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildBranch(params) {
|
||||||
|
// params.nodeId
|
||||||
|
// params.parentId
|
||||||
|
// params.edgeType
|
||||||
|
// params.nodesObj
|
||||||
|
// params.isRoot
|
||||||
|
|
||||||
|
let treeNode = {
|
||||||
|
children: [],
|
||||||
|
c: "#D7D7D7",
|
||||||
|
id: $scope.workflowTree.nextIndex,
|
||||||
|
nodeId: params.nodeId,
|
||||||
|
canDelete: true,
|
||||||
|
canEdit: true,
|
||||||
|
canAddTo: true,
|
||||||
|
placeholder: false,
|
||||||
|
edgeType: params.edgeType,
|
||||||
|
unifiedJobTemplate: _.clone(params.nodesObj[params.nodeId].summary_fields.unified_job_template),
|
||||||
|
isNew: false,
|
||||||
|
edited: false,
|
||||||
|
originalEdge: params.edgeType,
|
||||||
|
originalNodeObj: _.clone(params.nodesObj[params.nodeId]),
|
||||||
|
promptValues: {},
|
||||||
|
isRoot: params.isRoot ? params.isRoot : false
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.workflowTree.data.totalNodes++;
|
||||||
|
|
||||||
|
$scope.workflowTree.nextIndex++;
|
||||||
|
|
||||||
|
if(params.parentId) {
|
||||||
|
treeNode.originalParentId = params.parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop across the success nodes and add them recursively
|
||||||
|
_.forEach(params.nodesObj[params.nodeId].success_nodes, function(successNodeId) {
|
||||||
|
treeNode.children.push(buildBranch({
|
||||||
|
nodeId: successNodeId,
|
||||||
|
parentId: params.nodeId,
|
||||||
|
edgeType: "success",
|
||||||
|
nodesObj: params.nodesObj
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// failure nodes
|
||||||
|
_.forEach(params.nodesObj[params.nodeId].failure_nodes, function(failureNodesId) {
|
||||||
|
treeNode.children.push(buildBranch({
|
||||||
|
nodeId: failureNodesId,
|
||||||
|
parentId: params.nodeId,
|
||||||
|
edgeType: "failure",
|
||||||
|
nodesObj: params.nodesObj
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// always nodes
|
||||||
|
_.forEach(params.nodesObj[params.nodeId].always_nodes, function(alwaysNodesId) {
|
||||||
|
treeNode.children.push(buildBranch({
|
||||||
|
nodeId: alwaysNodesId,
|
||||||
|
parentId: params.nodeId,
|
||||||
|
edgeType: "always",
|
||||||
|
nodesObj: params.nodesObj
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return treeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// // Inject the edit form
|
||||||
|
// generator.inject(form, {
|
||||||
|
// mode: 'edit' ,
|
||||||
|
// scope: $scope,
|
||||||
|
// related: false
|
||||||
|
// });
|
||||||
|
// generator.reset();
|
||||||
|
|
||||||
|
// Select2-ify the lables input
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#workflow_job_template_labels',
|
||||||
|
multiple: true,
|
||||||
|
addNew: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Make the variables textarea look nice
|
||||||
|
// ParseTypeChange({
|
||||||
|
// scope: $scope,
|
||||||
|
// field_id: 'workflow_job_template_variables',
|
||||||
|
// onChange: function() {
|
||||||
|
// $scope[form.name + '_form'].$setDirty();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Initialize the organization lookup
|
||||||
|
// LookUpInit({
|
||||||
|
// scope: $scope,
|
||||||
|
// form: form,
|
||||||
|
// list: OrganizationList,
|
||||||
|
// field: 'organization',
|
||||||
|
// input_type: 'radio'
|
||||||
|
// });
|
||||||
|
|
||||||
|
Rest.setUrl('api/v1/labels');
|
||||||
|
Wait("start");
|
||||||
|
Rest.get()
|
||||||
|
.success(function (data) {
|
||||||
|
$scope.labelOptions = data.results
|
||||||
|
.map((i) => ({label: i.name, value: i.id}));
|
||||||
|
|
||||||
|
var seeMoreResolve = $q.defer();
|
||||||
|
|
||||||
|
var getNext = function(data, arr, resolve) {
|
||||||
|
Rest.setUrl(data.next);
|
||||||
|
Rest.get()
|
||||||
|
.success(function (data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, arr.concat(data.results), resolve);
|
||||||
|
} else {
|
||||||
|
resolve.resolve(arr.concat(data.results));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath('workflow_job_templates') + id +
|
||||||
|
"/labels");
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, data.results, seeMoreResolve);
|
||||||
|
} else {
|
||||||
|
seeMoreResolve.resolve(data.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
seeMoreResolve.promise.then(function (labels) {
|
||||||
|
$scope.$emit("choicesReady");
|
||||||
|
var opts = labels
|
||||||
|
.map(i => ({id: i.id + "",
|
||||||
|
test: i.name}));
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#workflow_job_template_labels',
|
||||||
|
multiple: true,
|
||||||
|
addNew: true,
|
||||||
|
opts: opts
|
||||||
|
});
|
||||||
|
Wait("stop");
|
||||||
|
});
|
||||||
|
}).error(function(){
|
||||||
|
// job template id is null in this case
|
||||||
|
$scope.$emit("choicesReady");
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.error(function (data, status) {
|
||||||
|
ProcessErrors($scope, data, status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get labels. GET returned ' +
|
||||||
|
'status: ' + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the workflow nodes
|
||||||
|
JobTemplateService.getWorkflowJobTemplateNodes(id)
|
||||||
|
.then(function(data){
|
||||||
|
|
||||||
|
let nodesArray = data.data.results;
|
||||||
|
let nodesObj = {};
|
||||||
|
let nonRootNodeIds = [];
|
||||||
|
let allNodeIds = [];
|
||||||
|
|
||||||
|
// Determine which nodes are root nodes
|
||||||
|
_.forEach(nodesArray, function(node) {
|
||||||
|
nodesObj[node.id] = _.clone(node);
|
||||||
|
|
||||||
|
allNodeIds.push(node.id);
|
||||||
|
|
||||||
|
_.forEach(node.success_nodes, function(nodeId){
|
||||||
|
nonRootNodeIds.push(nodeId);
|
||||||
|
});
|
||||||
|
_.forEach(node.failure_nodes, function(nodeId){
|
||||||
|
nonRootNodeIds.push(nodeId);
|
||||||
|
});
|
||||||
|
_.forEach(node.always_nodes, function(nodeId){
|
||||||
|
nonRootNodeIds.push(nodeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let rootNodes = _.difference(allNodeIds, nonRootNodeIds);
|
||||||
|
|
||||||
|
// Loop across the root nodes and re-build the tree
|
||||||
|
_.forEach(rootNodes, function(rootNodeId) {
|
||||||
|
let branch = buildBranch({
|
||||||
|
nodeId: rootNodeId,
|
||||||
|
edgeType: "always",
|
||||||
|
nodesObj: nodesObj,
|
||||||
|
isRoot: true
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflowTree.data.children.push(branch);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
|
||||||
|
// this workflowTree object and removing the children object for some reason (?)
|
||||||
|
// This happens on occasion and I think is a race condition (?)
|
||||||
|
if(!$scope.workflowTree.data.children) {
|
||||||
|
$scope.workflowTree.data.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable.
|
||||||
|
// It won't get included until this the tree has been built - I'm open to better ways of doing this.
|
||||||
|
$scope.includeWorkflowMaker = true;
|
||||||
|
|
||||||
|
}, function(error){
|
||||||
|
ProcessErrors($scope, error.data, error.status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get workflow job template nodes. GET returned ' +
|
||||||
|
'status: ' + error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Go out and GET the workflow job temlate data needed to populate the form
|
||||||
|
JobTemplateService.getWorkflowJobTemplate(id)
|
||||||
|
.then(function(data){
|
||||||
|
let workflowJobTemplateData = data.data;
|
||||||
|
$scope.workflow_job_template_obj = workflowJobTemplateData;
|
||||||
|
$scope.name = workflowJobTemplateData.name;
|
||||||
|
let fld, i;
|
||||||
|
for (fld in form.fields) {
|
||||||
|
if (fld !== 'variables' && fld !== 'survey' && workflowJobTemplateData[fld] !== null && workflowJobTemplateData[fld] !== undefined) {
|
||||||
|
if (form.fields[fld].type === 'select') {
|
||||||
|
if ($scope[fld + '_options'] && $scope[fld + '_options'].length > 0) {
|
||||||
|
for (i = 0; i < $scope[fld + '_options'].length; i++) {
|
||||||
|
if (workflowJobTemplateData[fld] === $scope[fld + '_options'][i].value) {
|
||||||
|
$scope[fld] = $scope[fld + '_options'][i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$scope[fld] = workflowJobTemplateData[fld];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$scope[fld] = workflowJobTemplateData[fld];
|
||||||
|
if(!Empty(workflowJobTemplateData.summary_fields.survey)) {
|
||||||
|
$scope.survey_exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fld === 'variables') {
|
||||||
|
// Parse extra_vars, converting to YAML.
|
||||||
|
$scope.variables = ParseVariableString(workflowJobTemplateData.extra_vars);
|
||||||
|
|
||||||
|
ParseTypeChange({ scope: $scope, field_id: 'workflow_job_template_variables' });
|
||||||
|
}
|
||||||
|
if (form.fields[fld].type === 'lookup' && workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel]) {
|
||||||
|
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||||
|
workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Wait('stop');
|
||||||
|
$scope.url = workflowJobTemplateData.url;
|
||||||
|
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
||||||
|
|
||||||
|
}, function(error){
|
||||||
|
ProcessErrors($scope, error.data, error.status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get workflow job template. GET returned ' +
|
||||||
|
'status: ' + error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursiveNodeUpdates(params, completionCallback) {
|
||||||
|
// params.parentId
|
||||||
|
// params.node
|
||||||
|
|
||||||
|
let generatePostUrl = function(){
|
||||||
|
|
||||||
|
let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.workflow_job_template_obj.related.workflow_nodes;
|
||||||
|
|
||||||
|
if(params.parentId) {
|
||||||
|
if(params.node.edgeType === 'success') {
|
||||||
|
base += "/success_nodes";
|
||||||
|
}
|
||||||
|
else if(params.node.edgeType === 'failure') {
|
||||||
|
base += "/failure_nodes";
|
||||||
|
}
|
||||||
|
else if(params.node.edgeType === 'always') {
|
||||||
|
base += "/always_nodes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let buildSendableNodeData = function() {
|
||||||
|
// Create the node
|
||||||
|
let sendableNodeData = {
|
||||||
|
unified_job_template: params.node.unifiedJobTemplate.id
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check to see if the user has provided any prompt values that are different
|
||||||
|
// from the defaults in the job template
|
||||||
|
|
||||||
|
if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) {
|
||||||
|
if(params.node.unifiedJobTemplate.ask_credential_on_launch) {
|
||||||
|
sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null;
|
||||||
|
}
|
||||||
|
if(params.node.unifiedJobTemplate.ask_inventory_on_launch) {
|
||||||
|
sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null;
|
||||||
|
}
|
||||||
|
if(params.node.unifiedJobTemplate.ask_limit_on_launch) {
|
||||||
|
sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null;
|
||||||
|
}
|
||||||
|
if(params.node.unifiedJobTemplate.ask_job_type_on_launch) {
|
||||||
|
sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null;
|
||||||
|
}
|
||||||
|
if(params.node.unifiedJobTemplate.ask_tags_on_launch) {
|
||||||
|
sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null;
|
||||||
|
}
|
||||||
|
if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) {
|
||||||
|
sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendableNodeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let continueRecursing = function(parentId) {
|
||||||
|
$scope.totalIteratedNodes++;
|
||||||
|
|
||||||
|
if($scope.totalIteratedNodes === $scope.workflowTree.data.totalNodes) {
|
||||||
|
// We're done recursing, lets move on
|
||||||
|
completionCallback();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(params.node.children && params.node.children.length > 0) {
|
||||||
|
_.forEach(params.node.children, function(child) {
|
||||||
|
if(child.edgeType === "success") {
|
||||||
|
recursiveNodeUpdates({
|
||||||
|
parentId: parentId,
|
||||||
|
node: child
|
||||||
|
}, completionCallback);
|
||||||
|
}
|
||||||
|
else if(child.edgeType === "failure") {
|
||||||
|
recursiveNodeUpdates({
|
||||||
|
parentId: parentId,
|
||||||
|
node: child
|
||||||
|
}, completionCallback);
|
||||||
|
}
|
||||||
|
else if(child.edgeType === "always") {
|
||||||
|
recursiveNodeUpdates({
|
||||||
|
parentId: parentId,
|
||||||
|
node: child
|
||||||
|
}, completionCallback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(params.node.isNew) {
|
||||||
|
|
||||||
|
JobTemplateService.addWorkflowNode({
|
||||||
|
url: generatePostUrl(),
|
||||||
|
data: buildSendableNodeData()
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
continueRecursing(data.data.id);
|
||||||
|
}, function(error) {
|
||||||
|
ProcessErrors($scope, error.data, error.status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to add workflow node. ' +
|
||||||
|
'POST returned status: ' +
|
||||||
|
error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
|
||||||
|
|
||||||
|
if(params.node.edited) {
|
||||||
|
|
||||||
|
$scope.editRequests.push({
|
||||||
|
id: params.node.nodeId,
|
||||||
|
data: buildSendableNodeData()
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep
|
||||||
|
|
||||||
|
$scope.disassociateRequests.push({
|
||||||
|
parentId: params.node.originalParentId,
|
||||||
|
nodeId: params.node.nodeId,
|
||||||
|
edge: params.node.originalEdge
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can only associate if we have a parent.
|
||||||
|
// If we don't have a parent then this is a root node
|
||||||
|
// and the act of disassociating will make it a root node
|
||||||
|
if(params.parentId) {
|
||||||
|
$scope.associateRequests.push({
|
||||||
|
parentId: params.parentId,
|
||||||
|
nodeId: params.node.nodeId,
|
||||||
|
edge: params.node.edgeType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(!params.node.originalParentId && params.parentId) {
|
||||||
|
// This used to be a root node but is now not a root node
|
||||||
|
$scope.associateRequests.push({
|
||||||
|
parentId: params.parentId,
|
||||||
|
nodeId: params.node.nodeId,
|
||||||
|
edge: params.node.edgeType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
continueRecursing(params.node.nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.openWorkflowMaker = function() {
|
||||||
|
$scope.$broadcast("showWorkflowMaker");
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formSave = function () {
|
||||||
|
let fld, data = {};
|
||||||
|
$scope.invalid_survey = false;
|
||||||
|
|
||||||
|
// Can't have a survey enabled without a survey
|
||||||
|
if($scope.survey_enabled === true && $scope.survey_exists!==true){
|
||||||
|
$scope.survey_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.clearApiErrors($scope);
|
||||||
|
|
||||||
|
Wait('start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (fld in form.fields) {
|
||||||
|
data[fld] = $scope[fld];
|
||||||
|
}
|
||||||
|
|
||||||
|
data.extra_vars = ToJSON($scope.parseType,
|
||||||
|
$scope.variables, true);
|
||||||
|
|
||||||
|
// The idea here is that we want to find the new option elements that also have a label that exists in the dom
|
||||||
|
$("#workflow_job_template_labels > option").filter("[data-select2-tag=true]").each(function(optionIndex, option) {
|
||||||
|
$("#workflow_job_template_labels").siblings(".select2").first().find(".select2-selection__choice").each(function(labelIndex, label) {
|
||||||
|
if($(option).text() === $(label).attr('title')) {
|
||||||
|
// Mark that the option has a label present so that we can filter by that down below
|
||||||
|
$(option).attr('data-label-is-present', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.newLabels = $("#workflow_job_template_labels > option")
|
||||||
|
.filter("[data-select2-tag=true]")
|
||||||
|
.filter("[data-label-is-present=true]")
|
||||||
|
.map((i, val) => ({name: $(val).text()}));
|
||||||
|
|
||||||
|
$scope.totalIteratedNodes = 0;
|
||||||
|
|
||||||
|
// TODO: this is the only way that I could figure out to get
|
||||||
|
// these promise arrays to play nicely. I tried to just append
|
||||||
|
// a single promise to deletePromises but it just wasn't working
|
||||||
|
let editWorkflowJobTemplate = [id].map(function(id) {
|
||||||
|
return JobTemplateService.updateWorkflowJobTemplate({
|
||||||
|
id: id,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if($scope.workflowTree && $scope.workflowTree.data && $scope.workflowTree.data.children && $scope.workflowTree.data.children.length > 0) {
|
||||||
|
let completionCallback = function() {
|
||||||
|
|
||||||
|
let disassociatePromises = $scope.disassociateRequests.map(function(request) {
|
||||||
|
return JobTemplateService.disassociateWorkflowNode({
|
||||||
|
parentId: request.parentId,
|
||||||
|
nodeId: request.nodeId,
|
||||||
|
edge: request.edge
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let editNodePromises = $scope.editRequests.map(function(request) {
|
||||||
|
return JobTemplateService.editWorkflowNode({
|
||||||
|
id: request.id,
|
||||||
|
data: request.data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.all(disassociatePromises.concat(editNodePromises).concat(editWorkflowJobTemplate))
|
||||||
|
.then(function() {
|
||||||
|
|
||||||
|
let associatePromises = $scope.associateRequests.map(function(request) {
|
||||||
|
return JobTemplateService.associateWorkflowNode({
|
||||||
|
parentId: request.parentId,
|
||||||
|
nodeId: request.nodeId,
|
||||||
|
edge: request.edge
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) {
|
||||||
|
return JobTemplateService.deleteWorkflowJobTemplateNode(nodeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.all(associatePromises.concat(deletePromises))
|
||||||
|
.then(function() {
|
||||||
|
|
||||||
|
var orgDefer = $q.defer();
|
||||||
|
var associationDefer = $q.defer();
|
||||||
|
var associatedLabelsDefer = $q.defer();
|
||||||
|
|
||||||
|
var getNext = function(data, arr, resolve) {
|
||||||
|
Rest.setUrl(data.next);
|
||||||
|
Rest.get()
|
||||||
|
.success(function (data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, arr.concat(data.results), resolve);
|
||||||
|
} else {
|
||||||
|
resolve.resolve(arr.concat(data.results));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||||
|
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, data.results, associatedLabelsDefer);
|
||||||
|
} else {
|
||||||
|
associatedLabelsDefer.resolve(data.results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
associatedLabelsDefer.promise.then(function (current) {
|
||||||
|
current = current.map(data => data.id);
|
||||||
|
var labelsToAdd = $scope.labels
|
||||||
|
.map(val => val.value);
|
||||||
|
var labelsToDisassociate = current
|
||||||
|
.filter(val => labelsToAdd
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, disassociate: true}));
|
||||||
|
var labelsToAssociate = labelsToAdd
|
||||||
|
.filter(val => current
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, associate: true}));
|
||||||
|
var pass = labelsToDisassociate
|
||||||
|
.concat(labelsToAssociate);
|
||||||
|
associationDefer.resolve(pass);
|
||||||
|
});
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath("organizations"));
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
orgDefer.resolve(data.results[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
orgDefer.promise.then(function(orgId) {
|
||||||
|
var toPost = [];
|
||||||
|
$scope.newLabels = $scope.newLabels
|
||||||
|
.map(function(i, val) {
|
||||||
|
val.organization = orgId;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.newLabels.each(function(i, val) {
|
||||||
|
toPost.push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
associationDefer.promise.then(function(arr) {
|
||||||
|
toPost = toPost
|
||||||
|
.concat(arr);
|
||||||
|
|
||||||
|
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||||
|
|
||||||
|
var defers = [];
|
||||||
|
for (var i = 0; i < toPost.length; i++) {
|
||||||
|
defers.push(Rest.post(toPost[i]));
|
||||||
|
}
|
||||||
|
$q.all(defers)
|
||||||
|
.then(function() {
|
||||||
|
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_.forEach($scope.workflowTree.data.children, function(child) {
|
||||||
|
recursiveNodeUpdates({
|
||||||
|
node: child
|
||||||
|
}, completionCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) {
|
||||||
|
return JobTemplateService.deleteWorkflowJobTemplateNode(nodeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.all(deletePromises.concat(editWorkflowJobTemplate))
|
||||||
|
.then(function() {
|
||||||
|
var orgDefer = $q.defer();
|
||||||
|
var associationDefer = $q.defer();
|
||||||
|
var associatedLabelsDefer = $q.defer();
|
||||||
|
|
||||||
|
var getNext = function(data, arr, resolve) {
|
||||||
|
Rest.setUrl(data.next);
|
||||||
|
Rest.get()
|
||||||
|
.success(function (data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, arr.concat(data.results), resolve);
|
||||||
|
} else {
|
||||||
|
resolve.resolve(arr.concat(data.results));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||||
|
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
if (data.next) {
|
||||||
|
getNext(data, data.results, associatedLabelsDefer);
|
||||||
|
} else {
|
||||||
|
associatedLabelsDefer.resolve(data.results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
associatedLabelsDefer.promise.then(function (current) {
|
||||||
|
current = current.map(data => data.id);
|
||||||
|
var labelsToAdd = $scope.labels
|
||||||
|
.map(val => val.value);
|
||||||
|
var labelsToDisassociate = current
|
||||||
|
.filter(val => labelsToAdd
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, disassociate: true}));
|
||||||
|
var labelsToAssociate = labelsToAdd
|
||||||
|
.filter(val => current
|
||||||
|
.indexOf(val) === -1)
|
||||||
|
.map(val => ({id: val, associate: true}));
|
||||||
|
var pass = labelsToDisassociate
|
||||||
|
.concat(labelsToAssociate);
|
||||||
|
associationDefer.resolve(pass);
|
||||||
|
});
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath("organizations"));
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
orgDefer.resolve(data.results[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
orgDefer.promise.then(function(orgId) {
|
||||||
|
var toPost = [];
|
||||||
|
$scope.newLabels = $scope.newLabels
|
||||||
|
.map(function(i, val) {
|
||||||
|
val.organization = orgId;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.newLabels.each(function(i, val) {
|
||||||
|
toPost.push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
associationDefer.promise.then(function(arr) {
|
||||||
|
toPost = toPost
|
||||||
|
.concat(arr);
|
||||||
|
|
||||||
|
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||||
|
|
||||||
|
var defers = [];
|
||||||
|
for (var i = 0; i < toPost.length; i++) {
|
||||||
|
defers.push(Rest.post(toPost[i]));
|
||||||
|
}
|
||||||
|
$q.all(defers)
|
||||||
|
.then(function() {
|
||||||
|
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Wait('stop');
|
||||||
|
Alert("Error", "Error saving workflow job template. " +
|
||||||
|
"Parser returned: " + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formCancel = function () {
|
||||||
|
$state.transitionTo('templates');
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="tab-pane" id="workflow_edit">
|
||||||
|
<div ui-view></div>
|
||||||
|
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||||
|
<workflow-maker ng-if="includeWorkflowMaker" tree-data="workflowTree"></workflow-maker>
|
||||||
|
</div>
|
||||||
190
awx/ui/client/src/job-templates/job-template.service.js
Normal file
190
awx/ui/client/src/job-templates/job-template.service.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default ['Rest', 'GetBasePath', '$q', function(Rest, GetBasePath, $q){
|
||||||
|
return {
|
||||||
|
deleteJobTemplate: function(id){
|
||||||
|
var url = GetBasePath('job_templates');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.destroy();
|
||||||
|
},
|
||||||
|
deleteWorkflowJobTemplate: function(id) {
|
||||||
|
var url = GetBasePath('workflow_job_templates');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.destroy();
|
||||||
|
},
|
||||||
|
createJobTemplate: function(data){
|
||||||
|
var url = GetBasePath('job_templates');
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.post(data);
|
||||||
|
},
|
||||||
|
createWorkflowJobTemplate: function(data) {
|
||||||
|
var url = GetBasePath('workflow_job_templates');
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.post(data);
|
||||||
|
},
|
||||||
|
getLabelOptions: function(){
|
||||||
|
var url = GetBasePath('labels');
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
// Turn the labels into something consumable
|
||||||
|
var labels = data.results.map((i) => ({label: i.name, value: i.id}));
|
||||||
|
deferred.resolve(labels);
|
||||||
|
}).error(function(msg, code) {
|
||||||
|
deferred.reject(msg, code);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
|
||||||
|
},
|
||||||
|
getJobTemplate: function(id) {
|
||||||
|
var url = GetBasePath('job_templates');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
addWorkflowNode: function(params) {
|
||||||
|
// params.url
|
||||||
|
// params.data
|
||||||
|
|
||||||
|
Rest.setUrl(params.url);
|
||||||
|
return Rest.post(params.data);
|
||||||
|
},
|
||||||
|
editWorkflowNode: function(params) {
|
||||||
|
// params.id
|
||||||
|
// params.data
|
||||||
|
|
||||||
|
var url = GetBasePath('workflow_job_template_nodes') + params.id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.put(params.data);
|
||||||
|
},
|
||||||
|
getJobTemplateLaunchInfo: function(id) {
|
||||||
|
var url = GetBasePath('job_templates');
|
||||||
|
|
||||||
|
url = url + id + '/launch';
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
getWorkflowJobTemplateNodes: function(id) {
|
||||||
|
var url = GetBasePath('workflow_job_templates');
|
||||||
|
|
||||||
|
url = url + id + '/workflow_nodes';
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
updateWorkflowJobTemplate: function(params) {
|
||||||
|
// params.id
|
||||||
|
// params.data
|
||||||
|
|
||||||
|
var url = GetBasePath('workflow_job_templates');
|
||||||
|
|
||||||
|
url = url + params.id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.put(params.data);
|
||||||
|
},
|
||||||
|
getWorkflowJobTemplate: function(id) {
|
||||||
|
var url = GetBasePath('workflow_job_templates');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
deleteWorkflowJobTemplateNode: function(id) {
|
||||||
|
var url = GetBasePath('workflow_job_template_nodes') + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.destroy();
|
||||||
|
},
|
||||||
|
disassociateWorkflowNode: function(params) {
|
||||||
|
//params.parentId
|
||||||
|
//params.nodeId
|
||||||
|
//params.edge
|
||||||
|
|
||||||
|
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||||
|
|
||||||
|
if(params.edge === 'success') {
|
||||||
|
url = url + '/success_nodes';
|
||||||
|
}
|
||||||
|
else if(params.edge === 'failure') {
|
||||||
|
url = url + '/failure_nodes';
|
||||||
|
}
|
||||||
|
else if(params.edge === 'always') {
|
||||||
|
url = url + '/always_nodes';
|
||||||
|
}
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.post({
|
||||||
|
"id": params.nodeId,
|
||||||
|
"disassociate": true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
associateWorkflowNode: function(params) {
|
||||||
|
//params.parentId
|
||||||
|
//params.nodeId
|
||||||
|
//params.edge
|
||||||
|
|
||||||
|
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||||
|
|
||||||
|
if(params.edge === 'success') {
|
||||||
|
url = url + '/success_nodes';
|
||||||
|
}
|
||||||
|
else if(params.edge === 'failure') {
|
||||||
|
url = url + '/failure_nodes';
|
||||||
|
}
|
||||||
|
else if(params.edge === 'always') {
|
||||||
|
url = url + '/always_nodes';
|
||||||
|
}
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.post({
|
||||||
|
id: params.nodeId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getUnifiedJobTemplate: function(id) {
|
||||||
|
var url = GetBasePath('unified_job_templates');
|
||||||
|
|
||||||
|
url = url + "?id=" + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
getCredential: function(id) {
|
||||||
|
var url = GetBasePath('credentials');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
},
|
||||||
|
getInventory: function(id) {
|
||||||
|
var url = GetBasePath('inventory');
|
||||||
|
|
||||||
|
url = url + id;
|
||||||
|
|
||||||
|
Rest.setUrl(url);
|
||||||
|
return Rest.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
@@ -6,16 +6,15 @@
|
|||||||
|
|
||||||
export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Alert',
|
export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Alert',
|
||||||
'JobTemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath',
|
'JobTemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath',
|
||||||
'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService',
|
'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'JobTemplateService',
|
||||||
function(
|
function(
|
||||||
$scope, $rootScope, $location, $stateParams, Rest, Alert,
|
$scope, $rootScope, $location, $stateParams, Rest, Alert,
|
||||||
JobTemplateList, Prompt, ClearScope, ProcessErrors, GetBasePath,
|
JobTemplateList, Prompt, ClearScope, ProcessErrors, GetBasePath,
|
||||||
InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService
|
InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService, JobTemplateService
|
||||||
) {
|
) {
|
||||||
ClearScope();
|
ClearScope();
|
||||||
|
|
||||||
var list = JobTemplateList,
|
var list = JobTemplateList;
|
||||||
defaultUrl = GetBasePath('job_templates');
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
@@ -42,43 +41,113 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Al
|
|||||||
$state.go('jobTemplates.add');
|
$state.go('jobTemplates.add');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.editJobTemplate = function(id) {
|
$scope.editJobTemplate = function(template) {
|
||||||
$state.go('jobTemplates.edit', { job_template_id: id });
|
if(template) {
|
||||||
|
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||||
|
$state.transitionTo('templates.editJobTemplate', {job_template_id: template.id});
|
||||||
|
}
|
||||||
|
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||||
|
$state.transitionTo('templates.editWorkflowJobTemplate', {workflow_job_template_id: template.id});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Something went wrong - Let the user know that we're unable to launch because we don't know
|
||||||
|
// what type of job template this is
|
||||||
|
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to edit.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Alert('Error: Unable to edit template', 'Template parameter is missing');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteJobTemplate = function(id, name) {
|
$scope.deleteJobTemplate = function(template) {
|
||||||
var action = function() {
|
if(template) {
|
||||||
$('#prompt-modal').modal('hide');
|
Prompt({
|
||||||
Wait('start');
|
hdr: 'Delete',
|
||||||
var url = defaultUrl + id + '/';
|
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the ' + (template.type === "Workflow Job Template" ? 'workflow ' : '') + 'job template below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(template.name) + '</div>',
|
||||||
Rest.setUrl(url);
|
action: function() {
|
||||||
Rest.destroy()
|
|
||||||
.success(function() {
|
function handleSuccessfulDelete() {
|
||||||
$state.go('^', null, { reload: true });
|
// TODO: look at this
|
||||||
})
|
if (parseInt($state.params.id) === template.id) {
|
||||||
.error(function(data) {
|
$state.go("^", null, {reload: true});
|
||||||
Wait('stop');
|
} else {
|
||||||
ProcessErrors($scope, data, status, null, {
|
$state.go(".", null, {reload: true});
|
||||||
hdr: 'Error!',
|
}
|
||||||
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status
|
Wait('stop');
|
||||||
});
|
}
|
||||||
|
|
||||||
|
$('#prompt-modal').modal('hide');
|
||||||
|
Wait('start');
|
||||||
|
if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||||
|
JobTemplateService.deleteWorkflowJobTemplate(template.id)
|
||||||
|
.then(function () {
|
||||||
|
handleSuccessfulDelete();
|
||||||
|
}, function (data) {
|
||||||
|
Wait('stop');
|
||||||
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
|
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + status });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||||
|
JobTemplateService.deleteJobTemplate(template.id)
|
||||||
|
.then(function () {
|
||||||
|
handleSuccessfulDelete();
|
||||||
|
}, function (data) {
|
||||||
|
Wait('stop');
|
||||||
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
|
msg: 'Call to delete job template failed. DELETE returned status: ' + status });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Wait('stop');
|
||||||
|
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while deleting.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actionText: 'DELETE'
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
else {
|
||||||
Prompt({
|
Alert('Error: Unable to delete template', 'Template parameter is missing');
|
||||||
hdr: 'Delete',
|
}
|
||||||
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the job template below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(name) + '</div>',
|
|
||||||
action: action,
|
|
||||||
actionText: 'DELETE'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submitJob = function(id) {
|
$scope.submitJob = function(template) {
|
||||||
InitiatePlaybookRun({ scope: $scope, id: id });
|
if(template) {
|
||||||
|
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||||
|
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'job_template' });
|
||||||
|
}
|
||||||
|
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||||
|
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Something went wrong - Let the user know that we're unable to launch because we don't know
|
||||||
|
// what type of job template this is
|
||||||
|
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Alert('Error: Unable to launch template', 'Template parameter is missing');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.scheduleJob = function(id) {
|
$scope.scheduleJob = function(template) {
|
||||||
$state.go('jobTemplateSchedules', { id: id });
|
if(template) {
|
||||||
|
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||||
|
$state.go('jobTemplateSchedules', {id: template.id});
|
||||||
|
}
|
||||||
|
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||||
|
$state.go('workflowJobTemplateSchedules', {id: template.id});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Something went wrong - Let the user know that we're unable to redirect to schedule because we don't know
|
||||||
|
// what type of job template this is
|
||||||
|
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to schedule.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Alert('Error: Unable to schedule job', 'Template parameter is missing');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'templates',
|
||||||
|
route: '/templates',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: "TEMPLATES"
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
unified_job_templates_search: {
|
||||||
|
value: {
|
||||||
|
type: 'workflow_job_template,job_template'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchPrefix: 'unified_job_templates',
|
||||||
|
views: {
|
||||||
|
'@': {
|
||||||
|
controller: 'JobTemplatesListController',
|
||||||
|
templateProvider: function(JobTemplateList, generateList) {
|
||||||
|
let html = generateList.build({
|
||||||
|
list: JobTemplateList,
|
||||||
|
mode: 'edit'
|
||||||
|
});
|
||||||
|
html = generateList.wrapPanel(html);
|
||||||
|
return generateList.insertFormView() + html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
Dataset: ['JobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||||
|
function(list, qs, $stateParams, GetBasePath) {
|
||||||
|
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||||
|
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,4 +8,4 @@ import controller from './job-templates-list.controller';
|
|||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobTemplatesList', [])
|
angular.module('jobTemplatesList', [])
|
||||||
.controller('JobTemplatesList', controller);
|
.controller('JobTemplatesListController', controller);
|
||||||
|
|||||||
@@ -4,47 +4,98 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
import deleteJobTemplate from './delete-job-template.service';
|
import jobTemplateService from './job-template.service';
|
||||||
|
|
||||||
import surveyMaker from './survey-maker/main';
|
import surveyMaker from './survey-maker/main';
|
||||||
import jobTemplatesList from './list/main';
|
import jobTemplatesList from './list/main';
|
||||||
import jobTemplatesAdd from './add/main';
|
import jobTemplatesAdd from './add-job-template/main';
|
||||||
import jobTemplatesEdit from './edit/main';
|
import jobTemplatesEdit from './edit-job-template/main';
|
||||||
import jobTemplatesCopy from './copy/main';
|
import jobTemplatesCopy from './copy/main';
|
||||||
|
import workflowAdd from './add-workflow/main';
|
||||||
|
import workflowEdit from './edit-workflow/main';
|
||||||
import labels from './labels/main';
|
import labels from './labels/main';
|
||||||
|
import workflowChart from './workflow-chart/main';
|
||||||
|
import workflowMaker from './workflow-maker/main';
|
||||||
|
import jobTemplatesListRoute from './list/job-templates-list.route';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobTemplates', [surveyMaker.name, jobTemplatesList.name, jobTemplatesAdd.name,
|
angular.module('jobTemplates', [surveyMaker.name, jobTemplatesList.name, jobTemplatesAdd.name,
|
||||||
jobTemplatesEdit.name, jobTemplatesCopy.name, labels.name
|
jobTemplatesEdit.name, jobTemplatesCopy.name, labels.name, workflowAdd.name, workflowEdit.name,
|
||||||
|
workflowChart.name, workflowMaker.name
|
||||||
])
|
])
|
||||||
.service('deleteJobTemplate', deleteJobTemplate)
|
.service('JobTemplateService', jobTemplateService)
|
||||||
.config(['$stateProvider', 'stateDefinitionsProvider',
|
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
|
||||||
function($stateProvider, stateDefinitionsProvider) {
|
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider ) {
|
||||||
let stateDefinitions = stateDefinitionsProvider.$get();
|
let stateTree, addJobTemplate, editJobTemplate, addWorkflow, editWorkflow,
|
||||||
|
stateDefinitions = stateDefinitionsProvider.$get(),
|
||||||
|
stateExtender = $stateExtenderProvider.$get();
|
||||||
|
|
||||||
$stateProvider.state({
|
function generateStateTree() {
|
||||||
name: 'jobTemplates',
|
|
||||||
url: '/job_templates',
|
addJobTemplate = stateDefinitions.generateTree({
|
||||||
lazyLoad: () => stateDefinitions.generateTree({
|
name: 'templates.addJobTemplate',
|
||||||
parent: 'jobTemplates',
|
url: '/add_job_template',
|
||||||
modes: ['add', 'edit'],
|
modes: ['add'],
|
||||||
list: 'JobTemplateList',
|
|
||||||
form: 'JobTemplateForm',
|
form: 'JobTemplateForm',
|
||||||
controllers: {
|
controllers: {
|
||||||
list: 'JobTemplatesList',
|
add: 'JobTemplateAdd'
|
||||||
add: 'JobTemplatesAdd',
|
}
|
||||||
edit: 'JobTemplatesEdit'
|
});
|
||||||
},
|
|
||||||
data: {
|
editJobTemplate = stateDefinitions.generateTree({
|
||||||
activityStream: true,
|
name: 'templates.editJobTemplate',
|
||||||
activityStreamTarget: 'job_template',
|
url: '/job_template/:job_template_id',
|
||||||
socket: {
|
modes: ['edit'],
|
||||||
"groups": {
|
form: 'JobTemplateForm',
|
||||||
"jobs": ["status_changed"]
|
controllers: {
|
||||||
}
|
edit: 'JobTemplateEdit'
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
})
|
|
||||||
});
|
addWorkflow = stateDefinitions.generateTree({
|
||||||
|
name: 'templates.addWorkflowJobTemplate',
|
||||||
|
url: '/add_workflow_job_template',
|
||||||
|
modes: ['add'],
|
||||||
|
form: 'WorkflowForm',
|
||||||
|
controllers: {
|
||||||
|
add: 'WorkflowAdd'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editWorkflow = stateDefinitions.generateTree({
|
||||||
|
name: 'templates.editWorkflowJobTemplate',
|
||||||
|
url: '/workflow_job_template/:workflow_job_template_id',
|
||||||
|
modes: ['edit'],
|
||||||
|
form: 'WorkflowForm',
|
||||||
|
controllers: {
|
||||||
|
edit: 'WorkflowEdit'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
addJobTemplate,
|
||||||
|
editJobTemplate,
|
||||||
|
addWorkflow,
|
||||||
|
editWorkflow
|
||||||
|
]).then((generated) => {
|
||||||
|
return {
|
||||||
|
states: _.reduce(generated, (result, definition) => {
|
||||||
|
return result.concat(definition.states);
|
||||||
|
}, [
|
||||||
|
stateExtender.buildDefinition(jobTemplatesListRoute)
|
||||||
|
|
||||||
|
])
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stateTree = {
|
||||||
|
name: 'templates',
|
||||||
|
url: '/templates',
|
||||||
|
lazyLoad: () => generateStateTree()
|
||||||
|
};
|
||||||
|
|
||||||
|
$stateProvider.state(stateTree);
|
||||||
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|||||||
11
awx/ui/client/src/job-templates/workflow-chart/main.js
Normal file
11
awx/ui/client/src/job-templates/workflow-chart/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import workflowChart from './workflow-chart.directive';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('jobTemplatesWorkflowChart', [])
|
||||||
|
.directive('workflowChart', workflowChart);
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
.WorkflowChart-isActiveEdit {
|
||||||
|
stroke: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeConnector circle, .nodeConnector .linkCross, .node .addCircle, .node .removeCircle, .node .WorkflowChart-hoverPath {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .addCircle, .nodeConnector .addCircle {
|
||||||
|
fill: #5CB85C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addCircle.addHovering {
|
||||||
|
fill: #449D44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .removeCircle {
|
||||||
|
fill: #D9534F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeCircle.removeHovering {
|
||||||
|
fill: #C9302C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .WorkflowChart-defaultText {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
fill: #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .rect {
|
||||||
|
fill: #FCFCFC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rect.placeholder {
|
||||||
|
stroke-dasharray: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .transparentRect {
|
||||||
|
fill: #FFFFFF;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowChart-alwaysShowAdd circle,
|
||||||
|
.WorkflowChart-alwaysShowAdd path,
|
||||||
|
.WorkflowChart-alwaysShowAdd .linkCross,
|
||||||
|
.hovering .addCircle,
|
||||||
|
.hovering .removeCircle,
|
||||||
|
.hovering .WorkflowChart-hoverPath,
|
||||||
|
.hovering .linkCross {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link.placeholder {
|
||||||
|
stroke-dasharray: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowChart-svg {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
.WorkflowChart-nodeTypeCircle {
|
||||||
|
fill: #848992;
|
||||||
|
}
|
||||||
|
.WorkflowChart-nodeTypeLetter {
|
||||||
|
fill: #FFFFFF;
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default [
|
||||||
|
function() {
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
treeData: '=',
|
||||||
|
addNode: '&',
|
||||||
|
editNode: '&',
|
||||||
|
deleteNode: '&'
|
||||||
|
},
|
||||||
|
restrict: 'E',
|
||||||
|
link: function(scope, element) {
|
||||||
|
|
||||||
|
var margin = {top: 20, right: 20, bottom: 20, left: 20},
|
||||||
|
width = 950,
|
||||||
|
height = 590 - margin.top - margin.bottom,
|
||||||
|
i = 0,
|
||||||
|
rectW = 120,
|
||||||
|
rectH = 60,
|
||||||
|
rootW = 60,
|
||||||
|
rootH = 40;
|
||||||
|
|
||||||
|
var tree = d3.layout.tree()
|
||||||
|
.size([height, width]);
|
||||||
|
|
||||||
|
var line = d3.svg.line()
|
||||||
|
.x(function(d){return d.x;})
|
||||||
|
.y(function(d){return d.y;});
|
||||||
|
|
||||||
|
function lineData(d){
|
||||||
|
|
||||||
|
var sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + rectW;
|
||||||
|
var sourceY = d.source.isStartNode ? d.source.x + 10 + rootH / 2 : d.source.x + rectH / 2;
|
||||||
|
var targetX = d.target.y;
|
||||||
|
var targetY = d.target.x + rectH / 2;
|
||||||
|
|
||||||
|
var points = [
|
||||||
|
{
|
||||||
|
x: sourceX,
|
||||||
|
y: sourceY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: targetX,
|
||||||
|
y: targetY
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return line(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this function is hacky and we need to come up with a better solution
|
||||||
|
// see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
|
||||||
|
function wrap(text) {
|
||||||
|
if(text && text.length > 15) {
|
||||||
|
return text.substring(0,15) + '...';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var svg = d3.select(element[0]).append("svg")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height)
|
||||||
|
.attr("class", "WorkflowChart-svg")
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
|
var node = svg.selectAll(".node"),
|
||||||
|
link = svg.selectAll(".link");
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
// Declare the nodes
|
||||||
|
var nodes = tree.nodes(scope.treeData);
|
||||||
|
node = node.data(nodes, function(d) { d.y = d.depth * 180; return d.id || (d.id = ++i); });
|
||||||
|
link = link.data(tree.links(nodes), function(d) { return d.source.id + "-" + d.target.id; });
|
||||||
|
|
||||||
|
var nodeEnter = node.enter().append("g")
|
||||||
|
.attr("class", "node")
|
||||||
|
.attr("id", function(d){return "node-" + d.id;})
|
||||||
|
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
|
||||||
|
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
||||||
|
.attr("fill", "red");
|
||||||
|
|
||||||
|
nodeEnter.each(function(d) {
|
||||||
|
var thisNode = d3.select(this);
|
||||||
|
if(d.isStartNode) {
|
||||||
|
thisNode.append("rect")
|
||||||
|
.attr("width", 60)
|
||||||
|
.attr("height", 40)
|
||||||
|
.attr("y", 10)
|
||||||
|
.attr("rx", 5)
|
||||||
|
.attr("ry", 5)
|
||||||
|
.attr("fill", "#5cb85c")
|
||||||
|
.attr("class", "WorkflowChart-rootNode")
|
||||||
|
.call(add_node);
|
||||||
|
thisNode.append("path")
|
||||||
|
.style("fill", "white")
|
||||||
|
.attr("transform", function() { return "translate(" + 30 + "," + 30 + ")"; })
|
||||||
|
.attr("d", d3.svg.symbol()
|
||||||
|
.size(120)
|
||||||
|
.type("cross")
|
||||||
|
)
|
||||||
|
.call(add_node);
|
||||||
|
thisNode.append("text")
|
||||||
|
.attr("x", 14)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("dy", ".35em")
|
||||||
|
.attr("class", "WorkflowChart-defaultText")
|
||||||
|
.text(function () { return "START"; });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
thisNode.append("rect")
|
||||||
|
.attr("width", rectW)
|
||||||
|
.attr("height", rectH)
|
||||||
|
.attr("rx", 5)
|
||||||
|
.attr("ry", 5)
|
||||||
|
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; })
|
||||||
|
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; })
|
||||||
|
.attr("class", function(d) {
|
||||||
|
return d.placeholder ? "rect placeholder" : "rect";
|
||||||
|
});
|
||||||
|
thisNode.append("text")
|
||||||
|
.attr("x", rectW / 2)
|
||||||
|
.attr("y", rectH / 2)
|
||||||
|
.attr("dy", ".35em")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("class", "WorkflowChart-defaultText WorkflowChart-nameText")
|
||||||
|
.text(function (d) {
|
||||||
|
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
||||||
|
}).each(wrap);
|
||||||
|
|
||||||
|
thisNode.append("circle")
|
||||||
|
.attr("cy", rectH)
|
||||||
|
.attr("r", 10)
|
||||||
|
.attr("class", "WorkflowChart-nodeTypeCircle")
|
||||||
|
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||||
|
|
||||||
|
thisNode.append("text")
|
||||||
|
.attr("y", rectH)
|
||||||
|
.attr("dy", ".35em")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("class", "WorkflowChart-nodeTypeLetter")
|
||||||
|
.text(function (d) {
|
||||||
|
return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : "");
|
||||||
|
})
|
||||||
|
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||||
|
|
||||||
|
thisNode.append("rect")
|
||||||
|
.attr("width", rectW)
|
||||||
|
.attr("height", rectH)
|
||||||
|
.attr("class", "transparentRect")
|
||||||
|
.call(edit_node)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
if(!d.isStartNode) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
if(!d.isStartNode) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thisNode.append("circle")
|
||||||
|
.attr("id", function(d){return "node-" + d.id + "-add";})
|
||||||
|
.attr("cx", rectW)
|
||||||
|
.attr("r", 10)
|
||||||
|
.attr("class", "addCircle nodeCircle")
|
||||||
|
.style("display", function(d) { return d.placeholder ? "none" : null; })
|
||||||
|
.call(add_node)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#node-" + d.id + "-add")
|
||||||
|
.classed("addHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#node-" + d.id + "-add")
|
||||||
|
.classed("addHovering", false);
|
||||||
|
});
|
||||||
|
thisNode.append("path")
|
||||||
|
.attr("class", "nodeAddCross WorkflowChart-hoverPath")
|
||||||
|
.style("fill", "white")
|
||||||
|
.attr("transform", function() { return "translate(" + rectW + "," + 0 + ")"; })
|
||||||
|
.attr("d", d3.svg.symbol()
|
||||||
|
.size(60)
|
||||||
|
.type("cross")
|
||||||
|
)
|
||||||
|
.style("display", function(d) { return d.placeholder ? "none" : null; })
|
||||||
|
.call(add_node)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#node-" + d.id + "-add")
|
||||||
|
.classed("addHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#node-" + d.id + "-add")
|
||||||
|
.classed("addHovering", false);
|
||||||
|
});
|
||||||
|
thisNode.append("circle")
|
||||||
|
.attr("id", function(d){return "node-" + d.id + "-remove";})
|
||||||
|
.attr("cx", rectW)
|
||||||
|
.attr("cy", rectH)
|
||||||
|
.attr("r", 10)
|
||||||
|
.attr("class", "removeCircle")
|
||||||
|
.style("display", function(d) { return (d.canDelete === false || d.placeholder) ? "none" : null; })
|
||||||
|
.call(remove_node)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#node-" + d.id + "-remove")
|
||||||
|
.classed("removeHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#node-" + d.id + "-remove")
|
||||||
|
.classed("removeHovering", false);
|
||||||
|
});
|
||||||
|
thisNode.append("path")
|
||||||
|
.attr("class", "nodeRemoveCross WorkflowChart-hoverPath")
|
||||||
|
.style("fill", "white")
|
||||||
|
.attr("transform", function() { return "translate(" + rectW + "," + rectH + ") rotate(-45)"; })
|
||||||
|
.attr("d", d3.svg.symbol()
|
||||||
|
.size(60)
|
||||||
|
.type("cross")
|
||||||
|
)
|
||||||
|
.style("display", function(d) { return (d.canDelete === false || d.placeholder) ? "none" : null; })
|
||||||
|
.call(remove_node)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#node-" + d.id + "-remove")
|
||||||
|
.classed("removeHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#node-" + d.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#node-" + d.id + "-remove")
|
||||||
|
.classed("removeHovering", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
node.exit().remove();
|
||||||
|
|
||||||
|
var linkEnter = link.enter().append("g")
|
||||||
|
.attr("class", "nodeConnector")
|
||||||
|
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
|
||||||
|
|
||||||
|
// Add entering links in the parent’s old position.
|
||||||
|
linkEnter.insert("path", ".node")
|
||||||
|
.attr("class", function(d) {
|
||||||
|
return (d.source.placeholder || d.target.placeholder) ? "link placeholder" : "link";
|
||||||
|
})
|
||||||
|
.attr("d", lineData)
|
||||||
|
.attr('stroke', function(d) {
|
||||||
|
if(d.target.edgeType) {
|
||||||
|
if(d.target.edgeType === "failure") {
|
||||||
|
return "#d9534f";
|
||||||
|
}
|
||||||
|
else if(d.target.edgeType === "success") {
|
||||||
|
return "#5cb85c";
|
||||||
|
}
|
||||||
|
else if(d.target.edgeType === "always"){
|
||||||
|
return "#337ab7";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "#D7D7D7";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
linkEnter.append("circle")
|
||||||
|
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";})
|
||||||
|
.attr("cx", function(d) {
|
||||||
|
return (d.target.y + d.source.y + rectW) / 2;
|
||||||
|
})
|
||||||
|
.attr("cy", function(d) {
|
||||||
|
return (d.target.x + d.source.x + rectH) / 2;
|
||||||
|
})
|
||||||
|
.attr("r", 10)
|
||||||
|
.attr("class", "addCircle linkCircle")
|
||||||
|
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||||
|
.call(add_node_between)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||||
|
.classed("addHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||||
|
.classed("addHovering", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
linkEnter.append("path")
|
||||||
|
.attr("class", "linkCross")
|
||||||
|
.style("fill", "white")
|
||||||
|
.attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; })
|
||||||
|
.attr("d", d3.svg.symbol()
|
||||||
|
.size(60)
|
||||||
|
.type("cross")
|
||||||
|
)
|
||||||
|
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||||
|
.call(add_node_between)
|
||||||
|
.on("mouseover", function(d) {
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||||
|
.classed("hovering", true);
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||||
|
.classed("addHovering", true);
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d){
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||||
|
.classed("hovering", false);
|
||||||
|
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||||
|
.classed("addHovering", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
link.exit().remove();
|
||||||
|
|
||||||
|
// Transition nodes and links to their new positions.
|
||||||
|
var t = svg.transition();
|
||||||
|
|
||||||
|
t.selectAll(".nodeCircle")
|
||||||
|
.style("display", function(d) { return d.placeholder ? "none" : null; });
|
||||||
|
|
||||||
|
t.selectAll(".nodeAddCross")
|
||||||
|
.style("display", function(d) { return d.placeholder ? "none" : null; });
|
||||||
|
|
||||||
|
t.selectAll(".removeCircle")
|
||||||
|
.style("display", function(d) { return (d.canDelete === false || d.placeholder) ? "none" : null; });
|
||||||
|
|
||||||
|
t.selectAll(".nodeRemoveCross")
|
||||||
|
.style("display", function(d) { return (d.canDelete === false || d.placeholder) ? "none" : null; });
|
||||||
|
|
||||||
|
t.selectAll(".link")
|
||||||
|
.attr("class", function(d) {
|
||||||
|
return (d.source.placeholder || d.target.placeholder) ? "link placeholder" : "link";
|
||||||
|
})
|
||||||
|
.attr("d", lineData)
|
||||||
|
.attr('stroke', function(d) {
|
||||||
|
if(d.target.edgeType) {
|
||||||
|
if(d.target.edgeType === "failure") {
|
||||||
|
return "#d9534f";
|
||||||
|
}
|
||||||
|
else if(d.target.edgeType === "success") {
|
||||||
|
return "#5cb85c";
|
||||||
|
}
|
||||||
|
else if(d.target.edgeType === "always"){
|
||||||
|
return "#337ab7";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "#D7D7D7";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
t.selectAll(".linkCircle")
|
||||||
|
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||||
|
.attr("cx", function(d) {
|
||||||
|
return (d.target.y + d.source.y + rectW) / 2;
|
||||||
|
})
|
||||||
|
.attr("cy", function(d) {
|
||||||
|
return (d.target.x + d.source.x + rectH) / 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.selectAll(".linkCross")
|
||||||
|
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||||
|
.attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; });
|
||||||
|
|
||||||
|
t.selectAll(".rect")
|
||||||
|
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; })
|
||||||
|
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; })
|
||||||
|
.attr("class", function(d) {
|
||||||
|
return d.placeholder ? "rect placeholder" : "rect";
|
||||||
|
});
|
||||||
|
|
||||||
|
t.selectAll(".WorkflowChart-nameText")
|
||||||
|
.text(function (d) {
|
||||||
|
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
t.selectAll(".node")
|
||||||
|
.attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; });
|
||||||
|
|
||||||
|
t.selectAll(".WorkflowChart-nodeTypeCircle")
|
||||||
|
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" ) ? null : "none"; });
|
||||||
|
|
||||||
|
t.selectAll(".WorkflowChart-nodeTypeLetter")
|
||||||
|
.text(function (d) {
|
||||||
|
return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : "");
|
||||||
|
})
|
||||||
|
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_node() {
|
||||||
|
this.on("click", function(d) {
|
||||||
|
scope.addNode({
|
||||||
|
parent: d,
|
||||||
|
betweenTwoNodes: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_node_between() {
|
||||||
|
this.on("click", function(d) {
|
||||||
|
scope.addNode({
|
||||||
|
parent: d,
|
||||||
|
betweenTwoNodes: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_node() {
|
||||||
|
this.on("click", function(d) {
|
||||||
|
scope.deleteNode({
|
||||||
|
nodeToDelete: d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit_node() {
|
||||||
|
this.on("click", function(d) {
|
||||||
|
if(d.canEdit){
|
||||||
|
scope.editNode({
|
||||||
|
nodeToEdit: d
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$on('refreshWorkflowChart', function(){
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
11
awx/ui/client/src/job-templates/workflow-maker/main.js
Normal file
11
awx/ui/client/src/job-templates/workflow-maker/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import helper from './workflow-help.service';
|
||||||
|
import workflowMaker from './workflow-maker.directive';
|
||||||
|
import WorkflowMakerController from './workflow-maker.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('jobTemplates.workflowMaker', [])
|
||||||
|
.service('WorkflowHelpService', helper)
|
||||||
|
// In order to test this controller I had to expose it at the module level
|
||||||
|
// like so. Is this correct? Is there a better pattern for doing this?
|
||||||
|
.controller('WorkflowMakerController', WorkflowMakerController)
|
||||||
|
.directive('workflowMaker', workflowMaker);
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
export default ['CreateDialog', 'Wait', '$q', function(CreateDialog, Wait, $q){
|
||||||
|
return {
|
||||||
|
openDialog: function(params){
|
||||||
|
// params.scope
|
||||||
|
|
||||||
|
let deferred = $q.defer();
|
||||||
|
|
||||||
|
if (params.scope.removeWorkflowDialogReady) {
|
||||||
|
params.scope.removeWorkflowDialogReady();
|
||||||
|
}
|
||||||
|
params.scope.removeWorkflowDialogReady = params.scope.$on('WorkflowDialogReady', function() {
|
||||||
|
$('#workflow-modal-dialog').dialog('open');
|
||||||
|
|
||||||
|
deferred.resolve();
|
||||||
|
});
|
||||||
|
Wait('start');
|
||||||
|
CreateDialog({
|
||||||
|
id: 'workflow-modal-dialog',
|
||||||
|
scope: params.scope,
|
||||||
|
width: 1400,
|
||||||
|
height: 720,
|
||||||
|
draggable: false,
|
||||||
|
dialogClass: 'SurveyMaker-dialog',
|
||||||
|
position: ['center',20],
|
||||||
|
onClose: function() {
|
||||||
|
$('#workflow-modal-dialog').empty();
|
||||||
|
},
|
||||||
|
onOpen: function() {
|
||||||
|
Wait('stop');
|
||||||
|
|
||||||
|
// Let the modal height be variable based on the content
|
||||||
|
// and set a uniform padding
|
||||||
|
$('#workflow-modal-dialog').css({'padding': '20px'});
|
||||||
|
|
||||||
|
},
|
||||||
|
_allowInteraction: function(e) {
|
||||||
|
return !!$(e.target).is('.select2-input') || this._super(e);
|
||||||
|
},
|
||||||
|
callback: 'WorkflowDialogReady'
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
closeDialog: function() {
|
||||||
|
$('#workflow-modal-dialog').dialog('destroy');
|
||||||
|
},
|
||||||
|
searchTree: function(params) {
|
||||||
|
// params.element
|
||||||
|
// params.matchingId
|
||||||
|
|
||||||
|
if(params.element.id === params.matchingId){
|
||||||
|
return params.element;
|
||||||
|
}else if (params.element.children && params.element.children.length > 0){
|
||||||
|
let result = null;
|
||||||
|
const thisService = this;
|
||||||
|
_.forEach(params.element.children, function(child) {
|
||||||
|
result = thisService.searchTree({
|
||||||
|
element: child,
|
||||||
|
matchingId: params.matchingId
|
||||||
|
});
|
||||||
|
if(result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
removeNodeFromTree: function(params) {
|
||||||
|
// params.tree
|
||||||
|
// params.nodeToBeDeleted
|
||||||
|
|
||||||
|
let parentNode = this.searchTree({
|
||||||
|
element: params.tree,
|
||||||
|
matchingId: params.nodeToBeDeleted.parent.id
|
||||||
|
});
|
||||||
|
let nodeToBeDeleted = this.searchTree({
|
||||||
|
element: parentNode,
|
||||||
|
matchingId: params.nodeToBeDeleted.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if(nodeToBeDeleted.children) {
|
||||||
|
_.forEach(nodeToBeDeleted.children, function(child) {
|
||||||
|
if(nodeToBeDeleted.isRoot) {
|
||||||
|
child.isRoot = true;
|
||||||
|
child.edgeType = "always";
|
||||||
|
}
|
||||||
|
|
||||||
|
parentNode.children.push(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_.forEach(parentNode.children, function(child, index) {
|
||||||
|
if(child.id === params.nodeToBeDeleted.id) {
|
||||||
|
parentNode.children.splice(index, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addPlaceholderNode: function(params) {
|
||||||
|
// params.parent
|
||||||
|
// params.betweenTwoNodes
|
||||||
|
// params.tree
|
||||||
|
// params.id
|
||||||
|
|
||||||
|
let placeholder = {
|
||||||
|
children: [],
|
||||||
|
c: "#D7D7D7",
|
||||||
|
id: params.id,
|
||||||
|
canDelete: true,
|
||||||
|
canEdit: false,
|
||||||
|
canAddTo: true,
|
||||||
|
placeholder: true,
|
||||||
|
isNew: true,
|
||||||
|
edited: false
|
||||||
|
};
|
||||||
|
|
||||||
|
let parentNode = (params.betweenTwoNodes) ? this.searchTree({element: params.tree, matchingId: params.parent.source.id}) : this.searchTree({element: params.tree, matchingId: params.parent.id});
|
||||||
|
let placeholderRef;
|
||||||
|
|
||||||
|
if(params.betweenTwoNodes) {
|
||||||
|
_.forEach(parentNode.children, function(child, index) {
|
||||||
|
if(child.id === params.parent.target.id) {
|
||||||
|
placeholder.children.push(angular.copy(child));
|
||||||
|
parentNode.children[index] = placeholder;
|
||||||
|
placeholderRef = parentNode.children[index];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(parentNode.children) {
|
||||||
|
parentNode.children.push(placeholder);
|
||||||
|
placeholderRef = parentNode.children[parentNode.children.length - 1];
|
||||||
|
} else {
|
||||||
|
parentNode.children = [placeholder];
|
||||||
|
placeholderRef = parentNode.children[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeholderRef;
|
||||||
|
},
|
||||||
|
getSiblingConnectionTypes: function(params) {
|
||||||
|
// params.parentId
|
||||||
|
// params.tree
|
||||||
|
|
||||||
|
let siblingConnectionTypes = {};
|
||||||
|
|
||||||
|
let parentNode = this.searchTree({
|
||||||
|
element: params.tree,
|
||||||
|
matchingId: params.parentId
|
||||||
|
});
|
||||||
|
|
||||||
|
if(parentNode.children && parentNode.children.length > 0) {
|
||||||
|
// Loop across them and add the types as keys to siblingConnectionTypes
|
||||||
|
_.forEach(parentNode.children, function(child) {
|
||||||
|
if(!child.placeholder && child.edgeType) {
|
||||||
|
siblingConnectionTypes[child.edgeType] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(siblingConnectionTypes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
@import "./client/src/shared/branding/colors.default.less";
|
||||||
|
|
||||||
|
.WorkflowMaker-header {
|
||||||
|
display: flex;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-title {
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
display: flex;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-titleText {
|
||||||
|
color: @list-title-txt;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-exitHolder {
|
||||||
|
justify-content: flex-end;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-exit{
|
||||||
|
cursor:pointer;
|
||||||
|
padding:0px;
|
||||||
|
border: none;
|
||||||
|
height:20px;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color:@default-bg;
|
||||||
|
color:@d7grey;
|
||||||
|
transition: color 0.2s;
|
||||||
|
line-height:1;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-exit:hover{
|
||||||
|
color:@default-icon;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-contentHolder {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #EBEBEB;
|
||||||
|
height: ~"calc(100% - 85px)";
|
||||||
|
}
|
||||||
|
.WorkflowMaker-contentLeft {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-contentRight {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
border-left: 1px solid #EBEBEB;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-buttonHolder {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-saveButton{
|
||||||
|
background-color: @submit-button-bg;
|
||||||
|
color: @submit-button-text;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
padding-left:15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowMaker-saveButton:disabled{
|
||||||
|
background-color: @submit-button-bg-dis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowMaker-saveButton:hover{
|
||||||
|
background-color: @submit-button-bg-hov;
|
||||||
|
color: @submit-button-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowMaker-cancelButton{
|
||||||
|
background-color: @default-bg;
|
||||||
|
color: @btn-txt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid @btn-bord;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
padding-left:15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowMaker-cancelButton:hover{
|
||||||
|
background-color: @btn-bg-hov;
|
||||||
|
color: @btn-txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WorkflowMaker-deleteOverlay {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-deleteModal {
|
||||||
|
height: 200px;
|
||||||
|
width: 600px;
|
||||||
|
background-color: @default-bg;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formTitle {
|
||||||
|
color: @list-title-txt;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formHelp {
|
||||||
|
color: #707070;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formLists {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formTitle {
|
||||||
|
display: flex;
|
||||||
|
color: #707070;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formLabel {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-formElement {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-legend {
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
color: #707070;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-chart {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-legendLeft {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-legendRight {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-legendItem {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-legendItem:not(:last-child) {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-onSuccessLegend {
|
||||||
|
height: 4px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: @submit-button-bg;
|
||||||
|
margin: 18px 5px 18px 0px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-onFailLegend {
|
||||||
|
height: 4px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: #d9534f;
|
||||||
|
margin: 18px 5px 18px 0px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-alwaysLegend {
|
||||||
|
height: 4px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: #337ab7;
|
||||||
|
margin: 18px 5px 18px 0px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-letterCircle{
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #848992;
|
||||||
|
color: #FFF;
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 5px 10px 0px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.WorkflowMaker-totalJobs {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,814 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ '$scope', 'WorkflowHelpService', 'generateList', 'JobTemplateList', 'ProjectList',
|
||||||
|
'GetBasePath', 'SearchInit', 'PaginateInit', 'Wait', 'JobTemplateService',
|
||||||
|
'ProcessErrors', 'InventorySourcesList', 'CreateSelect2', 'WorkflowMakerForm',
|
||||||
|
'GenerateForm', 'LookUpInit', 'InventoryList', 'CredentialList', '$q', '$timeout',
|
||||||
|
function($scope, WorkflowHelpService, GenerateList, JobTemplateList, ProjectList,
|
||||||
|
GetBasePath, SearchInit, PaginateInit, Wait, JobTemplateService,
|
||||||
|
ProcessErrors, InventorySourcesList, CreateSelect2, WorkflowMakerForm,
|
||||||
|
GenerateForm, LookUpInit, InventoryList, CredentialList, $q, $timeout) {
|
||||||
|
|
||||||
|
let form = WorkflowMakerForm(),
|
||||||
|
generator = GenerateForm;
|
||||||
|
|
||||||
|
$scope.workflowMakerFormConfig = {
|
||||||
|
nodeMode: "idle",
|
||||||
|
activeTab: "jobs",
|
||||||
|
formIsValid: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the intial edge type to success
|
||||||
|
$scope.edgeType = "success";
|
||||||
|
|
||||||
|
$scope.job_type_options = [
|
||||||
|
{
|
||||||
|
label: "Run",
|
||||||
|
value: "run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Check",
|
||||||
|
value: "check"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let job_template_url = GetBasePath('job_templates');
|
||||||
|
// TODO: we won't be able to rely on this in the future for security purposes. Will need to come up
|
||||||
|
// with another way to get the list of job templates that have credentials that don't require passwords
|
||||||
|
// on launch
|
||||||
|
job_template_url += "?not__credential__vault_password=ASK¬__credential__password=ASK";
|
||||||
|
//http://localhost:3000/api/v1/job_templates/?not__credential__vault_password=ASK¬__credential__password=ASK
|
||||||
|
|
||||||
|
// Set up the lists for the add/edit node form
|
||||||
|
let jobTemplatesList = _.cloneDeep(JobTemplateList);
|
||||||
|
delete jobTemplatesList.fields.type;
|
||||||
|
delete jobTemplatesList.fields.description;
|
||||||
|
delete jobTemplatesList.fields.smart_status;
|
||||||
|
delete jobTemplatesList.fields.labels;
|
||||||
|
jobTemplatesList.fields.name.columnClass = "col-md-11";
|
||||||
|
jobTemplatesList.name = "workflow_job_templates";
|
||||||
|
|
||||||
|
let project_url = GetBasePath('projects');
|
||||||
|
|
||||||
|
let projectList = _.cloneDeep(ProjectList);
|
||||||
|
delete projectList.fields.status;
|
||||||
|
delete projectList.fields.scm_type;
|
||||||
|
delete projectList.fields.last_updated;
|
||||||
|
projectList.fields.name.columnClass = "col-md-11";
|
||||||
|
projectList.name = "workflow_projects";
|
||||||
|
|
||||||
|
let inventory_sources_url = GetBasePath('inventory_sources');
|
||||||
|
|
||||||
|
let inventorySourcesList = _.cloneDeep(InventorySourcesList);
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$scope.$watchCollection('workflow_job_templates', function () {
|
||||||
|
if($scope.selectedTemplate) {
|
||||||
|
// Loop across the inventories and see if one of them should be "checked"
|
||||||
|
$scope.workflow_job_templates.forEach(function(row, i) {
|
||||||
|
if (row.id === $scope.selectedTemplate.id) {
|
||||||
|
$scope.workflow_job_templates[i].checked = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.workflow_job_templates[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watchCollection('workflow_projects', function () {
|
||||||
|
if($scope.selectedTemplate) {
|
||||||
|
// Loop across the inventories and see if one of them should be "checked"
|
||||||
|
$scope.workflow_projects.forEach(function(row, i) {
|
||||||
|
if (row.id === $scope.selectedTemplate.id) {
|
||||||
|
$scope.workflow_projects[i].checked = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.workflow_projects[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watchCollection('workflow_inventory_sources', function () {
|
||||||
|
if($scope.selectedTemplate) {
|
||||||
|
// Loop across the inventories and see if one of them should be "checked"
|
||||||
|
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||||
|
if (row.id === $scope.selectedTemplate.id) {
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watchGroup(['selectedTemplate', 'edgeType'], function() {
|
||||||
|
if($scope.selectedTemplate && $scope.edgeType) {
|
||||||
|
$scope.workflowMakerFormConfig.formIsValid = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.workflowMakerFormConfig.formIsValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPromptFields() {
|
||||||
|
$scope.credential = null;
|
||||||
|
$scope.credential_name = null;
|
||||||
|
$scope.inventory = null;
|
||||||
|
$scope.inventory_name = null;
|
||||||
|
$scope.job_type = null;
|
||||||
|
$scope.limit = null;
|
||||||
|
$scope.job_tags = null;
|
||||||
|
$scope.skip_tags = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetNodeForm() {
|
||||||
|
$scope.workflowMakerFormConfig.nodeMode = "idle";
|
||||||
|
$scope.showTypeOptions = false;
|
||||||
|
delete $scope.selectedTemplate;
|
||||||
|
delete $scope.workflow_job_templates;
|
||||||
|
delete $scope.workflow_projects;
|
||||||
|
delete $scope.workflow_inventory_sources;
|
||||||
|
delete $scope.placeholderNode;
|
||||||
|
delete $scope.betweenTwoNodes;
|
||||||
|
$scope.nodeBeingEdited = null;
|
||||||
|
$scope.edgeType = "success";
|
||||||
|
$scope.edgeTypeRestriction = null;
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||||
|
|
||||||
|
resetPromptFields();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJobTemplates() {
|
||||||
|
SearchInit({
|
||||||
|
scope: $scope,
|
||||||
|
set: jobTemplatesList.name,
|
||||||
|
list: jobTemplatesList,
|
||||||
|
url: job_template_url
|
||||||
|
});
|
||||||
|
|
||||||
|
PaginateInit({
|
||||||
|
scope: $scope,
|
||||||
|
list: jobTemplatesList,
|
||||||
|
url: job_template_url,
|
||||||
|
mode: 'lookup',
|
||||||
|
pageSize: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.search(JobTemplateList.iterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProjects() {
|
||||||
|
SearchInit({
|
||||||
|
scope: $scope,
|
||||||
|
set: projectList.name,
|
||||||
|
list: projectList,
|
||||||
|
url: project_url
|
||||||
|
});
|
||||||
|
|
||||||
|
PaginateInit({
|
||||||
|
scope: $scope,
|
||||||
|
list: projectList,
|
||||||
|
url: project_url,
|
||||||
|
mode: 'lookup',
|
||||||
|
pageSize: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.search(ProjectList.iterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadInventorySources() {
|
||||||
|
SearchInit({
|
||||||
|
scope: $scope,
|
||||||
|
set: inventorySourcesList.name,
|
||||||
|
list: inventorySourcesList,
|
||||||
|
url: inventory_sources_url
|
||||||
|
});
|
||||||
|
|
||||||
|
PaginateInit({
|
||||||
|
scope: $scope,
|
||||||
|
list: inventorySourcesList,
|
||||||
|
url: inventory_sources_url,
|
||||||
|
mode: 'lookup',
|
||||||
|
pageSize: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.search(InventorySourcesList.iterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.closeWorkflowMaker = function() {
|
||||||
|
// Revert the data to the master which was created when the dialog was opened
|
||||||
|
$scope.treeData.data = angular.copy($scope.treeDataMaster);
|
||||||
|
WorkflowHelpService.closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveWorkflowMaker = function() {
|
||||||
|
WorkflowHelpService.closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ADD NODE FUNCTIONS */
|
||||||
|
|
||||||
|
$scope.startAddNode = function(parent, betweenTwoNodes) {
|
||||||
|
|
||||||
|
if($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||||
|
$scope.cancelNodeForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.workflowMakerFormConfig.nodeMode = "add";
|
||||||
|
$scope.addParent = parent;
|
||||||
|
$scope.betweenTwoNodes = betweenTwoNodes;
|
||||||
|
|
||||||
|
$scope.placeholderNode = WorkflowHelpService.addPlaceholderNode({
|
||||||
|
parent: parent,
|
||||||
|
betweenTwoNodes: betweenTwoNodes,
|
||||||
|
tree: $scope.treeData.data,
|
||||||
|
id: $scope.treeData.nextIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.treeData.nextIndex++;
|
||||||
|
|
||||||
|
loadJobTemplates();
|
||||||
|
loadProjects();
|
||||||
|
loadInventorySources();
|
||||||
|
|
||||||
|
let siblingConnectionTypes = WorkflowHelpService.getSiblingConnectionTypes({
|
||||||
|
tree: $scope.treeData.data,
|
||||||
|
parentId: betweenTwoNodes ? parent.source.id : parent.id
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
$scope.edgeType = "always";
|
||||||
|
$scope.showTypeOptions = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) {
|
||||||
|
// This is a problem...
|
||||||
|
}
|
||||||
|
else if(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
|
||||||
|
$scope.edgeTypeRestriction = "successFailure";
|
||||||
|
$scope.edgeType = "success";
|
||||||
|
}
|
||||||
|
else if(_.includes(siblingConnectionTypes, "always")) {
|
||||||
|
$scope.edgeTypeRestriction = "always";
|
||||||
|
$scope.edgeType = "always";
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.showTypeOptions = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.confirmNodeForm = function() {
|
||||||
|
if($scope.workflowMakerFormConfig.nodeMode === "add") {
|
||||||
|
if($scope.selectedTemplate && $scope.edgeType) {
|
||||||
|
|
||||||
|
$scope.placeholderNode.unifiedJobTemplate = $scope.selectedTemplate;
|
||||||
|
$scope.placeholderNode.edgeType = $scope.edgeType;
|
||||||
|
if($scope.placeholderNode.unifiedJobTemplate.type === 'job_template') {
|
||||||
|
$scope.placeholderNode.promptValues = {
|
||||||
|
credential: {
|
||||||
|
id: $scope.credential,
|
||||||
|
name: $scope.credential_name
|
||||||
|
},
|
||||||
|
inventory: {
|
||||||
|
id: $scope.inventory,
|
||||||
|
name: $scope.inventory_name
|
||||||
|
},
|
||||||
|
limit: $scope.limit,
|
||||||
|
job_type: $scope.job_type && $scope.job_type.value ? $scope.job_type.value : null,
|
||||||
|
job_tags: $scope.job_tags,
|
||||||
|
skip_tags: $scope.skip_tags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$scope.placeholderNode.canEdit = true;
|
||||||
|
|
||||||
|
delete $scope.placeholderNode.placeholder;
|
||||||
|
|
||||||
|
resetNodeForm();
|
||||||
|
|
||||||
|
// Increment the total node counter
|
||||||
|
$scope.treeData.data.totalNodes++;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if($scope.workflowMakerFormConfig.nodeMode === "edit") {
|
||||||
|
if($scope.selectedTemplate && $scope.edgeType) {
|
||||||
|
$scope.nodeBeingEdited.unifiedJobTemplate = $scope.selectedTemplate;
|
||||||
|
$scope.nodeBeingEdited.edgeType = $scope.edgeType;
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template') {
|
||||||
|
$scope.nodeBeingEdited.promptValues = {
|
||||||
|
credential: {
|
||||||
|
id: $scope.credential,
|
||||||
|
name: $scope.credential_name
|
||||||
|
},
|
||||||
|
inventory: {
|
||||||
|
id: $scope.inventory,
|
||||||
|
name: $scope.inventory_name
|
||||||
|
},
|
||||||
|
limit: $scope.limit,
|
||||||
|
job_type: $scope.job_type && $scope.job_type.value ? $scope.job_type.value : null,
|
||||||
|
job_tags: $scope.job_tags,
|
||||||
|
skip_tags: $scope.skip_tags
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.nodeBeingEdited.isActiveEdit = false;
|
||||||
|
|
||||||
|
$scope.nodeBeingEdited.edited = true;
|
||||||
|
|
||||||
|
resetNodeForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelNodeForm = function() {
|
||||||
|
if($scope.workflowMakerFormConfig.nodeMode === "add") {
|
||||||
|
// Remove the placeholder node from the tree
|
||||||
|
WorkflowHelpService.removeNodeFromTree({
|
||||||
|
tree: $scope.treeData.data,
|
||||||
|
nodeToBeDeleted: $scope.placeholderNode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if($scope.workflowMakerFormConfig.nodeMode === "edit") {
|
||||||
|
$scope.nodeBeingEdited.isActiveEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the form
|
||||||
|
resetNodeForm();
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
};
|
||||||
|
|
||||||
|
/* EDIT NODE FUNCTIONS */
|
||||||
|
|
||||||
|
$scope.startEditNode = function(nodeToEdit) {
|
||||||
|
|
||||||
|
if(!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
|
||||||
|
if($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||||
|
$scope.cancelNodeForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.workflowMakerFormConfig.nodeMode = "edit";
|
||||||
|
|
||||||
|
let parent = WorkflowHelpService.searchTree({
|
||||||
|
element: $scope.treeData.data,
|
||||||
|
matchingId: nodeToEdit.parent.id
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.nodeBeingEdited = WorkflowHelpService.searchTree({
|
||||||
|
element: parent,
|
||||||
|
matchingId: nodeToEdit.id
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.nodeBeingEdited.isActiveEdit = true;
|
||||||
|
|
||||||
|
let finishConfiguringEdit = function() {
|
||||||
|
|
||||||
|
// build any prompt values
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_credential_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.credential) {
|
||||||
|
$scope.credential_name = $scope.nodeBeingEdited.promptValues.credential.name;
|
||||||
|
$scope.credentiial = $scope.nodeBeingEdited.promptValues.credential.id;
|
||||||
|
}
|
||||||
|
else if($scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential) {
|
||||||
|
$scope.credential_name = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.name ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.name : null;
|
||||||
|
$scope.credential = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.id ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.id : null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.credential_name = null;
|
||||||
|
$scope.credential = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_inventory_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.inventory) {
|
||||||
|
$scope.inventory_name = $scope.nodeBeingEdited.promptValues.inventory.name;
|
||||||
|
$scope.inventory = $scope.nodeBeingEdited.promptValues.inventory.id;
|
||||||
|
}
|
||||||
|
else if($scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory) {
|
||||||
|
$scope.inventory_name = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.name ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.name : null;
|
||||||
|
$scope.inventory = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.id ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.id : null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.inventory_name = null;
|
||||||
|
$scope.inventory = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_job_type_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.job_type) {
|
||||||
|
$scope.job_type = {
|
||||||
|
value: $scope.nodeBeingEdited.promptValues.job_type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if($scope.nodeBeingEdited.originalNodeObj.job_type) {
|
||||||
|
$scope.job_type = {
|
||||||
|
value: $scope.nodeBeingEdited.originalNodeObj.job_type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if($scope.nodeBeingEdited.unifiedJobTemplate.job_type) {
|
||||||
|
$scope.job_type = {
|
||||||
|
value: $scope.nodeBeingEdited.unifiedJobTemplate.job_type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.job_type = {
|
||||||
|
value: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default needs to be in place before we can select2-ify the dropdown
|
||||||
|
$timeout(function() {
|
||||||
|
CreateSelect2({
|
||||||
|
element: '#workflow_maker_job_type',
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_limit_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.limit === 'string') {
|
||||||
|
$scope.limit = $scope.nodeBeingEdited.promptValues.limit;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.originalNodeObj.limit === 'string') {
|
||||||
|
$scope.limit = $scope.nodeBeingEdited.originalNodeObj.limit;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.unifiedJobTemplate.limit === 'string') {
|
||||||
|
$scope.limit = $scope.nodeBeingEdited.unifiedJobTemplate.limit;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.limit = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_skip_tags_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.skip_tags === 'string') {
|
||||||
|
$scope.skip_tags = $scope.nodeBeingEdited.promptValues.skip_tags;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.originalNodeObj.skip_tags === 'string') {
|
||||||
|
$scope.skip_tags = $scope.nodeBeingEdited.originalNodeObj.skip_tags;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.unifiedJobTemplate.skip_tags === 'string') {
|
||||||
|
$scope.skip_tags = $scope.nodeBeingEdited.unifiedJobTemplate.skip_tags;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.skip_tags = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_tags_on_launch) {
|
||||||
|
if($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.job_tags === 'string') {
|
||||||
|
$scope.job_tags = $scope.nodeBeingEdited.promptValues.job_tags;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.originalNodeObj.job_tags === 'string') {
|
||||||
|
$scope.job_tags = $scope.nodeBeingEdited.originalNodeObj.job_tags;
|
||||||
|
}
|
||||||
|
else if(typeof $scope.nodeBeingEdited.unifiedJobTemplate.job_tags === 'string') {
|
||||||
|
$scope.job_tags = $scope.nodeBeingEdited.unifiedJobTemplate.job_tags;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.job_tags = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.type === "job_template") {
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectedTemplate = $scope.nodeBeingEdited.unifiedJobTemplate;
|
||||||
|
|
||||||
|
switch($scope.nodeBeingEdited.unifiedJobTemplate.type) {
|
||||||
|
case "job_template":
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||||
|
break;
|
||||||
|
case "project":
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = "project_sync";
|
||||||
|
break;
|
||||||
|
case "inventory_source":
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = "inventory_sync";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadJobTemplates();
|
||||||
|
loadProjects();
|
||||||
|
loadInventorySources();
|
||||||
|
|
||||||
|
$scope.edgeType = $scope.nodeBeingEdited.edgeType;
|
||||||
|
$scope.showTypeOptions = (parent && parent.isStartNode) ? false : true;
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine whether or not we need to go out and GET this nodes unified job template
|
||||||
|
// in order to determine whether or not prompt fields are needed
|
||||||
|
|
||||||
|
if(!$scope.nodeBeingEdited.isNew && !$scope.nodeBeingEdited.edited && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type === 'job') {
|
||||||
|
// This is a node that we got back from the api with an incomplete
|
||||||
|
// unified job template so we're going to pull down the whole object
|
||||||
|
|
||||||
|
JobTemplateService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
|
||||||
|
.then(function(data){
|
||||||
|
|
||||||
|
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
|
||||||
|
|
||||||
|
let defers = [];
|
||||||
|
let retrievingCredential = false;
|
||||||
|
let retrievingInventory = false;
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_credential_on_launch && $scope.nodeBeingEdited.originalNodeObj.credential) {
|
||||||
|
defers.push(JobTemplateService.getCredential($scope.nodeBeingEdited.originalNodeObj.credential));
|
||||||
|
retrievingCredential = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeBeingEdited.unifiedJobTemplate.ask_inventory_on_launch && $scope.nodeBeingEdited.originalNodeObj.inventory) {
|
||||||
|
defers.push(JobTemplateService.getInventory($scope.nodeBeingEdited.originalNodeObj.inventory));
|
||||||
|
retrievingInventory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q.all(defers)
|
||||||
|
.then(function(responses) {
|
||||||
|
if(retrievingCredential) {
|
||||||
|
$scope.credential = responses[0].data.id;
|
||||||
|
$scope.credential_name = responses[0].data.name;
|
||||||
|
$scope.nodeBeingEdited.promptValues.credential = {
|
||||||
|
name: responses[0].data.name,
|
||||||
|
id: responses[0].data.id
|
||||||
|
};
|
||||||
|
|
||||||
|
if(retrievingInventory) {
|
||||||
|
$scope.inventory = responses[1].data.id;
|
||||||
|
$scope.inventory_name = responses[1].data.name;
|
||||||
|
$scope.nodeBeingEdited.promptValues.inventory = {
|
||||||
|
name: responses[1].data.name,
|
||||||
|
id: responses[1].data.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(retrievingInventory) {
|
||||||
|
$scope.inventory = responses[0].data.id;
|
||||||
|
$scope.inventory_name = responses[0].data.name;
|
||||||
|
$scope.nodeBeingEdited.promptValues.inventory = {
|
||||||
|
name: responses[0].data.name,
|
||||||
|
id: responses[0].data.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finishConfiguringEdit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}, function(error) {
|
||||||
|
ProcessErrors($scope, error.data, error.status, form, {
|
||||||
|
hdr: 'Error!',
|
||||||
|
msg: 'Failed to get unified job template. GET returned ' +
|
||||||
|
'status: ' + error.status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
finishConfiguringEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/* DELETE NODE FUNCTIONS */
|
||||||
|
|
||||||
|
function resetDeleteNode() {
|
||||||
|
$scope.nodeToBeDeleted = null;
|
||||||
|
$scope.deleteOverlayVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.startDeleteNode = function(nodeToDelete) {
|
||||||
|
$scope.nodeToBeDeleted = nodeToDelete;
|
||||||
|
$scope.deleteOverlayVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelDeleteNode = function() {
|
||||||
|
resetDeleteNode();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.confirmDeleteNode = function() {
|
||||||
|
if($scope.nodeToBeDeleted) {
|
||||||
|
|
||||||
|
// TODO: turn this into a promise so that we can handle errors
|
||||||
|
|
||||||
|
WorkflowHelpService.removeNodeFromTree({
|
||||||
|
tree: $scope.treeData.data,
|
||||||
|
nodeToBeDeleted: $scope.nodeToBeDeleted
|
||||||
|
});
|
||||||
|
|
||||||
|
if($scope.nodeToBeDeleted.isNew !== true) {
|
||||||
|
$scope.treeData.data.deletedNodes.push($scope.nodeToBeDeleted.nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.nodeToBeDeleted.isActiveEdit) {
|
||||||
|
resetNodeForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDeleteNode();
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
|
||||||
|
$scope.treeData.data.totalNodes--;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggleFormTab = function(tab) {
|
||||||
|
if($scope.workflowMakerFormConfig.activeTab !== tab) {
|
||||||
|
$scope.workflowMakerFormConfig.activeTab = tab;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggle_job_template = function(id) {
|
||||||
|
|
||||||
|
$scope.workflow_projects.forEach(function(row, i) {
|
||||||
|
$scope.workflow_projects[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_job_templates.forEach(function(row, i) {
|
||||||
|
if (row.id === id) {
|
||||||
|
$scope.selectedTemplate = angular.copy(row);
|
||||||
|
if($scope.selectedTemplate.ask_credential_on_launch) {
|
||||||
|
if($scope.selectedTemplate.summary_fields.credential) {
|
||||||
|
$scope.credential_name = $scope.selectedTemplate.summary_fields.credential.name ? $scope.selectedTemplate.summary_fields.credential.name : null;
|
||||||
|
$scope.credential = $scope.selectedTemplate.summary_fields.credential.id ? $scope.selectedTemplate.summary_fields.credential.id : null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.credential_name = null;
|
||||||
|
$scope.credential = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.selectedTemplate.ask_inventory_on_launch) {
|
||||||
|
if($scope.selectedTemplate.summary_fields.inventory) {
|
||||||
|
$scope.inventory_name = $scope.selectedTemplate.summary_fields.inventory.name ? $scope.selectedTemplate.summary_fields.inventory.name : null;
|
||||||
|
$scope.inventory = $scope.selectedTemplate.summary_fields.inventory.id ? $scope.selectedTemplate.summary_fields.inventory.id : null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.inventory_name = null;
|
||||||
|
$scope.inventory = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.selectedTemplate.ask_job_type_on_launch) {
|
||||||
|
$scope.job_type = {
|
||||||
|
value: $scope.selectedTemplate.job_type ? $scope.selectedTemplate.job_type : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// The default needs to be in place before we can select2-ify the dropdown
|
||||||
|
CreateSelect2({
|
||||||
|
element: '#workflow_maker_job_type',
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.selectedTemplate.ask_limit_on_launch) {
|
||||||
|
$scope.limit = $scope.selectedTemplate.limit ? $scope.selectedTemplate.limit : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.selectedTemplate.ask_skip_tags_on_launch) {
|
||||||
|
$scope.skip_tags = $scope.selectedTemplate.skip_tags ? $scope.selectedTemplate.skip_tags : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($scope.selectedTemplate.ask_tags_on_launch) {
|
||||||
|
$scope.job_tags = $scope.selectedTemplate.job_tags ? $scope.selectedTemplate.job_tags : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.workflow_job_templates[i].checked = 1;
|
||||||
|
} else {
|
||||||
|
$scope.workflow_job_templates[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggle_project = function(id) {
|
||||||
|
|
||||||
|
resetPromptFields();
|
||||||
|
|
||||||
|
$scope.workflow_job_templates.forEach(function(row, i) {
|
||||||
|
$scope.workflow_job_templates[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_projects.forEach(function(row, i) {
|
||||||
|
if (row.id === id) {
|
||||||
|
$scope.selectedTemplate = angular.copy(row);
|
||||||
|
$scope.workflow_projects[i].checked = 1;
|
||||||
|
} else {
|
||||||
|
$scope.workflow_projects[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggle_inventory_source = function(id) {
|
||||||
|
|
||||||
|
resetPromptFields();
|
||||||
|
|
||||||
|
$scope.workflow_job_templates.forEach(function(row, i) {
|
||||||
|
$scope.workflow_job_templates[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_projects.forEach(function(row, i) {
|
||||||
|
$scope.workflow_projects[i].checked = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||||
|
if (row.id === id) {
|
||||||
|
$scope.selectedTemplate = angular.copy(row);
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 1;
|
||||||
|
} else {
|
||||||
|
$scope.workflow_inventory_sources[i].checked = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$on('showWorkflowMaker', function(){
|
||||||
|
$scope.treeDataMaster = angular.copy($scope.treeData.data);
|
||||||
|
WorkflowHelpService.openDialog({
|
||||||
|
scope: $scope
|
||||||
|
})
|
||||||
|
.then(function(){
|
||||||
|
|
||||||
|
$scope.$broadcast("refreshWorkflowChart");
|
||||||
|
|
||||||
|
generator.inject(form, {
|
||||||
|
mode: 'add',
|
||||||
|
related: false,
|
||||||
|
scope: $scope,
|
||||||
|
id: 'workflow-maker-form'
|
||||||
|
});
|
||||||
|
|
||||||
|
LookUpInit({
|
||||||
|
scope: $scope,
|
||||||
|
form: form,
|
||||||
|
list: InventoryList,
|
||||||
|
field: 'inventory',
|
||||||
|
input_type: "radio"
|
||||||
|
});
|
||||||
|
|
||||||
|
LookUpInit({
|
||||||
|
url: GetBasePath('credentials') + '?kind=ssh',
|
||||||
|
scope: $scope,
|
||||||
|
form: form,
|
||||||
|
current_item: null,
|
||||||
|
list: CredentialList,
|
||||||
|
field: 'credential',
|
||||||
|
hdr: 'Select Machine Credential',
|
||||||
|
input_type: "radio"
|
||||||
|
});
|
||||||
|
|
||||||
|
GenerateList.inject(jobTemplatesList, {
|
||||||
|
mode: 'lookup',
|
||||||
|
id: 'workflow-jobs-list',
|
||||||
|
scope: $scope,
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
GenerateList.inject(projectList, {
|
||||||
|
mode: 'lookup',
|
||||||
|
id: 'workflow-project-sync-list',
|
||||||
|
scope: $scope,
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
GenerateList.inject(inventorySourcesList, {
|
||||||
|
mode: 'lookup',
|
||||||
|
id: 'workflow-inventory-sync-list',
|
||||||
|
scope: $scope,
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import workflowMakerController from './workflow-maker.controller';
|
||||||
|
|
||||||
|
export default [ 'templateUrl',
|
||||||
|
function(templateUrl) {
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
treeData: '='
|
||||||
|
},
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: templateUrl('job-templates/workflow-maker/workflow-maker'),
|
||||||
|
controller: workflowMakerController
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<div id="workflow-modal-dialog" style="display: none;">
|
||||||
|
<div class="WorkflowMaker-deleteOverlay" ng-show="deleteOverlayVisible">
|
||||||
|
<div class="Modal-content modal-content">
|
||||||
|
<div class="Modal-header">
|
||||||
|
<div class="Modal-title">REMOVE</div>
|
||||||
|
<div class="Modal-exitHolder">
|
||||||
|
<button class="close Modal-exit" ng-click="cancelDeleteNode()">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="Modal-body ng-binding">
|
||||||
|
<div class="Prompt-bodyQuery">Are you sure you want to remove the template below?</div>
|
||||||
|
<div class="Prompt-bodyTarget">{{nodeToBeDeleted.unifiedJobTemplate.name}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="Modal-footer">
|
||||||
|
<button ng-click="cancelDeleteNode()" class="btn Modal-defaultButton Modal-footerButton">CANCEL</a>
|
||||||
|
<button ng-click="confirmDeleteNode()" class="btn Modal-footerButton ng-binding Modal-errorButton">DELETE</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-header">
|
||||||
|
<div class="WorkflowMaker-title">
|
||||||
|
<div class="WorkflowMaker-titleText">EDIT WORKFLOW</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-exitHolder">
|
||||||
|
<button class="WorkflowMaker-exit" ng-click="closeWorkflowMaker()">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-contentHolder">
|
||||||
|
<div class="WorkflowMaker-contentLeft">
|
||||||
|
<div class="WorkflowMaker-legend">
|
||||||
|
<div class="WorkflowMaker-legendLeft">
|
||||||
|
<div class="WorkflowMaker-legendItem">KEY:</div>
|
||||||
|
<div class="WorkflowMaker-legendItem">
|
||||||
|
<div class="WorkflowMaker-onSuccessLegend"></div>
|
||||||
|
<div>On Success</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-legendItem">
|
||||||
|
<div class="WorkflowMaker-onFailLegend"></div>
|
||||||
|
<div>On Fail</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-legendItem">
|
||||||
|
<div class="WorkflowMaker-alwaysLegend"></div>
|
||||||
|
<div>Always</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-legendItem">
|
||||||
|
<div class="WorkflowMaker-letterCircle">P</div>
|
||||||
|
<div>Project Sync</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-legendItem">
|
||||||
|
<div class="WorkflowMaker-letterCircle">I</div>
|
||||||
|
<div>Inventory Sync</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-legendRight">
|
||||||
|
<span class="WorkflowMaker-totalJobs">TOTAL JOBS</span>
|
||||||
|
<span class="badge List-titleBadge" ng-bind="treeData.data.totalNodes"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<workflow-chart tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" delete-node="startDeleteNode(nodeToDelete)" class="WorkflowMaker-chart"></workflow-chart>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-contentRight">
|
||||||
|
<div class="WorkflowMaker-formTitle">{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited && nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "ADD A TEMPLATE"}}</div>
|
||||||
|
<div class="WorkflowMaker-formHelp" ng-show="workflowMakerFormConfig.nodeMode === 'idle'">Please hover over a template and click the Add button.</div>
|
||||||
|
<div class="WorkflowMaker-form" ng-show="workflowMakerFormConfig.nodeMode === 'add' || workflowMakerFormConfig.nodeMode === 'edit'">
|
||||||
|
<div class="Form-tabHolder">
|
||||||
|
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'jobs'}" ng-click="toggleFormTab('jobs')">JOBS</div>
|
||||||
|
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'project_sync'}" ng-click="toggleFormTab('project_sync')">PROJECT SYNC</div>
|
||||||
|
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'inventory_sync'}" ng-click="toggleFormTab('inventory_sync')">INVENTORY SYNC</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-formLists">
|
||||||
|
<div id="workflow-jobs-list" ng-show="workflowMakerFormConfig.activeTab === 'jobs'"></div>
|
||||||
|
<div id="workflow-project-sync-list" ng-show="workflowMakerFormConfig.activeTab === 'project_sync'"></div>
|
||||||
|
<div id="workflow-inventory-sync-list" ng-show="workflowMakerFormConfig.activeTab === 'inventory_sync'"></div>
|
||||||
|
</div>
|
||||||
|
<div ng-show="showTypeOptions" class="WorkflowMaker-formElement">
|
||||||
|
<label class="Form-inputLabelContainer">
|
||||||
|
<span class="Form-inputLabel prepend-asterisk"> TYPE</span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<div ng-hide="edgeTypeRestriction ==='always'">
|
||||||
|
<input type="radio" ng-model="edgeType" id="workflow-type-success" value="success">
|
||||||
|
<label class="WorkflowMaker-formLabel" for="workflow-type-success">On Success</label>
|
||||||
|
</div>
|
||||||
|
<div ng-hide="edgeTypeRestriction ==='always'">
|
||||||
|
<input type="radio" ng-model="edgeType" id="workflow-type-failure" value="failure">
|
||||||
|
<label class="WorkflowMaker-formLabel" for="workflow-type-failure">On Failure</label>
|
||||||
|
</div>
|
||||||
|
<div ng-hide="edgeTypeRestriction ==='successFailure'">
|
||||||
|
<input type="radio" ng-model="edgeType" id="workflow-type-always" value="always">
|
||||||
|
<label class="WorkflowMaker-formLabel" for="workflow-type-always">Always</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="workflow-maker-form"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="WorkflowMaker-buttonHolder">
|
||||||
|
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> Close</button>
|
||||||
|
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()"> Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -13,6 +13,7 @@ import Hosts from "./lists/Hosts";
|
|||||||
import Inventories from "./lists/Inventories";
|
import Inventories from "./lists/Inventories";
|
||||||
import InventoryGroups from "./lists/InventoryGroups";
|
import InventoryGroups from "./lists/InventoryGroups";
|
||||||
import InventoryHosts from "./lists/InventoryHosts";
|
import InventoryHosts from "./lists/InventoryHosts";
|
||||||
|
import InventorySources from "./lists/InventorySources";
|
||||||
import JobEvents from "./lists/JobEvents";
|
import JobEvents from "./lists/JobEvents";
|
||||||
import JobHosts from "./lists/JobHosts";
|
import JobHosts from "./lists/JobHosts";
|
||||||
import JobTemplates from "./lists/JobTemplates";
|
import JobTemplates from "./lists/JobTemplates";
|
||||||
@@ -38,6 +39,7 @@ export
|
|||||||
Inventories,
|
Inventories,
|
||||||
InventoryGroups,
|
InventoryGroups,
|
||||||
InventoryHosts,
|
InventoryHosts,
|
||||||
|
InventorySources,
|
||||||
JobEvents,
|
JobEvents,
|
||||||
JobHosts,
|
JobHosts,
|
||||||
JobTemplates,
|
JobTemplates,
|
||||||
|
|||||||
29
awx/ui/client/src/lists/InventorySources.js
Normal file
29
awx/ui/client/src/lists/InventorySources.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('InventorySourcesListDefinition', [])
|
||||||
|
.value('InventorySourcesList', {
|
||||||
|
|
||||||
|
name: 'workflow_inventory_sources',
|
||||||
|
iterator: 'inventory_source',
|
||||||
|
listTitle: 'Inventory Sources',
|
||||||
|
index: false,
|
||||||
|
hover: true,
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
name: {
|
||||||
|
key: true,
|
||||||
|
label: 'Name',
|
||||||
|
columnClass: 'col-md-11'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {},
|
||||||
|
|
||||||
|
fieldActions: {}
|
||||||
|
});
|
||||||
@@ -10,11 +10,12 @@ export default
|
|||||||
.factory('JobTemplateList', ['i18n', function(i18n) {
|
.factory('JobTemplateList', ['i18n', function(i18n) {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
name: 'job_templates',
|
name: 'templates',
|
||||||
iterator: 'job_template',
|
iterator: 'unified_job_templates',
|
||||||
selectTitle: i18n._('Add Job Template'),
|
basePath: 'unified_job_templates',
|
||||||
editTitle: i18n._('Job Templates'),
|
selectTitle: i18n._('Template'),
|
||||||
listTitle: i18n._('Job Templates'),
|
editTitle: i18n._('Templates'),
|
||||||
|
listTitle: i18n._('Templates'),
|
||||||
selectInstructions: "Click on a row to select it, and click Finished when done. Use the <i class=\"icon-plus\"></i> " +
|
selectInstructions: "Click on a row to select it, and click Finished when done. Use the <i class=\"icon-plus\"></i> " +
|
||||||
"button to create a new job template.",
|
"button to create a new job template.",
|
||||||
index: false,
|
index: false,
|
||||||
@@ -24,7 +25,14 @@ export default
|
|||||||
name: {
|
name: {
|
||||||
key: true,
|
key: true,
|
||||||
label: i18n._('Name'),
|
label: i18n._('Name'),
|
||||||
columnClass: 'col-lg-2 col-md-2 col-sm-4 col-xs-9'
|
columnClass: 'col-lg-2 col-md-2 col-sm-4 col-xs-9',
|
||||||
|
ngHref: "{{job_template.editLink}}"
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
label: i18n._('Type'),
|
||||||
|
searchType: 'select',
|
||||||
|
searchOptions: [], // will be set by Options call to job templates resource
|
||||||
|
columnClass: 'col-lg-2 col-md-2 col-sm-4 hidden-xs'
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
label: i18n._('Description'),
|
label: i18n._('Description'),
|
||||||
@@ -34,80 +42,90 @@ export default
|
|||||||
label: i18n._('Activity'),
|
label: i18n._('Activity'),
|
||||||
columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs',
|
columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs',
|
||||||
nosort: true,
|
nosort: true,
|
||||||
ngInclude: "'/static/partials/job-template-smart-status.html'",
|
//ngInclude: "'/static/partials/job-template-smart-status.html'",
|
||||||
type: 'template'
|
type: 'template'
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
label: i18n._('Labels'),
|
label: i18n._('Labels'),
|
||||||
type: 'labels',
|
type: 'labels',
|
||||||
nosort: true,
|
nosort: true,
|
||||||
columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs'
|
columnClass: 'List-tableCell col-lg-2 col-md-4 hidden-sm hidden-xs'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
add: {
|
add: {
|
||||||
mode: 'all', // One of: edit, select, all
|
mode: 'all', // One of: edit, select, all
|
||||||
ngClick: 'addJobTemplate()',
|
type: 'buttonDropdown',
|
||||||
basePaths: ['job_templates'],
|
basePaths: ['templates'],
|
||||||
awToolTip: i18n._('Create a new template'),
|
awToolTip: i18n._('Create a new template'),
|
||||||
actionClass: 'btn List-buttonSubmit',
|
actionClass: 'btn List-dropdownSuccess',
|
||||||
buttonContent: i18n._('+ ADD'),
|
buttonContent: i18n._('ADD'),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
optionContent: 'Job Template',
|
||||||
|
optionSref: 'templates.addJobTemplate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
optionContent: 'Workflow Job Template',
|
||||||
|
optionSref: 'templates.addWorkflowJobTemplate'
|
||||||
|
}
|
||||||
|
],
|
||||||
ngShow: 'canAdd'
|
ngShow: 'canAdd'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fieldActions: {
|
fieldActions: {
|
||||||
|
|
||||||
columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-3',
|
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-3',
|
||||||
|
|
||||||
submit: {
|
submit: {
|
||||||
label: i18n._('Launch'),
|
label: i18n._('Launch'),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
ngClick: 'submitJob(job_template.id)',
|
ngClick: 'submitJob(unified_job_templates)',
|
||||||
awToolTip: i18n._('Start a job using this template'),
|
awToolTip: i18n._('Start a job using this template'),
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: 'job_template.summary_fields.user_capabilities.start'
|
ngShow: 'unified_job_templates.summary_fields.user_capabilities.start'
|
||||||
},
|
},
|
||||||
schedule: {
|
schedule: {
|
||||||
label: i18n._('Schedule'),
|
label: i18n._('Schedule'),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
ngClick: 'scheduleJob(job_template.id)',
|
ngClick: 'scheduleJob(unified_job_templates)',
|
||||||
awToolTip: i18n._('Schedule future job template runs'),
|
awToolTip: i18n._('Schedule future job template runs'),
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: 'job_template.summary_fields.user_capabilities.schedule'
|
ngShow: 'unified_job_templates.summary_fields.user_capabilities.schedule'
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
label: i18n._('Copy'),
|
label: i18n._('Copy'),
|
||||||
'ui-sref': 'jobTemplates.copy({id: job_template.id})',
|
'ui-sref': 'templates.copy({id: unified_job_templates.id})',
|
||||||
"class": 'btn-danger btn-xs',
|
"class": 'btn-danger btn-xs',
|
||||||
awToolTip: i18n._('Copy template'),
|
awToolTip: i18n._('Copy template'),
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: 'job_template.summary_fields.user_capabilities.copy'
|
ngShow: 'unified_job_templates.summary_fields.user_capabilities.copy'
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
label: i18n._('Edit'),
|
label: i18n._('Edit'),
|
||||||
ngClick: "editJobTemplate(job_template.id)",
|
ngClick: "editJobTemplate(unified_job_templates)",
|
||||||
awToolTip: i18n._('Edit template'),
|
awToolTip: i18n._('Edit template'),
|
||||||
"class": 'btn-default btn-xs',
|
"class": 'btn-default btn-xs',
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: 'job_template.summary_fields.user_capabilities.edit'
|
ngShow: 'unified_job_templates.summary_fields.user_capabilities.edit'
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
label: i18n._('View'),
|
label: i18n._('View'),
|
||||||
ngClick: "editJobTemplate(job_template.id)",
|
ngClick: "editJobTemplate(unified_job_templates.id)",
|
||||||
awToolTip: i18n._('View template'),
|
awToolTip: i18n._('View template'),
|
||||||
"class": 'btn-default btn-xs',
|
"class": 'btn-default btn-xs',
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: '!job_template.summary_fields.user_capabilities.edit'
|
ngShow: '!unified_job_templates.summary_fields.user_capabilities.edit'
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
label: i18n._('Delete'),
|
label: i18n._('Delete'),
|
||||||
ngClick: "deleteJobTemplate(job_template.id, job_template.name)",
|
ngClick: "deleteJobTemplate(unified_job_templates)",
|
||||||
"class": 'btn-danger btn-xs',
|
"class": 'btn-danger btn-xs',
|
||||||
awToolTip: i18n._('Delete template'),
|
awToolTip: i18n._('Delete template'),
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
ngShow: 'job_template.summary_fields.user_capabilities.delete'
|
ngShow: 'unified_job_templates.summary_fields.user_capabilities.delete'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};}]);
|
};}]);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default
|
|||||||
key: true,
|
key: true,
|
||||||
label: i18n._('Name'),
|
label: i18n._('Name'),
|
||||||
columnClass: 'col-lg-5 col-md-5 col-sm-9 col-xs-8',
|
columnClass: 'col-lg-5 col-md-5 col-sm-9 col-xs-8',
|
||||||
linkTo: '/#/job_templates/{{job_template.id}}',
|
linkTo: '/#/templates/{{job_template.id}}'
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
label: i18n._('Description'),
|
label: i18n._('Description'),
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="MainMenu-item"
|
<a class="MainMenu-item"
|
||||||
id="main_menu_job_templates_mobile_link"
|
id="main_menu_job_templates_mobile_link"
|
||||||
href="/#/job_templates"
|
href="/#/templates"
|
||||||
ng-class="{'is-currentRoute' : isCurrentState('jobTemplates')}">
|
ng-class="{'is-currentRoute' : isCurrentState('templates')}">
|
||||||
<span class="MainMenu-itemText">
|
<span class="MainMenu-itemText">
|
||||||
<translate>JOB TEMPLATES</translate>
|
<translate>TEMPLATES</translate>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="MainMenu-item"
|
<a class="MainMenu-item"
|
||||||
@@ -106,11 +106,11 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left"
|
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left"
|
||||||
id="main_menu_job_templates_link"
|
id="main_menu_job_templates_link"
|
||||||
href="/#/job_templates"
|
href="/#/templates"
|
||||||
ng-hide="licenseMissing"
|
ng-hide="licenseMissing"
|
||||||
ng-class="{'is-currentRoute' : isCurrentState('jobTemplates'), 'is-loggedOut' : !current_user || !current_user.username}">
|
ng-class="{'is-currentRoute' : isCurrentState('templates'), 'is-loggedOut' : !current_user || !current_user.username}">
|
||||||
<span class="MainMenu-itemText">
|
<span class="MainMenu-itemText">
|
||||||
<translate>JOB TEMPLATES</translate>
|
<translate>TEMPLATES</translate>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left MainMenu-item--lastLeft"
|
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left MainMenu-item--lastLeft"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default
|
|||||||
// job templates
|
// job templates
|
||||||
$stateExtender.addState({
|
$stateExtender.addState({
|
||||||
name: 'jobTemplateSchedules',
|
name: 'jobTemplateSchedules',
|
||||||
route: '/job_templates/:job_template_id/schedules',
|
route: '/templates/job_template/:id/schedules',
|
||||||
templateUrl: templateUrl("scheduler/scheduler"),
|
templateUrl: templateUrl("scheduler/scheduler"),
|
||||||
controller: 'schedulerListController',
|
controller: 'schedulerListController',
|
||||||
data: {
|
data: {
|
||||||
@@ -32,7 +32,7 @@ export default
|
|||||||
activityStreamId: 'id'
|
activityStreamId: 'id'
|
||||||
},
|
},
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
parent: 'jobTemplates.edit',
|
parent: 'templates.editJobTemplate',
|
||||||
label: 'SCHEDULES'
|
label: 'SCHEDULES'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -57,9 +57,45 @@ export default
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// workflows
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'workflowJobTemplateSchedules',
|
||||||
|
route: '/templates/workflow_job_template/:id/schedules',
|
||||||
|
templateUrl: templateUrl("scheduler/scheduler"),
|
||||||
|
controller: 'schedulerController',
|
||||||
|
data: {
|
||||||
|
activityStream: true,
|
||||||
|
activityStreamTarget: 'job_template',
|
||||||
|
activityStreamId: 'id'
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'templates.editWorkflowJobTemplate',
|
||||||
|
label: 'SCHEDULES'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'workflowJobTemplateSchedules.add',
|
||||||
|
route: '/add',
|
||||||
|
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||||
|
controller: 'schedulerAddController',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'workflowJobTemplateSchedules',
|
||||||
|
label: 'CREATE SCHEDULE'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'workflowJobTemplateSchedules.edit',
|
||||||
|
route: '/:schedule_id',
|
||||||
|
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||||
|
controller: 'schedulerEditController',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'workflowJobTemplateSchedules',
|
||||||
|
label: '{{schedule_obj.name}}'
|
||||||
|
}
|
||||||
|
});
|
||||||
// projects
|
// projects
|
||||||
$stateExtender.addState({
|
$stateExtender.addState({
|
||||||
searchPrefix: 'schedule',
|
searchPrefix: 'schedule',
|
||||||
name: 'projectSchedules',
|
name: 'projectSchedules',
|
||||||
route: '/projects/:id/schedules',
|
route: '/projects/:id/schedules',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
|||||||
forms = _.chain([params.form]).flatten().compact().value(),
|
forms = _.chain([params.form]).flatten().compact().value(),
|
||||||
buttons,
|
buttons,
|
||||||
id = params.id,
|
id = params.id,
|
||||||
|
position = (params.position === undefined) ? { my: "center", at: "center", of: window } : params.position,
|
||||||
x, y, wh, ww;
|
x, y, wh, ww;
|
||||||
|
|
||||||
function updateButtonStatus(isValid) {
|
function updateButtonStatus(isValid) {
|
||||||
@@ -91,7 +92,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
|||||||
|
|
||||||
// Set modal dimensions based on viewport width
|
// Set modal dimensions based on viewport width
|
||||||
ww = $(document).width();
|
ww = $(document).width();
|
||||||
wh = $('body').height();
|
wh = $(document).height();
|
||||||
x = (width > ww) ? ww - 10 : width;
|
x = (width > ww) ? ww - 10 : width;
|
||||||
y = (height > wh) ? wh - 10 : height;
|
y = (height > wh) ? wh - 10 : height;
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
|||||||
resizable: resizable,
|
resizable: resizable,
|
||||||
draggable: draggable,
|
draggable: draggable,
|
||||||
dialogClass: dialogClass,
|
dialogClass: dialogClass,
|
||||||
|
position: position,
|
||||||
create: function () {
|
create: function () {
|
||||||
// Fix the close button
|
// Fix the close button
|
||||||
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).html('<i class="fa fa-times-circle"></i>');
|
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).html('<i class="fa fa-times-circle"></i>');
|
||||||
|
|||||||
@@ -142,10 +142,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
.factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList',
|
.factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList',
|
||||||
'Attr', 'Icon', 'Column',
|
'Attr', 'Icon', 'Column',
|
||||||
'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon',
|
'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon',
|
||||||
'Store', 'ActionButton', '$log', 'i18n',
|
'Store', 'ActionButton', '$log', 'i18n', '$timeout',
|
||||||
function ($rootScope, $location, $compile, GenerateList,
|
function ($rootScope, $location, $compile, GenerateList,
|
||||||
Attr, Icon, Column, NavigationLink, HelpCollapse,
|
Attr, Icon, Column, NavigationLink, HelpCollapse,
|
||||||
DropDown, Empty, SelectIcon, Store, ActionButton, $log, i18n) {
|
DropDown, Empty, SelectIcon, Store, ActionButton, $log, i18n, $timeout) {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
setForm: function (form) { this.form = form; },
|
setForm: function (form) { this.form = form; },
|
||||||
@@ -523,7 +523,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
|
|
||||||
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
|
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
|
||||||
|
|
||||||
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock') {
|
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock' && field.type !== 'workflow-chart') {
|
||||||
|
|
||||||
|
|
||||||
html += "<div class='form-group Form-formGroup ";
|
html += "<div class='form-group Form-formGroup ";
|
||||||
@@ -1258,9 +1258,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
html += "</div>"; //end of Form-header
|
html += "</div>"; //end of Form-header
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(this.form.related)) {
|
if (!_.isEmpty(this.form.related) || !_.isEmpty(this.form.relatedButtons)) {
|
||||||
var collection, details = i18n._('Details');
|
var collection, details = i18n._('Details');
|
||||||
html += `<div class="Form-tabHolder">`;
|
html += "<div class=\"Form-tabHolder\">";
|
||||||
|
|
||||||
if(this.mode === "edit"){
|
if(this.mode === "edit"){
|
||||||
html += `<div id="${this.form.name}_tab" class="Form-tab" ` +
|
html += `<div id="${this.form.name}_tab" class="Form-tab" ` +
|
||||||
@@ -1284,6 +1284,45 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
}
|
}
|
||||||
html += `}">${(collection.title || collection.editTitle)}</div>`;
|
html += `}">${(collection.title || collection.editTitle)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (itm in this.form.relatedButtons) {
|
||||||
|
button = this.form.relatedButtons[itm];
|
||||||
|
|
||||||
|
// Build button HTML
|
||||||
|
html += "<button type=\"button\" ";
|
||||||
|
html += "class=\"btn btn-sm";
|
||||||
|
html += (button['class']) ? " " + button['class'] : "";
|
||||||
|
html += "\" ";
|
||||||
|
html += "id=\"" + this.form.name + "_" + btn + "_btn\" ";
|
||||||
|
|
||||||
|
if(button.ngShow){
|
||||||
|
html += this.attr(button, 'ngShow');
|
||||||
|
}
|
||||||
|
if (button.ngClick) {
|
||||||
|
html += this.attr(button, 'ngClick');
|
||||||
|
}
|
||||||
|
if (button.awFeature) {
|
||||||
|
html += this.attr(button, 'awFeature');
|
||||||
|
}
|
||||||
|
if (button.ngDisabled) {
|
||||||
|
ngDisabled = (button.ngDisabled===true) ? this.form.name+"_form.$invalid" : button.ngDisabled;
|
||||||
|
if (btn !== 'reset') {
|
||||||
|
//html += "ng-disabled=\"" + this.form.name + "_form.$pristine || " + this.form.name + "_form.$invalid";
|
||||||
|
html += "ng-disabled=\"" + ngDisabled;
|
||||||
|
//html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : "";
|
||||||
|
html += "\" ";
|
||||||
|
} else {
|
||||||
|
//html += "ng-disabled=\"" + this.form.name + "_form.$pristine";
|
||||||
|
//html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : "";
|
||||||
|
//html += "\" ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(button.awToolTip) {
|
||||||
|
html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'";
|
||||||
|
}
|
||||||
|
html += ">";
|
||||||
|
html += " " + button.label + "</button>\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(this.mode === "add"){
|
else if(this.mode === "add"){
|
||||||
html += "<div id=\"" + this.form.name + "_tab\""+
|
html += "<div id=\"" + this.form.name + "_tab\""+
|
||||||
@@ -1297,6 +1336,29 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
"class=\"Form-tab Form-tab--disabled\">" + (collection.title || collection.editTitle) +
|
"class=\"Form-tab Form-tab--disabled\">" + (collection.title || collection.editTitle) +
|
||||||
"</div>\n";
|
"</div>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (itm in this.form.relatedButtons) {
|
||||||
|
button = this.form.relatedButtons[itm];
|
||||||
|
|
||||||
|
// Build button HTML
|
||||||
|
html += "<button type=\"button\" ";
|
||||||
|
html += "class=\"btn btn-sm Form-tab--disabled";
|
||||||
|
html += (button['class']) ? " " + button['class'] : "";
|
||||||
|
html += "\" ";
|
||||||
|
html += "id=\"" + this.form.name + "_" + btn + "_btn\" ";
|
||||||
|
|
||||||
|
if(button.ngShow){
|
||||||
|
html += this.attr(button, 'ngShow');
|
||||||
|
}
|
||||||
|
if (button.awFeature) {
|
||||||
|
html += this.attr(button, 'awFeature');
|
||||||
|
}
|
||||||
|
if(button.awToolTip) {
|
||||||
|
html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'";
|
||||||
|
}
|
||||||
|
html += ">";
|
||||||
|
html += " " + button.label + "</button>\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html += "</div>";//tabHolder
|
html += "</div>";//tabHolder
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1492,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
button.label = i18n._('View Survey');
|
button.label = i18n._('View Survey');
|
||||||
button['class'] = 'Form-surveyButton';
|
button['class'] = 'Form-surveyButton';
|
||||||
}
|
}
|
||||||
|
if (btn === 'workflow_editor') {
|
||||||
|
button.label = i18n._('Workflow Editor');
|
||||||
|
button['class'] = 'Form-primaryButton';
|
||||||
|
}
|
||||||
|
|
||||||
// Build button HTML
|
// Build button HTML
|
||||||
html += "<button type=\"button\" ";
|
html += "<button type=\"button\" ";
|
||||||
|
|||||||
@@ -2,35 +2,63 @@
|
|||||||
hiddenOnCurrentPage(options.basePaths) ||
|
hiddenOnCurrentPage(options.basePaths) ||
|
||||||
hiddenInCurrentMode(options.mode)">
|
hiddenInCurrentMode(options.mode)">
|
||||||
<div class="List-action--showTooltipOnDisabled" ng-if="options.showTipWhenDisabled" aw-tool-tip="{{options.awToolTip}}" data-tip-watch="{{options.dataTipWatch}}" data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}" data-title="{{options.dataTitle}}" data-tooltip-inner-class="{{options.tooltipInnerClass}}">
|
<div class="List-action--showTooltipOnDisabled" ng-if="options.showTipWhenDisabled" aw-tool-tip="{{options.awToolTip}}" data-tip-watch="{{options.dataTipWatch}}" data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}" data-title="{{options.dataTitle}}" data-tooltip-inner-class="{{options.tooltipInnerClass}}">
|
||||||
<button
|
<div class="btn-group" ng-if="options.type === 'buttonDropdown'" ng-disabled="{{options.ngDisabled}}">
|
||||||
toolbar-button
|
<button type="button" class="{{options.actionClass}} List-dropdownButton" ng-bind-html="options.buttonContent"></button>
|
||||||
mode="options.mode"
|
<button type="button" class="{{options.actionClass}} List-dropdownButton List-dropdownCaratButton dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
class="{{options.actionClass}}"
|
<span class="List-dropdownCarat"></span>
|
||||||
ng-disabled="{{options.ngDisabled}}"
|
<span class="sr-only">Toggle Dropdown</span>
|
||||||
ng-show="{{options.ngShow}}"
|
</button>
|
||||||
ng-click="$eval(options.ngClick)"
|
<ul class="dropdown-menu pull-right">
|
||||||
toolbar="true"
|
<li ng-repeat="option in options.options">
|
||||||
aw-feature="{{options.awFeature}}">
|
<a ui-sref="{{option.optionSref}}" ng-bind-html="option.optionContent"></a>
|
||||||
<span ng-bind-html="options.buttonContent"></span>
|
</li>
|
||||||
</button>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<span ng-if="!options.type || options.type === 'button'">
|
||||||
|
<button
|
||||||
|
toolbar-button
|
||||||
|
mode="options.mode"
|
||||||
|
class="{{options.actionClass}}"
|
||||||
|
ng-disabled="{{options.ngDisabled}}"
|
||||||
|
ng-show="{{options.ngShow}}"
|
||||||
|
ng-click="$eval(options.ngClick)"
|
||||||
|
toolbar="true"
|
||||||
|
aw-feature="{{options.awFeature}}">
|
||||||
|
<span ng-bind-html="options.buttonContent"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span ng-if="!options.showTipWhenDisabled">
|
<span ng-if="!options.showTipWhenDisabled">
|
||||||
<button
|
<div class="btn-group" ng-if="options.type === 'buttonDropdown'" ng-disabled="{{options.ngDisabled}}" aw-tool-tip="{{options.awToolTip}}" data-tip-watch="{{options.dataTipWatch}}" data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}">
|
||||||
toolbar-button
|
<button type="button" class="{{options.actionClass}} List-dropdownButton dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
mode="options.mode"
|
<span>{{options.buttonContent}} </span>
|
||||||
aw-tool-tip="{{options.awToolTip}}"
|
<span class="List-dropdownCarat"></span>
|
||||||
data-tip-watch="{{options.dataTipWatch}}"
|
<span class="sr-only">Toggle Dropdown</span>
|
||||||
data-placement="{{options.dataPlacement}}"
|
</button>
|
||||||
data-container="{{options.dataContainer}}"
|
<ul class="dropdown-menu pull-right">
|
||||||
class="{{options.actionClass}}"
|
<li ng-repeat="option in options.options">
|
||||||
data-title="{{options.dataTitle}}"
|
<a ui-sref="{{option.optionSref}}" ng-bind-html="option.optionContent"></a>
|
||||||
ng-disabled="{{options.ngDisabled}}"
|
</li>
|
||||||
ng-click="$eval(options.ngClick)"
|
</ul>
|
||||||
ng-show="{{options.ngShow}}"
|
</div>
|
||||||
toolbar="true"
|
<span ng-if="!options.type || options.type === 'button'">
|
||||||
aw-feature="{{options.awFeature}}">
|
<button
|
||||||
<span ng-bind-html="options.buttonContent"></span>
|
toolbar-button
|
||||||
</button>
|
mode="options.mode"
|
||||||
|
aw-tool-tip="{{options.awToolTip}}"
|
||||||
|
data-tip-watch="{{options.dataTipWatch}}"
|
||||||
|
data-placement="{{options.dataPlacement}}"
|
||||||
|
data-container="{{options.dataContainer}}"
|
||||||
|
class="{{options.actionClass}}"
|
||||||
|
data-title="{{options.dataTitle}}"
|
||||||
|
ng-disabled="{{options.ngDisabled}}"
|
||||||
|
ng-click="$eval(options.ngClick)"
|
||||||
|
ng-show="{{options.ngShow}}"
|
||||||
|
toolbar="true"
|
||||||
|
aw-feature="{{options.awFeature}}">
|
||||||
|
<span ng-bind-html="options.buttonContent"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -140,12 +140,14 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
|||||||
let formNode, states = [];
|
let formNode, states = [];
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'add':
|
case 'add':
|
||||||
|
// breadcrumbName necessary for resources that are more than one word like
|
||||||
|
// job templates. form.name can't have spaces in it or it busts form gen
|
||||||
formNode = $stateExtender.buildDefinition({
|
formNode = $stateExtender.buildDefinition({
|
||||||
name: params.name || `${params.parent}.add`,
|
name: params.name || `${params.parent}.add`,
|
||||||
url: params.url || '/add',
|
url: params.url || '/add',
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
[params.parent ? 'parent' : null]: `${params.parent}`,
|
[params.parent ? 'parent' : null]: `${params.parent}`,
|
||||||
label: `CREATE ${form.name}`
|
label: `CREATE ${form.breadcrumbName || form.name}`
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
'form': {
|
'form': {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
|||||||
$scope.created_by = data.summary_fields.created_by;
|
$scope.created_by = data.summary_fields.created_by;
|
||||||
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||||
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||||
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
$scope.job_template_url = '/#/templates/' + data.unified_job_template;
|
||||||
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||||
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||||
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Controller: JobTemplatesList', () => {
|
||||||
|
// Setup
|
||||||
|
let scope,
|
||||||
|
rootScope,
|
||||||
|
state,
|
||||||
|
JobTemplatesList,
|
||||||
|
ClearScope,
|
||||||
|
GetChoices,
|
||||||
|
Alert,
|
||||||
|
Prompt,
|
||||||
|
InitiatePlaybookRun,
|
||||||
|
rbacUiControlService,
|
||||||
|
canAddDeferred,
|
||||||
|
q,
|
||||||
|
JobTemplateService,
|
||||||
|
deleteWorkflowJobTemplateDeferred,
|
||||||
|
deleteJobTemplateDeferred;
|
||||||
|
|
||||||
|
beforeEach(angular.mock.module('Tower'));
|
||||||
|
beforeEach(angular.mock.module('jobTemplates', ($provide) => {
|
||||||
|
|
||||||
|
state = jasmine.createSpyObj('state', [
|
||||||
|
'$get',
|
||||||
|
'transitionTo',
|
||||||
|
'go'
|
||||||
|
]);
|
||||||
|
|
||||||
|
state.params = {
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
rbacUiControlService = {
|
||||||
|
canAdd: function(){
|
||||||
|
return angular.noop;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
JobTemplateService = {
|
||||||
|
deleteWorkflowJobTemplate: function(){
|
||||||
|
return angular.noop;
|
||||||
|
},
|
||||||
|
deleteJobTemplate: function(){
|
||||||
|
return angular.noop;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ClearScope = jasmine.createSpy('ClearScope');
|
||||||
|
GetChoices = jasmine.createSpy('GetChoices');
|
||||||
|
Alert = jasmine.createSpy('Alert');
|
||||||
|
Prompt = jasmine.createSpy('Prompt').and.callFake(function(args) {
|
||||||
|
args.action();
|
||||||
|
});
|
||||||
|
InitiatePlaybookRun = jasmine.createSpy('InitiatePlaybookRun');
|
||||||
|
|
||||||
|
$provide.value('ClearScope', ClearScope);
|
||||||
|
$provide.value('GetChoices', GetChoices);
|
||||||
|
$provide.value('Alert', Alert);
|
||||||
|
$provide.value('Prompt', Prompt);
|
||||||
|
$provide.value('state', state);
|
||||||
|
$provide.value('InitiatePlaybookRun', InitiatePlaybookRun);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject( ($rootScope, $controller, $q, _state_, _ConfigService_, _ClearScope_, _GetChoices_, _Alert_, _Prompt_, _InitiatePlaybookRun_) => {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
rootScope = $rootScope;
|
||||||
|
q = $q;
|
||||||
|
state = _state_;
|
||||||
|
ClearScope = _ClearScope_;
|
||||||
|
GetChoices = _GetChoices_;
|
||||||
|
Alert = _Alert_;
|
||||||
|
Prompt = _Prompt_;
|
||||||
|
InitiatePlaybookRun = _InitiatePlaybookRun_;
|
||||||
|
canAddDeferred = q.defer();
|
||||||
|
deleteWorkflowJobTemplateDeferred = q.defer();
|
||||||
|
deleteJobTemplateDeferred = q.defer();
|
||||||
|
|
||||||
|
rbacUiControlService.canAdd = jasmine.createSpy('canAdd').and.returnValue(canAddDeferred.promise);
|
||||||
|
|
||||||
|
JobTemplateService.deleteWorkflowJobTemplate = jasmine.createSpy('deleteWorkflowJobTemplate').and.returnValue(deleteWorkflowJobTemplateDeferred.promise);
|
||||||
|
JobTemplateService.deleteJobTemplate = jasmine.createSpy('deleteJobTemplate').and.returnValue(deleteJobTemplateDeferred.promise);
|
||||||
|
|
||||||
|
JobTemplatesList = $controller('JobTemplatesList', {
|
||||||
|
$scope: scope,
|
||||||
|
$rootScope: rootScope,
|
||||||
|
$state: state,
|
||||||
|
ClearScope: ClearScope,
|
||||||
|
GetChoices: GetChoices,
|
||||||
|
Alert: Alert,
|
||||||
|
Prompt: Prompt,
|
||||||
|
InitiatePlaybookRun: InitiatePlaybookRun,
|
||||||
|
rbacUiControlService: rbacUiControlService,
|
||||||
|
JobTemplateService: JobTemplateService
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call GetChoices', ()=> {
|
||||||
|
expect(GetChoices).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.editJobTemplate()', () => {
|
||||||
|
|
||||||
|
it('should call Alert when template param is not present', ()=>{
|
||||||
|
scope.editJobTemplate();
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to edit template', 'Template parameter is missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to templates.editJobTemplate when type is "Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.editJobTemplate(testTemplate);
|
||||||
|
expect(state.transitionTo).toHaveBeenCalledWith('templates.editJobTemplate', {id: 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to templates.templates.editWorkflowJobTemplate when type is "Workflow Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Workflow Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.editJobTemplate(testTemplate);
|
||||||
|
expect(state.transitionTo).toHaveBeenCalledWith('templates.editWorkflowJobTemplate', {id: 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Alert when type is not "Job Template" or "Workflow Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Some Other Type",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.editJobTemplate(testTemplate);
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to edit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.deleteJobTemplate()', () => {
|
||||||
|
|
||||||
|
it('should call Alert when template param is not present', ()=>{
|
||||||
|
scope.deleteJobTemplate();
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to delete template', 'Template parameter is missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Prompt if template param is present', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: "Test Template"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.deleteJobTemplate(testTemplate);
|
||||||
|
expect(Prompt).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call JobTemplateService.deleteWorkflowJobTemplate when the user takes affirmative action on the delete modal and type = "Workflow Job Template"', ()=>{
|
||||||
|
// Note that Prompt has been mocked up above to immediately call the callback function that gets passed in
|
||||||
|
// which is how we access the private function in the controller
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: "Test Template",
|
||||||
|
type: "Workflow Job Template"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.deleteJobTemplate(testTemplate);
|
||||||
|
expect(JobTemplateService.deleteWorkflowJobTemplate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call JobTemplateService.deleteJobTemplate when the user takes affirmative action on the delete modal and type = "Workflow Job Template"', ()=>{
|
||||||
|
// Note that Prompt has been mocked up above to immediately call the callback function that gets passed in
|
||||||
|
// which is how we access the private function in the controller
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: "Test Template",
|
||||||
|
type: "Job Template"
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.deleteJobTemplate(testTemplate);
|
||||||
|
expect(JobTemplateService.deleteJobTemplate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.submitJob()', () => {
|
||||||
|
|
||||||
|
it('should call Alert when template param is not present', ()=>{
|
||||||
|
scope.submitJob();
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to launch template', 'Template parameter is missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call InitiatePlaybookRun when type is "Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.submitJob(testTemplate);
|
||||||
|
expect(InitiatePlaybookRun).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('should call [something] when type is "Workflow Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Workflow Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.submitJob(testTemplate);
|
||||||
|
expect([something]).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Alert when type is not "Job Template" or "Workflow Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Some Other Type",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.submitJob(testTemplate);
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.scheduleJob()', () => {
|
||||||
|
|
||||||
|
it('should transition to jobTemplateSchedules when type is "Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.scheduleJob(testTemplate);
|
||||||
|
expect(state.go).toHaveBeenCalledWith('jobTemplateSchedules', {id: 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to workflowJobTemplateSchedules when type is "Workflow Job Template"', ()=>{
|
||||||
|
|
||||||
|
var testTemplate = {
|
||||||
|
type: "Workflow Job Template",
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.scheduleJob(testTemplate);
|
||||||
|
expect(state.go).toHaveBeenCalledWith('workflowJobTemplateSchedules', {id: 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Alert when template param is not present', ()=>{
|
||||||
|
scope.scheduleJob();
|
||||||
|
expect(Alert).toHaveBeenCalledWith('Error: Unable to schedule job', 'Template parameter is missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
169
awx/ui/tests/spec/workflows/workflow-add.controller-test.js
Normal file
169
awx/ui/tests/spec/workflows/workflow-add.controller-test.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Controller: WorkflowAdd', () => {
|
||||||
|
// Setup
|
||||||
|
let scope,
|
||||||
|
state,
|
||||||
|
WorkflowAdd,
|
||||||
|
ClearScope,
|
||||||
|
Alert,
|
||||||
|
GenerateForm,
|
||||||
|
initSurvey,
|
||||||
|
JobTemplateService,
|
||||||
|
q,
|
||||||
|
getLabelsDeferred,
|
||||||
|
createWorkflowJobTemplateDeferred,
|
||||||
|
httpBackend,
|
||||||
|
ProcessErrors,
|
||||||
|
CreateSelect2,
|
||||||
|
Wait,
|
||||||
|
ParseTypeChange,
|
||||||
|
ToJSON;
|
||||||
|
|
||||||
|
beforeEach(angular.mock.module('Tower'));
|
||||||
|
beforeEach(angular.mock.module('jobTemplates', ($provide) => {
|
||||||
|
|
||||||
|
state = jasmine.createSpyObj('state', [
|
||||||
|
'$get',
|
||||||
|
'transitionTo',
|
||||||
|
'go'
|
||||||
|
]);
|
||||||
|
|
||||||
|
GenerateForm = jasmine.createSpyObj('GenerateForm', [
|
||||||
|
'inject',
|
||||||
|
'reset',
|
||||||
|
'clearApiErrors'
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobTemplateService = {
|
||||||
|
getLabelOptions: function(){
|
||||||
|
return angular.noop;
|
||||||
|
},
|
||||||
|
createWorkflowJobTemplate: function(){
|
||||||
|
return angular.noop;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ClearScope = jasmine.createSpy('ClearScope');
|
||||||
|
Alert = jasmine.createSpy('Alert');
|
||||||
|
ProcessErrors = jasmine.createSpy('ProcessErrors');
|
||||||
|
CreateSelect2 = jasmine.createSpy('CreateSelect2');
|
||||||
|
Wait = jasmine.createSpy('Wait');
|
||||||
|
ParseTypeChange = jasmine.createSpy('ParseTypeChange');
|
||||||
|
ToJSON = jasmine.createSpy('ToJSON');
|
||||||
|
|
||||||
|
$provide.value('ClearScope', ClearScope);
|
||||||
|
$provide.value('Alert', Alert);
|
||||||
|
$provide.value('GenerateForm', GenerateForm);
|
||||||
|
$provide.value('state', state);
|
||||||
|
$provide.value('ProcessErrors', ProcessErrors);
|
||||||
|
$provide.value('CreateSelect2', CreateSelect2);
|
||||||
|
$provide.value('Wait', Wait);
|
||||||
|
$provide.value('ParseTypeChange', ParseTypeChange);
|
||||||
|
$provide.value('ToJSON', ToJSON);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject( ($rootScope, $controller, $q, $httpBackend, _state_, _ConfigService_, _ClearScope_, _GetChoices_, _Alert_, _GenerateForm_, _ProcessErrors_, _CreateSelect2_, _Wait_, _ParseTypeChange_, _ToJSON_) => {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
state = _state_;
|
||||||
|
q = $q;
|
||||||
|
ClearScope = _ClearScope_;
|
||||||
|
Alert = _Alert_;
|
||||||
|
GenerateForm = _GenerateForm_;
|
||||||
|
httpBackend = $httpBackend;
|
||||||
|
ProcessErrors = _ProcessErrors_;
|
||||||
|
CreateSelect2 = _CreateSelect2_;
|
||||||
|
Wait = _Wait_;
|
||||||
|
getLabelsDeferred = q.defer();
|
||||||
|
createWorkflowJobTemplateDeferred = q.defer();
|
||||||
|
ParseTypeChange = _ParseTypeChange_;
|
||||||
|
ToJSON = _ToJSON_;
|
||||||
|
|
||||||
|
JobTemplateService.getLabelOptions = jasmine.createSpy('getLabelOptions').and.returnValue(getLabelsDeferred.promise);
|
||||||
|
JobTemplateService.createWorkflowJobTemplate = jasmine.createSpy('createWorkflowJobTemplate').and.returnValue(createWorkflowJobTemplateDeferred.promise);
|
||||||
|
|
||||||
|
WorkflowAdd = $controller('WorkflowAdd', {
|
||||||
|
$scope: scope,
|
||||||
|
$state: state,
|
||||||
|
ClearScope: ClearScope,
|
||||||
|
Alert: Alert,
|
||||||
|
GenerateForm: GenerateForm,
|
||||||
|
JobTemplateService: JobTemplateService,
|
||||||
|
ProcessErrors: ProcessErrors,
|
||||||
|
CreateSelect2: CreateSelect2,
|
||||||
|
Wait: Wait,
|
||||||
|
ParseTypeChange: ParseTypeChange,
|
||||||
|
ToJSON
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call ClearScope', ()=>{
|
||||||
|
expect(ClearScope).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call GenerateForm.inject', ()=>{
|
||||||
|
expect(GenerateForm.inject).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call GenerateForm.reset', ()=>{
|
||||||
|
expect(GenerateForm.reset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get/set the label options and select2-ify the input', ()=>{
|
||||||
|
// Resolve JobTemplateService.getLabelsForJobTemplate
|
||||||
|
getLabelsDeferred.resolve({
|
||||||
|
foo: "bar"
|
||||||
|
});
|
||||||
|
// We expect the digest cycle to fire off this call to /static/config.js so we go ahead and handle it
|
||||||
|
httpBackend.expectGET('/static/config.js').respond(200);
|
||||||
|
scope.$digest();
|
||||||
|
expect(scope.labelOptions).toEqual({
|
||||||
|
foo: "bar"
|
||||||
|
});
|
||||||
|
expect(CreateSelect2).toHaveBeenCalledWith({
|
||||||
|
element:'#workflow_labels',
|
||||||
|
multiple: true,
|
||||||
|
addNew: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call ProcessErrors when getLabelsForJobTemplate returns a rejected promise', ()=>{
|
||||||
|
// Reject JobTemplateService.getLabelsForJobTemplate
|
||||||
|
getLabelsDeferred.reject({
|
||||||
|
data: "mockedData",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
// We expect the digest cycle to fire off this call to /static/config.js so we go ahead and handle it
|
||||||
|
httpBackend.expectGET('/static/config.js').respond(200);
|
||||||
|
scope.$digest();
|
||||||
|
expect(ProcessErrors).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.formSave()', () => {
|
||||||
|
|
||||||
|
it('should call JobTemplateService.createWorkflowJobTemplate', ()=>{
|
||||||
|
scope.name = "Test Workflow";
|
||||||
|
scope.description = "This is a test description";
|
||||||
|
scope.formSave();
|
||||||
|
expect(JobTemplateService.createWorkflowJobTemplate).toHaveBeenCalledWith({
|
||||||
|
name: "Test Workflow",
|
||||||
|
description: "This is a test description",
|
||||||
|
labels: undefined,
|
||||||
|
organization: undefined,
|
||||||
|
variables: undefined,
|
||||||
|
extra_vars: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.formCancel()', () => {
|
||||||
|
|
||||||
|
it('should transition to templates', ()=>{
|
||||||
|
scope.formCancel();
|
||||||
|
expect(state.transitionTo).toHaveBeenCalledWith('templates');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Controller: WorkflowMaker', () => {
|
||||||
|
// Setup
|
||||||
|
let scope,
|
||||||
|
WorkflowMakerController,
|
||||||
|
WorkflowHelpService;
|
||||||
|
|
||||||
|
beforeEach(angular.mock.module('Tower'));
|
||||||
|
beforeEach(angular.mock.module('jobTemplates', ($provide) => {
|
||||||
|
|
||||||
|
WorkflowHelpService = jasmine.createSpyObj('WorkflowHelpService', [
|
||||||
|
'closeDialog',
|
||||||
|
'addPlaceholderNode',
|
||||||
|
'getSiblingConnectionTypes'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$provide.value('WorkflowHelpService', WorkflowHelpService);
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject( ($rootScope, $controller, _WorkflowHelpService_) => {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
WorkflowHelpService = _WorkflowHelpService_;
|
||||||
|
|
||||||
|
WorkflowMakerController = $controller('WorkflowMakerController', {
|
||||||
|
$scope: scope,
|
||||||
|
WorkflowHelpService: WorkflowHelpService
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('scope.saveWorkflowMaker()', () => {
|
||||||
|
|
||||||
|
it('should close the dialog', ()=>{
|
||||||
|
scope.saveWorkflowMaker();
|
||||||
|
expect(WorkflowHelpService.closeDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.startAddNode()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.confirmNodeForm()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.cancelNodeForm()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.startEditNode()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.startDeleteNode()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.cancelDeleteNode()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.confirmDeleteNode()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.toggleFormTab()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.toggle_job_template()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.toggle_project()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scope.toggle_inventory_source()', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user