diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index fc0964a028..12ee00cba3 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -55,6 +55,8 @@ import 'tower/shared/InventoryTree'; import 'tower/shared/Timer'; import 'tower/shared/Socket'; +import 'tower/job-templates/main'; + /*#if DEBUG#*/ import {__deferLoadIfEnabled} from 'tower/debug'; __deferLoadIfEnabled(); @@ -74,6 +76,7 @@ var tower = angular.module('Tower', [ 'UserFormDefinition', 'FormGenerator', 'OrganizationListDefinition', + 'jobTemplates', 'UserListDefinition', 'UserHelper', 'PromptDialog', @@ -267,6 +270,11 @@ var tower = angular.module('Tower', [ controller: JobTemplatesAdd }). + when('/inventories/:inventory_id/job_templates/:template_id', { + templateUrl: urlPrefix + 'partials/job_templates.html', + controller: JobTemplatesEdit + }). + when('/inventories/:inventory_id/manage', { templateUrl: urlPrefix + 'partials/inventory-manage.html', controller: InventoriesManage diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 6cf2d5474a..b109ff26f9 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -13,6 +13,7 @@ * @description This controller's for the Inventory page */ +import 'tower/job-templates/main'; export function InventoriesList($scope, $rootScope, $location, $log, $routeParams, $compile, $filter, Rest, Alert, InventoryList, generateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, Wait, Stream, @@ -482,7 +483,8 @@ InventoriesAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $routeParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit, PaginateInit, - LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON, ParseVariableString, Stream) { + LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON, ParseVariableString, Stream, RelatedSearchInit, RelatedPaginateInit, + Prompt, PlaybookRun, CreateDialog, deleteJobTemplate) { ClearScope(); @@ -492,16 +494,28 @@ export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $ generator = GenerateForm, inventory_id = $routeParams.inventory_id, master = {}, - fld, json_data, data; + fld, json_data, data, + relatedSets = {}; form.well = true; form.formLabelSize = null; form.formFieldSize = null; - + $scope.inventory_id = inventory_id; generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.reset(); - LoadBreadCrumbs(); + + + // After the project is loaded, retrieve each related set + if ($scope.inventoryLoadedRemove) { + $scope.inventoryLoadedRemove(); + } + $scope.projectLoadedRemove = $scope.$on('inventoryLoaded', function () { + var set; + for (set in relatedSets) { + $scope.search(relatedSets[set].iterator); + } + }); Wait('start'); Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); @@ -530,6 +544,24 @@ export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $ data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; } } + relatedSets = form.relatedSets(data.related); + + // Initialize related search functions. Doing it here to make sure relatedSets object is populated. + RelatedSearchInit({ + scope: $scope, + form: form, + relatedSets: relatedSets + }); + RelatedPaginateInit({ + scope: $scope, + relatedSets: relatedSets + }); + + LoadBreadCrumbs({ + path: $location.path(), + title: $scope.inventory_name + }); + Wait('stop'); $scope.parseType = 'yaml'; ParseTypeChange({ @@ -546,6 +578,7 @@ export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $ field: 'organization', input_type: 'radio' }); + $scope.$emit('inventoryLoaded'); }) .error(function (data, status) { ProcessErrors($scope, data, status, null, { hdr: 'Error!', @@ -627,11 +660,179 @@ export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $ $scope.addScanJob = function(){ $location.path($location.path()+'/job_templates/add'); }; + + $scope.launchScanJob = function(){ + PlaybookRun({ scope: $scope, id: this.scan_job_template.id }); + }; + + $scope.scheduleScanJob = function(){ + $location.path('/job_templates/'+this.scan_job_template.id+'/schedules'); + }; + + $scope.editScanJob = function(){ + $location.path($location.path()+'/job_templates/'+this.scan_job_template.id); + }; + + $scope.copyScanJobTemplate = function(){ + var id = this.scan_job_template.id, + name = this.scan_job_template.name, + element, + buttons = [{ + "label": "Cancel", + "onClick": function() { + $(this).dialog('close'); + }, + "icon": "fa-times", + "class": "btn btn-default", + "id": "copy-close-button" + },{ + "label": "Copy", + "onClick": function() { + copyAction(); + }, + "icon": "fa-copy", + "class": "btn btn-primary", + "id": "job-copy-button" + }], + copyAction = function () { + // retrieve the copy of the job template object from the api, then overwrite the name and throw away the id + Wait('start'); + var url = GetBasePath('job_templates')+id; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + data.name = $scope.new_copy_name; + delete data.id; + $scope.$emit('GoToCopy', data); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + + CreateDialog({ + id: 'copy-job-modal' , + title: "Copy", + scope: $scope, + buttons: buttons, + width: 500, + height: 300, + minWidth: 200, + callback: 'CopyDialogReady' + }); + + $('#job_name').text(name); + $('#copy-job-modal').show(); + + + if ($scope.removeCopyDialogReady) { + $scope.removeCopyDialogReady(); + } + $scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() { + //clear any old remaining text + $scope.new_copy_name = "" ; + $scope.copy_form.$setPristine(); + $('#copy-job-modal').dialog('open'); + $('#job-copy-button').attr('ng-disabled', "!copy_form.$valid"); + element = angular.element(document.getElementById('job-copy-button')); + $compile(element)($scope); + + }); + + if ($scope.removeGoToCopy) { + $scope.removeGoToCopy(); + } + $scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) { + var url = GetBasePath('job_templates'), + old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ; + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + if(data.survey_enabled===true){ + $scope.$emit("CopySurvey", data, old_survey_url); + } + else { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/job_templates/' + data.id); + } + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }); + + if ($scope.removeCopySurvey) { + $scope.removeCopySurvey(); + } + $scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) { + // var url = data.related.survey_spec; + Rest.setUrl(old_url); + Rest.get() + .success(function (survey_data) { + + Rest.setUrl(new_data.related.survey_spec); + Rest.post(survey_data) + .success(function () { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/job_templates/' + new_data.id); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status }); + }); + + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status }); + }); + + }); + + }; + + $scope.deleteScanJob = function () { + var id = this.scan_job_template.id , + action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + deleteJobTemplate(id) + .success(function () { + $('#prompt-modal').modal('hide'); + $scope.search(form.related.scan_job_templates.iterator); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'DELETE returned status: ' + status }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Delete job template ' + this.scan_job_template.name + '?
', + action: action + }); + + }; + } InventoriesEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'generateList', 'OrganizationList', 'SearchInit', - 'PaginateInit', 'LookUpInit', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', 'Stream' + 'PaginateInit', 'LookUpInit', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', 'Stream', 'RelatedSearchInit', 'RelatedPaginateInit', + 'Prompt', 'PlaybookRun', 'CreateDialog', 'deleteJobTemplate' ]; diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index 063b376df9..8279d0f61e 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -276,7 +276,8 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ $scope.job_type_options = [ { value: 'run', label: 'Run' }, - { value: 'check', label: 'Check' } + { value: 'check', label: 'Check' }, + { value: 'scan' , label: 'Scan'} ]; $scope.verbosity_options = [ @@ -301,12 +302,13 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ LookUpInit({ scope: $scope, form: form, - current_item: null, + current_item: ($routeParams.inventory_id !== undefined) ? $routeParams.inventory_id : null, list: InventoryList, field: 'inventory', input_type: "radio" }); + // Clone the CredentialList object for use with cloud_credential. Cloning // and changing properties to avoid collision. jQuery.extend(true, CloudCredentialList, CredentialList); @@ -340,10 +342,17 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ parent_scope: $scope }); + + // Update playbook select whenever project value changes selectPlaybook = function (oldValue, newValue) { var url; - if (oldValue !== newValue) { + if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ + $scope.playbook_options = ['Default']; + $scope.playbook = 'Default'; + Wait('stop'); + } + else if (oldValue !== newValue) { if ($scope.project) { Wait('start'); url = GetBasePath('projects') + $scope.project + '/playbooks/'; @@ -365,6 +374,49 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ } }; + $scope.jobTypeChange = function(){ + if($scope.job_type){ + if($scope.job_type.value === 'scan'){ + // $scope.project_name = 'Default'; + // $scope.project = null; + $scope.toggleScanInfo(); + } + else if($scope.project_name === "Default"){ + $scope.project_name = null; + $scope.playbook_options = []; + // $scope.playbook = 'null'; + $scope.job_templates_form.playbook.$setPristine(); + } + } + }; + + $scope.toggleScanInfo = function() { + $scope.project_name = 'Default'; + if($scope.project === null){ + selectPlaybook(); + } + else { + $scope.project = null; + } + }; + + if ($routeParams.inventory_id) { + // This means that the job template form was accessed via inventory prop's + // This also means the job is a scan job. + $scope.job_type.value = 'scan'; + $scope.jobTypeChange(); + $scope.inventory = $routeParams.inventory_id; + Rest.setUrl(GetBasePath('inventory') + $routeParams.inventory_id + '/'); + Rest.get() + .success(function (data) { + $scope.inventory_name = data.name; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to lookup inventory: ' + data.id + '. GET returned status: ' + status }); + }); + } + // Detect and alert user to potential SCM status issues checkSCMStatus = function (oldValue, newValue) { if (oldValue !== newValue && !Empty($scope.project)) { @@ -432,7 +484,17 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ function saveCompleted() { - setTimeout(function() { $scope.$apply(function() { $location.path('/job_templates'); }); }, 500); + setTimeout(function() { + $scope.$apply(function() { + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'job_templates') { + ReturnToCaller(); + } + else { + ReturnToCaller(1); + } + }); + }, 500); } if ($scope.removeTemplateSaveSuccess) { @@ -472,7 +534,10 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ } } data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); - + if(data.job_type === 'scan' && $scope.default_scan === true){ + data.project = ""; + data.playbook = ""; + } Rest.setUrl(defaultUrl); Rest.post(data) .success(function(data) { @@ -591,7 +656,8 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, // Our job type options $scope.job_type_options = [ { value: 'run', label: 'Run' }, - { value: 'check', label: 'Check' } + { value: 'check', label: 'Check' }, + { value: 'scan', label: 'Scan'} ]; $scope.verbosity_options = [ @@ -617,7 +683,12 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, getPlaybooks = function (project) { var url; - if (!Empty(project)) { + if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ + $scope.playbook_options = ['Default']; + $scope.playbook = 'Default'; + Wait('stop'); + } + else if (!Empty(project)) { url = GetBasePath('projects') + project + '/playbooks/'; Wait('start'); Rest.setUrl(url); @@ -648,6 +719,32 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, } }; + $scope.jobTypeChange = function(){ + if($scope.job_type){ + if($scope.job_type.value === 'scan'){ + // $scope.project_name = 'Default'; + // $scope.project = null; + $scope.toggleScanInfo(); + } + else if($scope.project_name === "Default"){ + $scope.project_name = null; + $scope.playbook_options = []; + // $scope.playbook = 'null'; + $scope.job_templates_form.playbook.$setPristine(); + } + } + }; + + $scope.toggleScanInfo = function() { + $scope.project_name = 'Default'; + if($scope.project === null){ + getPlaybooks(); + } + else { + $scope.project = null; + } + }; + // Detect and alert user to potential SCM status issues checkSCMStatus = function () { if (!Empty($scope.project)) { @@ -888,7 +985,17 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, }); function saveCompleted() { - setTimeout(function() { $scope.$apply(function() { $location.path('/job_templates'); }); }, 500); + setTimeout(function() { + $scope.$apply(function() { + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'job_templates') { + ReturnToCaller(); + } + else { + ReturnToCaller(1); + } + }); + }, 500); } if ($scope.removeTemplateSaveSuccess) { diff --git a/awx/ui/static/js/forms/Inventories.js b/awx/ui/static/js/forms/Inventories.js index 5d0bd79d60..3b628da008 100644 --- a/awx/ui/static/js/forms/Inventories.js +++ b/awx/ui/static/js/forms/Inventories.js @@ -97,26 +97,35 @@ export default }, related: { - scan_jobs: { + scan_job_templates: { type: 'collection', - title: 'Scan Jobs', - iterator: 'scan_job', + title: 'Scan Job Templates', + iterator: 'scan_job_template', index: false, open: false, actions: { add: { - ngClick: "addScanJob(inventory_id)", + ngClick: "addScanJob()", icon: 'icon-plus', label: 'Add', - awToolTip: 'Add a scan job' + awToolTip: 'Add a scan job template' } }, fields: { + smart_status: { + label: 'Status', + // columnClass: 'col-md-2 col-sm-2 col-xs-2', + searchable: false, + nosort: true, + ngInclude: "'/static/partials/scan-job-template-smart-status.html'", + type: 'template' + }, name: { key: true, - label: 'Name' + label: 'Name', + linkTo: '/#/inventories/{{inventory_id}}/job_templates/{{scan_job_template.id}}' }, description: { label: 'Description' @@ -124,19 +133,39 @@ export default }, fieldActions: { + submit: { + label: 'Launch', + ngClick: "launchScanJob()", + awToolTip: 'Launch the scan job template', + 'class': 'btn btn-default' + }, + schedule: { + label: 'Schedule', + ngClick: 'scheduleScanJob()', + awToolTip: 'Schedule future job template runs', + dataPlacement: 'top', + }, edit: { label: 'Edit', - ngClick: "edit('organizations', organization.id, organization.name)", + ngClick: "editScanJob()", icon: 'icon-edit', - awToolTip: 'Edit the organization', + awToolTip: 'Edit the scan job template', 'class': 'btn btn-default' }, "delete": { label: 'Delete', - ngClick: "delete('organizations', organization.id, organization.name, 'organizations')", + ngClick: "deleteScanJob()", icon: 'icon-trash', "class": 'btn-danger', - awToolTip: 'Delete the organization' + awToolTip: 'Delete the scan job template' + }, + copy: { + label: 'Copy', + ngClick: "copyScanJobTemplate()", + "class": 'btn-danger btn-xs', + awToolTip: 'Copy template', + dataPlacement: 'top', + ngHide: 'job_template.summary_fields.can_copy === false' } } } @@ -144,14 +173,10 @@ export default relatedSets: function(urls) { return { - scan_jobs: { - iterator: 'scan_job', - url: urls.organizations - }, - // schedules: { - // iterator: 'schedule', - // url: urls.schedules - // } + scan_job_templates: { + iterator: 'scan_job_template', + url: urls.scan_job_templates + } }; } diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index f78f556474..c8fc483f53 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -66,6 +66,7 @@ export default label: 'Job Type', type: 'select', ngOptions: 'type.label for type in job_type_options track by type.value', + ngChange: 'jobTypeChange()', "default": 0, addRequired: true, editRequired: true, @@ -101,7 +102,7 @@ export default awPopOver: "

Select the project containing the playbook you want this job to execute.

", dataTitle: 'Project', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", }, playbook: { label: 'Playbook', @@ -113,7 +114,24 @@ export default awPopOver: "

Select the playbook to be executed by this job.

", dataTitle: 'Playbook', dataPlacement: 'right', - dataContainer: "body" + dataContainer: "body", + }, + // default_scan: { + // label: "Use default scan job project and playbook", + // type: 'checkbox', + // ngChange: 'toggleScanInfo()', + // ngShow: 'job_type.value === "scan" && project_name !== "Default"', + // column: 1, + // awPopOver: "

Scan jobs templates use a default project and default playbook. Uncheck this box to override these defaults.

", + // dataTitle: 'Scan jobs', + // dataPlacement: 'right', + // dataContainer: "body" + // }, + default_scan: { + type: 'custom', + column: 1, + ngShow: 'job_type.value === "scan" && project_name !== "Default"', + control: 'Reset to default project and playbook' }, credential: { label: 'Machine Credential', diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index c673bb3122..41626056ad 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -1,9 +1,9 @@ /********************************************* - * Copyright (c) 2014 AnsibleWorks, Inc. - * - * JobSubmission.js - * - */ +* Copyright (c) 2014 AnsibleWorks, Inc. +* +* JobSubmission.js +* +*/ /** * @ngdoc function * @name helpers.function:JobSubmission @@ -13,1047 +13,1047 @@ export default angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', - 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) - - .factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', 'ToJSON', 'Empty', 'GetBasePath', - function(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath) { - return function(params) { - var scope = params.scope, - callback = params.callback || 'JobLaunched', - job_launch_data = {}, - url = params.url, - vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', - // fld, - extra_vars; - - //found it easier to assume that there will be extra vars, and then check for a blank object at the end - job_launch_data.extra_vars = {}; - - //gather the extra vars from the job template if survey is enabled and prompt for vars is false - if (scope.removeGetExtraVars) { - scope.removeGetExtraVars(); - } - scope.removeGetExtraVars = scope.$on('GetExtraVars', function() { - - Rest.setUrl(vars_url); - Rest.get() - .success(function (data) { - if(!Empty(data.extra_vars)){ - data.extra_vars = ToJSON('yaml', data.extra_vars, false); - $.each(data.extra_vars, function(key,value){ - job_launch_data.extra_vars[key] = value; - }); - } - scope.$emit('BuildData'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, { hdr: 'Error!', - msg: 'Failed to retrieve job template extra variables.' }); - }); - }); - - //build the data object to be sent to the job launch endpoint. Any variables gathered from the survey and the extra variables text editor are inserted into the extra_vars dict of the job_launch_data - if (scope.removeBuildData) { - scope.removeBuildData(); - } - scope.removeBuildData = scope.$on('BuildData', function() { - if(!Empty(scope.passwords_needed_to_start) && scope.passwords_needed_to_start.length>0){ - scope.passwords.forEach(function(password) { - job_launch_data[password] = scope[password]; - scope.passwords_needed_to_start.push(password+'_confirm'); // i'm pushing these values into this array for use during the survey taker parsing - }); - } - if(scope.prompt_for_vars===true){ - extra_vars = ToJSON(scope.parseType, scope.extra_vars, false); - if(!Empty(extra_vars)){ - $.each(extra_vars, function(key,value){ - job_launch_data.extra_vars[key] = value; - }); - } - - } - - if(scope.survey_enabled===true){ - for (var i=0; i < scope.survey_questions.length; i++){ - var fld = scope.survey_questions[i].variable; - // grab all survey questions that have answers - if(scope.survey_questions[i].required || (scope.survey_questions[i].required === false && scope[fld].toString()!=="")) { - job_launch_data.extra_vars[fld] = scope[fld]; - } - // for optional text and text-areas, submit a blank string if min length is 0 - if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && (scope[fld] === "" || scope[fld] === undefined)){ - job_launch_data.extra_vars[fld] = ""; - } - } - } - - // include the credential used if the user was prompted to choose a cred - if(!Empty(scope.credential)){ - job_launch_data.credential_id = scope.credential; - } - - // If the extra_vars dict is empty, we don't want to include it if we didn't prompt for anything. - if(jQuery.isEmptyObject(job_launch_data.extra_vars)===true && scope.prompt_for_vars===false){ - delete job_launch_data.extra_vars; - } - - Rest.setUrl(url); - Rest.post(job_launch_data) - .success(function(data) { - Wait('stop'); - if(!$('#password-modal').is(':hidden')){ - $('#password-modal').dialog('close'); - } - scope.$emit(callback, data); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status }); - }); - }); - - // if the user has a survey and does not have 'prompt for vars' selected, then we want to - // include the extra vars from the job template in the job launch. so first check for these conditions - // and then overlay any survey vars over those. - if(scope.prompt_for_vars===false && scope.survey_enabled===true){ - scope.$emit('GetExtraVars'); - } - else { - scope.$emit('BuildData'); - } - - - }; - }]) - - .factory('PromptForCredential', ['$location', 'Wait', 'GetBasePath', 'LookUpInit', 'JobTemplateForm', 'CredentialList', 'Rest', 'Prompt', 'ProcessErrors', - function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors) { - return function(params) { - - var scope = params.scope, - callback = params.callback || 'CredentialReady', - selectionMade; - - Wait('stop'); - scope.credential = ''; - - if (scope.removeShowLookupDialog) { - scope.removeShowLookupDialog(); - } - scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() { - selectionMade = function () { - scope.$emit(callback, scope.credential); - }; - - LookUpInit({ - url: GetBasePath('credentials') + '?kind=ssh', - scope: scope, - form: JobTemplateForm(), - current_item: null, - list: CredentialList, - field: 'credential', - hdr: 'Credential Required', - instructions: "Launching this job requires a machine credential. Please select your machine credential now or Cancel to quit.", - postAction: selectionMade, - input_type: 'radio' - }); - scope.lookUpCredential(); - }); - - if (scope.removeAlertNoCredentials) { - scope.removeAlertNoCredentials(); - } - scope.removeAlertNoCredentials = scope.$on('AlertNoCredentials', function() { - var action = function () { - $('#prompt-modal').modal('hide'); - $location.url('/credentials/add'); - }; - - Prompt({ - hdr: 'Machine Credential Required', - body: "
There are no machine credentials defined in Tower. Launching this job requires a machine credential. " + - "Create one now?", - action: action - }); - }); - - Rest.setUrl(GetBasePath('credentials') + '?kind=ssh'); - Rest.get() - .success(function(data) { - if (data.results.length > 0) { - scope.$emit('ShowLookupDialog'); - } - else { - scope.$emit('AlertNoCredentials'); - } - }) - .error(function(data,status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Checking for machine credentials failed. GET returned: ' + status }); - }); - }; - }]) - - - - .factory('CreateLaunchDialog', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateDialog', 'GenerateForm', - 'JobVarsPromptForm', 'Wait', 'ParseTypeChange', - function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, - JobVarsPromptForm, Wait, ParseTypeChange) { - return function(params) { - var buttons, - scope = params.scope, - html = params.html, - // job_launch_data = {}, - callback = params.callback || 'PlaybookLaunchFinished', - // url = params.url, - e; - - // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; - html+=''; - $('#password-modal').empty().html(html); - $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); - e = angular.element(document.getElementById('password-modal')); - $compile(e)(scope); - - if(scope.prompt_for_vars===true){ - ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); - } - - buttons = [{ - label: "Cancel", - onClick: function() { - $('#password-modal').dialog('close'); - // scope.$emit('CancelJob'); - // scope.$destroy(); - }, - icon: "fa-times", - "class": "btn btn-default", - "id": "password-cancel-button" - },{ - label: "Launch", - onClick: function() { - scope.$emit(callback); - }, - icon: "fa-check", - "class": "btn btn-primary", - "id": "password-accept-button" - }]; - - CreateDialog({ - id: 'password-modal', - scope: scope, - buttons: buttons, - width: 620, - height: 700, //(scope.passwords.length > 1) ? 700 : 500, - minWidth: 500, - title: 'Launch Configuration', - callback: 'DialogReady', - onOpen: function(){ - Wait('stop'); - } - }); - - if (scope.removeDialogReady) { - scope.removeDialogReady(); - } - scope.removeDialogReady = scope.$on('DialogReady', function() { - $('#password-modal').dialog('open'); - $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); - e = angular.element(document.getElementById('password-accept-button')); - $compile(e)(scope); - // if(scope.prompt_for_vars===true){ - // setTimeout(function() { - // TextareaResize({ - // scope: scope, - // textareaId: 'job_variables', - // modalId: 'password-modal', - // formId: 'job_launch_form', - // parse: true - // }); - // }, 300); - // } - - }); - }; - - }]) - - - - - .factory('PromptForPasswords', ['$compile', 'Wait', 'Alert', 'CredentialForm', - function($compile, Wait, Alert, CredentialForm) { - return function(params) { - var scope = params.scope, - callback = params.callback || 'PasswordsAccepted', - url = params.url, - form = CredentialForm, - // acceptedPasswords = {}, - fld, field, - html=params.html || ""; - - scope.passwords = params.passwords; - // Wait('stop'); - - - html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; - // html += "
\n"; - - scope.passwords.forEach(function(password) { - // Prompt for password - field = form.fields[password]; - fld = password; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please enter a password.
\n"; - html += "
\n"; - html += "
\n"; - - // Add the related confirm field - if (field.associated) { - fld = field.associated; - field = form.fields[field.associated]; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please confirm the password.\n"; - html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; - html += "
\n"; - html += "\n"; - } - }); - // html += "\n"; - - - // $('#password-modal').empty().html(buildHtml); - // e = angular.element(document.getElementById('password-modal')); - // $compile(e)(scope); - scope.$emit(callback, html, url); - // CreateLaunchDialog({scope: scope}) - // buttons = [{ - // label: "Cancel", - // onClick: function() { - // scope.passwordCancel(); - // }, - // icon: "fa-times", - // "class": "btn btn-default", - // "id": "password-cancel-button" - // },{ - // label: "Continue", - // onClick: function() { - // scope.passwordAccept(); - // }, - // icon: "fa-check", - // "class": "btn btn-primary", - // "id": "password-accept-button" - // }]; - - - // CreateDialog({ - // id: 'password-modal', - // scope: scope, - // buttons: buttons, - // width: 600, - // height: (parent_scope.passwords.length > 1) ? 700 : 500, - // minWidth: 500, - // title: 'parent_scope.passwords Required', - // callback: 'DialogReady' - // }); - - // if (scope.removeDialogReady) { - // scope.removeDialogReady(); - // } - // scope.removeDialogReady = scope.$on('DialogReady', function() { - // $('#password-modal').dialog('open'); - // $('#password-accept-button').attr({ "disabled": "disabled" }); - // }); - // scope.keydown = function(e){ - // if(e.keyCode===13){ - // scope.passwordAccept(); - // } - // }; - - // scope.passwordAccept = function() { - // if (!scope.password_form.$invalid) { - // scope.passwords.forEach(function(password) { - // acceptedPasswords[password] = scope[password]; - // }); - // $('#password-modal').dialog('close'); - // scope.$emit(callback, acceptedPasswords); - // } - // }; - - // scope.passwordCancel = function() { - // $('#password-modal').dialog('close'); - // scope.$emit('CancelJob'); - // scope.$destroy(); - // }; - - // Password change - scope.clearPWConfirm = function (fld) { - // If password value changes, make sure password_confirm must be re-entered - scope[fld] = ''; - scope.job_launch_form[fld].$setValidity('awpassmatch', false); - scope.checkStatus(); - }; - - scope.checkStatus = function() { - if (!scope.job_launch_form.$invalid) { - $('#password-accept-button').removeAttr('disabled'); - } - else { - $('#password-accept-button').attr({ "disabled": "disabled" }); - } - }; - }; - }]) - - .factory('PromptForVars', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', - 'ParseVariableString', 'ToJSON', 'ProcessErrors', '$routeParams' , - function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait, - ParseVariableString, ToJSON, ProcessErrors, $routeParams) { - return function(params) { - var - // parent_scope = params.scope, - scope = params.scope, - callback = params.callback, - // job = params.job, - url = params.url, - vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', - html = params.html || ""; - - - function buildHtml(extra_vars){ - - html += GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); - html = html.replace("", ""); - scope.helpContainer = "
\n" + - "" + - " click for help
\n"; - - scope.helpText = "

After defining any extra variables, click Continue to start the job. Otherwise, click cancel to abort.

" + - "

Extra variables are passed as command line variables to the playbook run. It is equivalent to the -e or --extra-vars " + - "command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.

" + - "JSON:
\n" + - "
{
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n"; - - scope.extra_vars = ParseVariableString(extra_vars); - scope.parseType = 'yaml'; - scope.$emit(callback, html, url); - } - - Rest.setUrl(vars_url); - Rest.get() - .success(function (data) { - buildHtml(data.extra_vars); - - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, { hdr: 'Error!', - msg: 'Failed to retrieve organization: ' + $routeParams.id + '. GET status: ' + status }); - }); - - }; - }]) - - .factory('PromptForSurvey', ['$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', - 'GenerateForm', 'ShowSurveyModal', 'ProcessErrors', '$routeParams' , - function($compile, Wait, Alert, CredentialForm, CreateLaunchDialog, SurveyControllerInit, GetBasePath, Rest, Empty, - GenerateForm, ShowSurveyModal, ProcessErrors, $routeParams) { - return function(params) { - var html = params.html || "", - id= params.id, - url = params.url, - callback=params.callback, - scope = params.scope, - i, j, - requiredAsterisk, - requiredClasses, - defaultValue, - choices, - element, - minlength, maxlength, - checked, min, max, - survey_url = GetBasePath('job_templates') + id + '/survey_spec/' ; - - function buildHtml(question, index){ - question.index = index; - question.question_name = question.question_name.replace(//g, ">"); - question.question_description = (question.question_description) ? question.question_description.replace(//g, ">") : undefined; - - - requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; - requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; - - html+='
'; - html += '\n'; - - if(!Empty(question.question_description)){ - html += '
'+question.question_description+'
\n'; - } - - // if(question.default && question.default.indexOf('<') !== -1){ - // question.default = (question.default) ? question.default.replace(/') !== -1){ - // question.default = (question.default) ? question.default.replace(/>/g, ">") : undefined; - // } - scope[question.variable] = question.default; - - if(question.type === 'text' ){ - minlength = (!Empty(question.min)) ? Number(question.min) : ""; - maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ - '
'; - } - - if(question.type === "textarea"){ - scope[question.variable] = (question.default_textarea) ? question.default_textarea : (question.default) ? question.default : ""; - minlength = (!Empty(question.min)) ? Number(question.min) : ""; - maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ - '
'; - } - if(question.type === 'password' ){ - minlength = (!Empty(question.min)) ? Number(question.min) : ""; - maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ - '
'; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ - '
'; - html+= ''; - - } - if(question.type === 'multiplechoice'){ - choices = question.choices.split(/\n/); - element = (question.type==="multiselect") ? "checkbox" : 'radio'; - question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; - html+='
'; - for( j = 0; j/g, ">"); - html+= '' + - ''+choices[j] +'
' ; - } - html+= '
Please select an answer.
'+ - '
'; - html+= '
'; //end survey_taker_input - } - - if(question.type === "multiselect"){ - //seperate the choices out into an array - choices = question.choices.split(/\n/); - question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; - //ensure that the default answers are in an array - scope[question.variable] = question.default.split(/\n/); - //create a new object to be used by the surveyCheckboxes directive - scope[question.variable + '_object'] = { - name: question.variable, - value: (question.default.split(/\n/)[0]==="") ? [] : question.default.split(/\n/) , - required: question.required, - options:[] - }; - //load the options into the 'options' key of the new object - for(j=0; j'+ - '{{job_launch_form.'+question.variable+'_object.$error.checkbox}}'+ - '
Please select at least one answer.
'; - } - - if(question.type === 'integer'){ - min = (!Empty(question.min)) ? Number(question.min) : ""; - max = (!Empty(question.max)) ? Number(question.max) : "" ; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer that is a valid integer.
'+ - '
Please enter an answer between {{'+min+'}} and {{'+max+'}}.
'; - - } - - if(question.type === "float"){ - min = (!Empty(question.min)) ? question.min : ""; - max = (!Empty(question.max)) ? question.max : "" ; - defaultValue = (!Empty(question.default)) ? question.default : (!Empty(question.default_float)) ? question.default_float : "" ; - html+=''+ - '
Please enter an answer.
'+ - '
Please enter an answer that is a decimal number.
'+ - '
Please enter a decimal number between {{'+min+'}} and {{'+max+'}}.
'; - } - html+='
'; - if(question.index === scope.survey_questions.length-1){ - scope.$emit(callback, html, url); - } - } - - - - - Rest.setUrl(survey_url); - Rest.get() - .success(function (data) { - if(!Empty(data)){ - scope.survey_name = data.name; - scope.survey_description = data.description; - scope.survey_questions = data.spec; - - for(i=0; i0){ - scope.passwords_needed_to_start = passwords; - scope.$emit('PromptForPasswords', passwords, html, url); - } - else if (scope.ask_variables_on_launch){ - scope.$emit('PromptForVars', html, url); - } - else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { - scope.$emit('PromptForSurvey', html, url); - } - else { - scope.$emit('StartPlaybookRun', url); - } - } - - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - } - - }); - - // Get the job or job_template record - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - new_job_id = data.id; - launch_url = url;//data.related.start; - scope.passwords_needed_to_start = data.passwords_needed_to_start; - scope.prompt_for_vars = data.ask_variables_on_launch; - scope.survey_enabled = data.survey_enabled; - scope.ask_variables_on_launch = data.ask_variables_on_launch; - scope.variables_needed_to_start = data.variables_needed_to_start; - html = '
'; - - if(data.credential_needed_to_start === true){ - scope.$emit('PromptForCredential'); - } - else if (!Empty(data.passwords_needed_to_start) && data.passwords_needed_to_start.length > 0) { - scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); - } - else if (data.ask_variables_on_launch) { - scope.$emit('PromptForVars', html, url); - } - else if (!Empty(data.survey_enabled) && data.survey_enabled===true) { - scope.$emit('PromptForSurvey', html, url); - } - else { - scope.$emit('StartPlaybookRun', url); - } - - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - }; - } - ]) - - // Submit SCM Update request - .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', - 'ProjectsForm', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, ProjectsForm, Wait) { - return function (params) { - var scope = params.scope, - project_id = params.project_id, - url = GetBasePath('projects') + project_id + '/update/', - project; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { - // Refresh the project list after update request submitted - Wait('stop'); - if (/\d$/.test($location.path())) { - //Request submitted from projects/N page. Navigate back to the list so user can see status - $location.path('/projects'); - } - if (scope.socketStatus === 'error') { - Alert('Update Started', 'The request to start the SCM update process was submitted. ' + - 'To monitor the update status, refresh the page by clicking the button.', 'alert-info'); - if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - project = data; - if (project.can_update) { - if (project.passwords_needed_to_updated) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } - else { - Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); - }); - }; - } - ]) - - - // Submit Inventory Update request - .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { - return function (params) { - - var scope = params.scope, - url = params.url, - inventory_source; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { - Wait('stop'); - if (scope.socketStatus === 'error') { - Alert('Sync Started', 'The request to start the inventory sync process was submitted. ' + - 'To monitor the status refresh the page by clicking the button.', 'alert-info'); - if (scope.refreshGroups) { - // inventory detail page - scope.refreshGroups(); - } - else if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - inventory_source = data; - if (data.can_update) { - if (data.passwords_needed_to_update) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } else { - Wait('stop'); - Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); - }); - }; - } - ]); +'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) + +.factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', 'ToJSON', 'Empty', 'GetBasePath', +function(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath) { + return function(params) { + var scope = params.scope, + callback = params.callback || 'JobLaunched', + job_launch_data = {}, + url = params.url, + vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', + // fld, + extra_vars; + + //found it easier to assume that there will be extra vars, and then check for a blank object at the end + job_launch_data.extra_vars = {}; + + //gather the extra vars from the job template if survey is enabled and prompt for vars is false + if (scope.removeGetExtraVars) { + scope.removeGetExtraVars(); + } + scope.removeGetExtraVars = scope.$on('GetExtraVars', function() { + + Rest.setUrl(vars_url); + Rest.get() + .success(function (data) { + if(!Empty(data.extra_vars)){ + data.extra_vars = ToJSON('yaml', data.extra_vars, false); + $.each(data.extra_vars, function(key,value){ + job_launch_data.extra_vars[key] = value; + }); + } + scope.$emit('BuildData'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, { hdr: 'Error!', + msg: 'Failed to retrieve job template extra variables.' }); + }); + }); + + //build the data object to be sent to the job launch endpoint. Any variables gathered from the survey and the extra variables text editor are inserted into the extra_vars dict of the job_launch_data + if (scope.removeBuildData) { + scope.removeBuildData(); + } + scope.removeBuildData = scope.$on('BuildData', function() { + if(!Empty(scope.passwords_needed_to_start) && scope.passwords_needed_to_start.length>0){ + scope.passwords.forEach(function(password) { + job_launch_data[password] = scope[password]; + scope.passwords_needed_to_start.push(password+'_confirm'); // i'm pushing these values into this array for use during the survey taker parsing + }); + } + if(scope.prompt_for_vars===true){ + extra_vars = ToJSON(scope.parseType, scope.extra_vars, false); + if(!Empty(extra_vars)){ + $.each(extra_vars, function(key,value){ + job_launch_data.extra_vars[key] = value; + }); + } + + } + + if(scope.survey_enabled===true){ + for (var i=0; i < scope.survey_questions.length; i++){ + var fld = scope.survey_questions[i].variable; + // grab all survey questions that have answers + if(scope.survey_questions[i].required || (scope.survey_questions[i].required === false && scope[fld].toString()!=="")) { + job_launch_data.extra_vars[fld] = scope[fld]; + } + // for optional text and text-areas, submit a blank string if min length is 0 + if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && (scope[fld] === "" || scope[fld] === undefined)){ + job_launch_data.extra_vars[fld] = ""; + } + } + } + + // include the credential used if the user was prompted to choose a cred + if(!Empty(scope.credential)){ + job_launch_data.credential_id = scope.credential; + } + + // If the extra_vars dict is empty, we don't want to include it if we didn't prompt for anything. + if(jQuery.isEmptyObject(job_launch_data.extra_vars)===true && scope.prompt_for_vars===false){ + delete job_launch_data.extra_vars; + } + + Rest.setUrl(url); + Rest.post(job_launch_data) + .success(function(data) { + Wait('stop'); + if(!$('#password-modal').is(':hidden')){ + $('#password-modal').dialog('close'); + } + scope.$emit(callback, data); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status }); + }); + }); + + // if the user has a survey and does not have 'prompt for vars' selected, then we want to + // include the extra vars from the job template in the job launch. so first check for these conditions + // and then overlay any survey vars over those. + if(scope.prompt_for_vars===false && scope.survey_enabled===true){ + scope.$emit('GetExtraVars'); + } + else { + scope.$emit('BuildData'); + } + + + }; +}]) + +.factory('PromptForCredential', ['$location', 'Wait', 'GetBasePath', 'LookUpInit', 'JobTemplateForm', 'CredentialList', 'Rest', 'Prompt', 'ProcessErrors', +function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors) { + return function(params) { + + var scope = params.scope, + callback = params.callback || 'CredentialReady', + selectionMade; + + Wait('stop'); + scope.credential = ''; + + if (scope.removeShowLookupDialog) { + scope.removeShowLookupDialog(); + } + scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() { + selectionMade = function () { + scope.$emit(callback, scope.credential); + }; + + LookUpInit({ + url: GetBasePath('credentials') + '?kind=ssh', + scope: scope, + form: JobTemplateForm(), + current_item: null, + list: CredentialList, + field: 'credential', + hdr: 'Credential Required', + instructions: "Launching this job requires a machine credential. Please select your machine credential now or Cancel to quit.", + postAction: selectionMade, + input_type: 'radio' + }); + scope.lookUpCredential(); + }); + + if (scope.removeAlertNoCredentials) { + scope.removeAlertNoCredentials(); + } + scope.removeAlertNoCredentials = scope.$on('AlertNoCredentials', function() { + var action = function () { + $('#prompt-modal').modal('hide'); + $location.url('/credentials/add'); + }; + + Prompt({ + hdr: 'Machine Credential Required', + body: "
There are no machine credentials defined in Tower. Launching this job requires a machine credential. " + + "Create one now?", + action: action + }); + }); + + Rest.setUrl(GetBasePath('credentials') + '?kind=ssh'); + Rest.get() + .success(function(data) { + if (data.results.length > 0) { + scope.$emit('ShowLookupDialog'); + } + else { + scope.$emit('AlertNoCredentials'); + } + }) + .error(function(data,status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Checking for machine credentials failed. GET returned: ' + status }); + }); + }; +}]) + + + +.factory('CreateLaunchDialog', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateDialog', 'GenerateForm', +'JobVarsPromptForm', 'Wait', 'ParseTypeChange', +function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, + JobVarsPromptForm, Wait, ParseTypeChange) { + return function(params) { + var buttons, + scope = params.scope, + html = params.html, + // job_launch_data = {}, + callback = params.callback || 'PlaybookLaunchFinished', + // url = params.url, + e; + + // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; + html+=''; + $('#password-modal').empty().html(html); + $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); + e = angular.element(document.getElementById('password-modal')); + $compile(e)(scope); + + if(scope.prompt_for_vars===true){ + ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); + } + + buttons = [{ + label: "Cancel", + onClick: function() { + $('#password-modal').dialog('close'); + // scope.$emit('CancelJob'); + // scope.$destroy(); + }, + icon: "fa-times", + "class": "btn btn-default", + "id": "password-cancel-button" + },{ + label: "Launch", + onClick: function() { + scope.$emit(callback); + }, + icon: "fa-check", + "class": "btn btn-primary", + "id": "password-accept-button" + }]; + + CreateDialog({ + id: 'password-modal', + scope: scope, + buttons: buttons, + width: 620, + height: 700, //(scope.passwords.length > 1) ? 700 : 500, + minWidth: 500, + title: 'Launch Configuration', + callback: 'DialogReady', + onOpen: function(){ + Wait('stop'); + } + }); + + if (scope.removeDialogReady) { + scope.removeDialogReady(); + } + scope.removeDialogReady = scope.$on('DialogReady', function() { + $('#password-modal').dialog('open'); + $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); + e = angular.element(document.getElementById('password-accept-button')); + $compile(e)(scope); + // if(scope.prompt_for_vars===true){ + // setTimeout(function() { + // TextareaResize({ + // scope: scope, + // textareaId: 'job_variables', + // modalId: 'password-modal', + // formId: 'job_launch_form', + // parse: true + // }); + // }, 300); + // } + + }); + }; + + }]) + + + + + .factory('PromptForPasswords', ['$compile', 'Wait', 'Alert', 'CredentialForm', + function($compile, Wait, Alert, CredentialForm) { + return function(params) { + var scope = params.scope, + callback = params.callback || 'PasswordsAccepted', + url = params.url, + form = CredentialForm, + // acceptedPasswords = {}, + fld, field, + html=params.html || ""; + + scope.passwords = params.passwords; + // Wait('stop'); + + + html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; + // html += "
\n"; + + scope.passwords.forEach(function(password) { + // Prompt for password + field = form.fields[password]; + fld = password; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please enter a password.
\n"; + html += "
\n"; + html += "
\n"; + + // Add the related confirm field + if (field.associated) { + fld = field.associated; + field = form.fields[field.associated]; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please confirm the password.\n"; + html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; + html += "
\n"; + html += "\n"; + } + }); + // html += "\n"; + + + // $('#password-modal').empty().html(buildHtml); + // e = angular.element(document.getElementById('password-modal')); + // $compile(e)(scope); + scope.$emit(callback, html, url); + // CreateLaunchDialog({scope: scope}) + // buttons = [{ + // label: "Cancel", + // onClick: function() { + // scope.passwordCancel(); + // }, + // icon: "fa-times", + // "class": "btn btn-default", + // "id": "password-cancel-button" + // },{ + // label: "Continue", + // onClick: function() { + // scope.passwordAccept(); + // }, + // icon: "fa-check", + // "class": "btn btn-primary", + // "id": "password-accept-button" + // }]; + + + // CreateDialog({ + // id: 'password-modal', + // scope: scope, + // buttons: buttons, + // width: 600, + // height: (parent_scope.passwords.length > 1) ? 700 : 500, + // minWidth: 500, + // title: 'parent_scope.passwords Required', + // callback: 'DialogReady' + // }); + + // if (scope.removeDialogReady) { + // scope.removeDialogReady(); + // } + // scope.removeDialogReady = scope.$on('DialogReady', function() { + // $('#password-modal').dialog('open'); + // $('#password-accept-button').attr({ "disabled": "disabled" }); + // }); + // scope.keydown = function(e){ + // if(e.keyCode===13){ + // scope.passwordAccept(); + // } + // }; + + // scope.passwordAccept = function() { + // if (!scope.password_form.$invalid) { + // scope.passwords.forEach(function(password) { + // acceptedPasswords[password] = scope[password]; + // }); + // $('#password-modal').dialog('close'); + // scope.$emit(callback, acceptedPasswords); + // } + // }; + + // scope.passwordCancel = function() { + // $('#password-modal').dialog('close'); + // scope.$emit('CancelJob'); + // scope.$destroy(); + // }; + + // Password change + scope.clearPWConfirm = function (fld) { + // If password value changes, make sure password_confirm must be re-entered + scope[fld] = ''; + scope.job_launch_form[fld].$setValidity('awpassmatch', false); + scope.checkStatus(); + }; + + scope.checkStatus = function() { + if (!scope.job_launch_form.$invalid) { + $('#password-accept-button').removeAttr('disabled'); + } + else { + $('#password-accept-button').attr({ "disabled": "disabled" }); + } + }; + }; + }]) + + .factory('PromptForVars', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', + 'ParseVariableString', 'ToJSON', 'ProcessErrors', '$routeParams' , + function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait, + ParseVariableString, ToJSON, ProcessErrors, $routeParams) { + return function(params) { + var + // parent_scope = params.scope, + scope = params.scope, + callback = params.callback, + // job = params.job, + url = params.url, + vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', + html = params.html || ""; + + + function buildHtml(extra_vars){ + + html += GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); + html = html.replace("", ""); + scope.helpContainer = "
\n" + + "" + + " click for help
\n"; + + scope.helpText = "

After defining any extra variables, click Continue to start the job. Otherwise, click cancel to abort.

" + + "

Extra variables are passed as command line variables to the playbook run. It is equivalent to the -e or --extra-vars " + + "command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.

" + + "JSON:
\n" + + "
{
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n"; + + scope.extra_vars = ParseVariableString(extra_vars); + scope.parseType = 'yaml'; + scope.$emit(callback, html, url); + } + + Rest.setUrl(vars_url); + Rest.get() + .success(function (data) { + buildHtml(data.extra_vars); + + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, { hdr: 'Error!', + msg: 'Failed to retrieve organization: ' + $routeParams.id + '. GET status: ' + status }); + }); + + }; + }]) + + .factory('PromptForSurvey', ['$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', + 'GenerateForm', 'ShowSurveyModal', 'ProcessErrors', '$routeParams' , + function($compile, Wait, Alert, CredentialForm, CreateLaunchDialog, SurveyControllerInit, GetBasePath, Rest, Empty, + GenerateForm, ShowSurveyModal, ProcessErrors, $routeParams) { + return function(params) { + var html = params.html || "", + id= params.id, + url = params.url, + callback=params.callback, + scope = params.scope, + i, j, + requiredAsterisk, + requiredClasses, + defaultValue, + choices, + element, + minlength, maxlength, + checked, min, max, + survey_url = GetBasePath('job_templates') + id + '/survey_spec/' ; + + function buildHtml(question, index){ + question.index = index; + question.question_name = question.question_name.replace(//g, ">"); + question.question_description = (question.question_description) ? question.question_description.replace(//g, ">") : undefined; + + + requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; + requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; + + html+='
'; + html += '\n'; + + if(!Empty(question.question_description)){ + html += '
'+question.question_description+'
\n'; + } + + // if(question.default && question.default.indexOf('<') !== -1){ + // question.default = (question.default) ? question.default.replace(/') !== -1){ + // question.default = (question.default) ? question.default.replace(/>/g, ">") : undefined; + // } + scope[question.variable] = question.default; + + if(question.type === 'text' ){ + minlength = (!Empty(question.min)) ? Number(question.min) : ""; + maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ + '
'; + } + + if(question.type === "textarea"){ + scope[question.variable] = (question.default_textarea) ? question.default_textarea : (question.default) ? question.default : ""; + minlength = (!Empty(question.min)) ? Number(question.min) : ""; + maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ + '
'; + } + if(question.type === 'password' ){ + minlength = (!Empty(question.min)) ? Number(question.min) : ""; + maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ + '
'; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ + '
'; + html+= ''; + + } + if(question.type === 'multiplechoice'){ + choices = question.choices.split(/\n/); + element = (question.type==="multiselect") ? "checkbox" : 'radio'; + question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; + html+='
'; + for( j = 0; j/g, ">"); + html+= '' + + ''+choices[j] +'
' ; + } + html+= '
Please select an answer.
'+ + '
'; + html+= '
'; //end survey_taker_input + } + + if(question.type === "multiselect"){ + //seperate the choices out into an array + choices = question.choices.split(/\n/); + question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; + //ensure that the default answers are in an array + scope[question.variable] = question.default.split(/\n/); + //create a new object to be used by the surveyCheckboxes directive + scope[question.variable + '_object'] = { + name: question.variable, + value: (question.default.split(/\n/)[0]==="") ? [] : question.default.split(/\n/) , + required: question.required, + options:[] + }; + //load the options into the 'options' key of the new object + for(j=0; j'+ + '{{job_launch_form.'+question.variable+'_object.$error.checkbox}}'+ + '
Please select at least one answer.
'; + } + + if(question.type === 'integer'){ + min = (!Empty(question.min)) ? Number(question.min) : ""; + max = (!Empty(question.max)) ? Number(question.max) : "" ; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer that is a valid integer.
'+ + '
Please enter an answer between {{'+min+'}} and {{'+max+'}}.
'; + + } + + if(question.type === "float"){ + min = (!Empty(question.min)) ? question.min : ""; + max = (!Empty(question.max)) ? question.max : "" ; + defaultValue = (!Empty(question.default)) ? question.default : (!Empty(question.default_float)) ? question.default_float : "" ; + html+=''+ + '
Please enter an answer.
'+ + '
Please enter an answer that is a decimal number.
'+ + '
Please enter a decimal number between {{'+min+'}} and {{'+max+'}}.
'; + } + html+='
'; + if(question.index === scope.survey_questions.length-1){ + scope.$emit(callback, html, url); + } + } + + + + + Rest.setUrl(survey_url); + Rest.get() + .success(function (data) { + if(!Empty(data)){ + scope.survey_name = data.name; + scope.survey_description = data.description; + scope.survey_questions = data.spec; + + for(i=0; i0){ + scope.passwords_needed_to_start = passwords; + scope.$emit('PromptForPasswords', passwords, html, url); + } + else if (scope.ask_variables_on_launch){ + scope.$emit('PromptForVars', html, url); + } + else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { + scope.$emit('PromptForSurvey', html, url); + } + else { + scope.$emit('StartPlaybookRun', url); + } + } + + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + } + + }); + + // Get the job or job_template record + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + new_job_id = data.id; + launch_url = url;//data.related.start; + scope.passwords_needed_to_start = data.passwords_needed_to_start; + scope.prompt_for_vars = data.ask_variables_on_launch; + scope.survey_enabled = data.survey_enabled; + scope.ask_variables_on_launch = data.ask_variables_on_launch; + scope.variables_needed_to_start = data.variables_needed_to_start; + html = '
'; + + if(data.credential_needed_to_start === true){ + scope.$emit('PromptForCredential'); + } + else if (!Empty(data.passwords_needed_to_start) && data.passwords_needed_to_start.length > 0) { + scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); + } + else if (data.ask_variables_on_launch) { + scope.$emit('PromptForVars', html, url); + } + else if (!Empty(data.survey_enabled) && data.survey_enabled===true) { + scope.$emit('PromptForSurvey', html, url); + } + else { + scope.$emit('StartPlaybookRun', url); + } + + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + }; + } + ]) + + // Submit SCM Update request + .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', + 'ProjectsForm', 'Wait', + function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, ProjectsForm, Wait) { + return function (params) { + var scope = params.scope, + project_id = params.project_id, + url = GetBasePath('projects') + project_id + '/update/', + project; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { + // Refresh the project list after update request submitted + Wait('stop'); + if (/\d$/.test($location.path())) { + //Request submitted from projects/N page. Navigate back to the list so user can see status + $location.path('/projects'); + } + if (scope.socketStatus === 'error') { + Alert('Update Started', 'The request to start the SCM update process was submitted. ' + + 'To monitor the update status, refresh the page by clicking the button.', 'alert-info'); + if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + project = data; + if (project.can_update) { + if (project.passwords_needed_to_updated) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } + else { + Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); + }); + }; + } + ]) + + + // Submit Inventory Update request + .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', + function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { + return function (params) { + + var scope = params.scope, + url = params.url, + inventory_source; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { + Wait('stop'); + if (scope.socketStatus === 'error') { + Alert('Sync Started', 'The request to start the inventory sync process was submitted. ' + + 'To monitor the status refresh the page by clicking the button.', 'alert-info'); + if (scope.refreshGroups) { + // inventory detail page + scope.refreshGroups(); + } + else if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + inventory_source = data; + if (data.can_update) { + if (data.passwords_needed_to_update) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } else { + Wait('stop'); + Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); + }); + }; + } + ]); diff --git a/awx/ui/static/js/helpers/JobTemplates.js b/awx/ui/static/js/helpers/JobTemplates.js index 5e55668282..bc69745570 100644 --- a/awx/ui/static/js/helpers/JobTemplates.js +++ b/awx/ui/static/js/helpers/JobTemplates.js @@ -171,6 +171,11 @@ angular.module('JobTemplatesHelper', ['Utilities']) input_type: "radio" }); + + if(scope.project === "" && scope.playbook === ""){ + scope.toggleScanInfo(); + } + RelatedSearchInit({ scope: scope, form: form, @@ -193,7 +198,4 @@ angular.module('JobTemplatesHelper', ['Utilities']) }; }; - - - }]); diff --git a/awx/ui/static/js/job-templates/delete-job-template.service.js b/awx/ui/static/js/job-templates/delete-job-template.service.js new file mode 100644 index 0000000000..a61754b460 --- /dev/null +++ b/awx/ui/static/js/job-templates/delete-job-template.service.js @@ -0,0 +1,20 @@ +var rest, getBasePath; + +export default + [ 'Rest', + 'GetBasePath', + function(_rest, _getBasePath) { + rest = _rest; + getBasePath = _getBasePath; + return deleteJobTemplate; + } + ]; + +function deleteJobTemplate(id) { + var url = getBasePath('job_templates'); + + url = url + id; + + rest.setUrl(url); + return rest.destroy(); +} diff --git a/awx/ui/static/js/job-templates/main.js b/awx/ui/static/js/job-templates/main.js new file mode 100644 index 0000000000..3daabbf1d6 --- /dev/null +++ b/awx/ui/static/js/job-templates/main.js @@ -0,0 +1,5 @@ +import deleteJobTemplate from './delete-job-template.service'; + +export default + angular.module('jobTemplates', []) + .service('deleteJobTemplate', deleteJobTemplate); diff --git a/awx/ui/static/js/lists/JobTemplates.js b/awx/ui/static/js/lists/JobTemplates.js index 6b1afb5df2..536768a692 100644 --- a/awx/ui/static/js/lists/JobTemplates.js +++ b/awx/ui/static/js/lists/JobTemplates.js @@ -95,7 +95,6 @@ export default awToolTip: 'Copy template', dataPlacement: 'top', ngHide: 'job_template.summary_fields.can_copy===false' - } } }); diff --git a/awx/ui/static/js/lists/ScanJobs.js b/awx/ui/static/js/lists/ScanJobs.js index c13b864f4f..33b1d6b0f8 100644 --- a/awx/ui/static/js/lists/ScanJobs.js +++ b/awx/ui/static/js/lists/ScanJobs.js @@ -15,8 +15,8 @@ export default angular.module('ScanJobsListDefinition', []) .value( 'ScanJobsList', { - name: 'scan_jobs', - iterator: 'scan_job', + name: 'scan_job_templates', + iterator: 'scan_job_template', editTitle: 'Scan Jobs', 'class': 'table-condensed', index: false, diff --git a/awx/ui/static/js/shared/form-generator.js b/awx/ui/static/js/shared/form-generator.js index 9affa6dcd8..516b478081 100644 --- a/awx/ui/static/js/shared/form-generator.js +++ b/awx/ui/static/js/shared/form-generator.js @@ -1107,6 +1107,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\" "; html += this.attr(field, 'ngOptions'); html += (field.ngChange) ? this.attr(field, 'ngChange') : ""; + html += (field.ngDisabled) ? this.attr(field, 'ngDisabled'): ""; + html += (field.ngRequired) ? this.attr(field, 'ngRequired') : ""; html += buildId(field, fld, this.form); html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; html += (options.mode === 'add' && field.addRequired) ? "required " : ""; @@ -1331,6 +1333,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\n"; html += "\n"; html += "\n"; html += "
-
+
+
diff --git a/awx/ui/static/partials/scan-job-template-smart-status.html b/awx/ui/static/partials/scan-job-template-smart-status.html new file mode 100644 index 0000000000..cb5787fed1 --- /dev/null +++ b/awx/ui/static/partials/scan-job-template-smart-status.html @@ -0,0 +1 @@ + diff --git a/awx/ui/tests/unit/job-templates/delete-job-template.service-test.js b/awx/ui/tests/unit/job-templates/delete-job-template.service-test.js new file mode 100644 index 0000000000..f1fcb871fc --- /dev/null +++ b/awx/ui/tests/unit/job-templates/delete-job-template.service-test.js @@ -0,0 +1,24 @@ +import jobTemplates from 'tower/job-templates/main'; +import {describeModule} from '../describe-module'; + +describeModule(jobTemplates.name) + .testService('deleteJobTemplate', function(test, restStub) { + + var service; + + test.withService(function(_service) { + service = _service; + }); + + it('deletes the job template', function() { + var result = {}; + + var actual = service(); + + restStub.succeedOn('destroy', result); + restStub.flush(); + + expect(actual).to.eventually.equal(result); + + }); + }); diff --git a/awx/ui/tests/unit/rest-stub.js b/awx/ui/tests/unit/rest-stub.js index be91f1afe7..e73c3b735b 100644 --- a/awx/ui/tests/unit/rest-stub.js +++ b/awx/ui/tests/unit/rest-stub.js @@ -35,10 +35,19 @@ RestStub.prototype = return this.deferred.promise; }, + destroy: function() { + this.deferred = this.deferred || {}; + this.deferred.destroy = this[this.currentUrl]; + + return this.deferred.destroy.promise; + }, succeedAt: function(url, value) { assertUrlDeferred(url, this); this[url].resolve(value); }, + succeedOn: function(method, value) { + this.deferred[method] = value; + }, succeed: function(value) { this.deferred.resolve(value); },