Merge branch 'mabashian-job-launch-modal-overhaul' into devel

This commit is contained in:
Michael Abashian 2016-05-19 13:22:41 -04:00
commit bd8ce603fd
25 changed files with 1417 additions and 813 deletions

View File

@ -472,6 +472,7 @@ input[type='radio']:checked:before {
transition: background-color 0.2s;
padding-left:15px;
padding-right: 15px;
margin-left: 20px;
}
.Form-cancelButton:hover{
@ -496,6 +497,7 @@ input[type='radio']:checked:before {
.Form-formGroup--singleColumn {
width: 100% !important;
padding-right: 0px;
max-width: 100% !important;
}
.Form-subCheckbox {

View File

@ -33,6 +33,7 @@ import organizations from './organizations/main';
import permissions from './permissions/main';
import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
import jobSubmission from './job-submission/main';
import notifications from './notifications/main';
import access from './access/main';
@ -103,6 +104,7 @@ var tower = angular.module('Tower', [
activityStream.name,
footer.name,
jobDetail.name,
jobSubmission.name,
notifications.name,
standardOut.name,
access.name,

View File

@ -1,9 +1,9 @@
/* jshint unused: vars */
export default
[ "PlaybookRun",
[ 'InitiatePlaybookRun',
'templateUrl',
'$location',
function JobTemplatesList(PlaybookRun, templateUrl, $location) {
function JobTemplatesList(InitiatePlaybookRun, templateUrl, $location) {
return {
restrict: 'E',
link: link,
@ -43,7 +43,7 @@ export default
};
scope.launchJobTemplate = function(jobTemplateId){
PlaybookRun({ scope: scope, id: jobTemplateId });
InitiatePlaybookRun({ scope: scope, id: jobTemplateId });
};
scope.editJobTemplate = function (jobTemplateId) {

View File

@ -55,12 +55,9 @@ export default
// Submit request to run an adhoc comamand
.factory('AdhocRun', ['$location','$stateParams', 'LaunchJob',
'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors',
'Wait', 'Empty', 'PromptForCredential', 'PromptForVars',
'PromptForSurvey' , 'CreateLaunchDialog',
'Wait', 'Empty', 'CreateLaunchDialog',
function ($location, $stateParams, LaunchJob, PromptForPasswords,
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty,
PromptForCredential, PromptForVars, PromptForSurvey,
CreateLaunchDialog) {
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog) {
return function (params) {
var id = params.project_id,
scope = params.scope.$new(),

View File

@ -1,306 +1,17 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:JobSubmission
* @description
* The JobSubmission.js file handles launching a job via a playbook run. There is a workflow that is involved in gathering all the
* variables needed to launch a job, including credentials, passwords, extra variables, and survey data. Depending on what information
* is needed to launch the job, a modal is built that prompts the user for any required information. This modal is built by creating
* an html string with all the fields necessary to launch the job. This html string then gets compiled and opened in a dialog modal.
*
* #Workflow when user hits launch button
*
* A 'get' call is made to the API's 'job_templates/:job_template_id/launch' endpoint for that job template. The response from the API will specify
*
*```
* "credential_needed_to_start": true,
* "can_start_without_user_input": false,
* "ask_variables_on_launch": false,
* "passwords_needed_to_start": [],
* "variables_needed_to_start": [],
* "survey_enabled": false
*```
* #Step 1a - Check if there is a credential included in the job template: PromptForCredential
*
* The first step is to check if a credential was specified in the job template, by looking at the value of `credential_needed_to_start` .
* If this boolean is true, then that means that the user did NOT specify a credential in the job template and we must prompt them to select a credential.
* This emits a call to `PromptForCredential` which will do a lookup on the credentials endpoint and show a modal window with the list
* of credentials for the user to choose from.
*
* #Step 1b - Check if the credential requires a password: CheckPasswords
*
* The second part of this process is to check if the credential the user picks requires a prompt for a password. A call is made (in the `CheckPasswords` factory)
* to the chosen credential
* and checks if ``password: ASK`` , ``become_password:ASK`` , or ``vault_password: ASK``. If any of these are ASK, then we begin building the html string for
* each required password (see step 2). If none of these require a password, then we contine on to prompting for vars (see step 3)
*
* #Step 2 - Build password html string: PromptForPasswords
*
* We may detect from the inital 'get' call that we may need to prompt the user for passwords. The ``passwords_needed_to_start`` array from the 'get' call
* will explictly tell us which passwords we must prompt for. Alternatively, we may have found that in steps 1a and 1b that
* we have must prompt for passwords. Either way, we arrive in `PromptForPasswords` factory which builds the html string depending on how the particular credential is setup.
*
* #Step 3 - extra vars text editor: PromptForVars
*
* We may arrive at step three if the credential selected does not require a password, or if the password html string is already done being built.
* if ``ask_variables_on_launch`` was true in the inital 'get' call, then we build the extra_vars text editor in the `PromptForVars` factory.
* This factory makes a REST call to the job template and finds if any 'extra_vars' were specified in the job template. It takes any specified
* extra vars and includes them in the extra_vars text editor that is built in the same factory. This code is added to the html string and passed along
* to the next step.
*
* #Step 4 - Survey Taker: PromptForSurvey
*
* The last step in building the job submission modal is building the survey taker. If ``survey_enabled`` is true from the initial 'get' call,
* we make a REST call to the survey endpoint for the specified job and gather the survey data. The `PromptForSurvey` factory takes the survey
* data and adds to the html string any various survey question.
*
* #Step 5 - build the modal: CreateLaunchDialog
*
* At this point, we need to compile our giant html string onto the modal and open the job submission modal. This happens in the `CreateLaunch`
* factory. In this factory the 'Launch' button for the job is tied to the validity of the form, which handles the validation of these fields.
*
* #Step 6 - Launch the job: LaunchJob
*
* This is maybe the most crucial step. We have setup everything we need in order to gather information from the user and now we want to be sure
* we handle it correctly. And there are many scenarios to take into account. The first scenario we check for is is ``survey_enabled=true`` and
* ``prompt_for_vars=false``, in which case we want to make sure to include the extra_vars from the job template in the data being
* sent to the API (it is important to note that anything specified in the extra vars on job submission will override vars specified in the job template.
* Likewise, any variables specified in the extra vars that are duplicated by the survey vars, will get overridden by the survey vars).
* If the previous scenario is NOT the case, then we continue to gather the modal's answers regularly: gather the passwords, then the extra_vars, then
* any survey results. Also note that we must gather any required survey answers, as well as any optional survey answers that happened to be provided
* by the user. We also include the credential that was chosen if the user was prompted to select a credential.
* At this point we have all the info we need and we are almost ready to perform a POST to the '/launch' endpoint. We must lastly check
* if the user was not prompted for anything and therefore we don't want to pass any extra_vars to the POST. Once this is done we
* make the REST POST call and provide all the data to hte API. The response from the API will be the job ID, which is used to redirect the user
* to the job detail page for that job run.
*
* @Usage
* This is usage information.
*/
'use strict';
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];
}
if(scope.survey_questions[i].required === false && _.isEmpty(scope[fld])) {
switch (scope.survey_questions[i].type) {
// for optional text and text-areas, submit a blank string if min length is 0
// -- this is confusing, for an explanation see:
// http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions
//
case "text":
case "textarea":
if (scope.survey_questions[i].min === 0) {
job_launch_data.extra_vars[fld] = "";
}
break;
// for optional select lists, if they are left blank make sure we submit
// a value that the API will consider "empty"
//
case "multiplechoice":
job_launch_data.extra_vars[fld] = "";
break;
case "multiselect":
job_launch_data.extra_vars[fld] = [];
break;
}
}
}
}
// 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',
'CheckPasswords',
function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors, CheckPasswords) {
return function(params) {
var scope = params.scope,
selectionMade;
Wait('stop');
scope.credential = '';
if (scope.removeShowLookupDialog) {
scope.removeShowLookupDialog();
}
scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() {
selectionMade = function () {
// scope.$emit(callback, scope.credential);
CheckPasswords({
scope: scope,
credential: scope.credential,
callback: 'ContinueCred'
});
};
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: "<div class=\"Prompt-bodyQuery\">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) {
.factory('CreateLaunchDialog', ['$compile', 'CreateDialog', 'Wait', 'ParseTypeChange',
function($compile, CreateDialog, Wait, ParseTypeChange) {
return function(params) {
var buttons,
scope = params.scope,
@ -363,43 +74,25 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
$('#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) {
.factory('PromptForPasswords', ['CredentialForm',
function(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 += "<div class=\"alert alert-info\">Launching this job requires the passwords listed below. Enter and confirm each password before continuing.</div>\n";
// html += "<form name=\"password_form\" novalidate>\n";
scope.passwords.forEach(function(password) {
// Prompt for password
field = form.fields[password];
@ -409,7 +102,6 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
html += "<label for=\"" + fld + "\">" + field.label + "</label>\n";
html += "<input type=\"password\" ";
html += "ng-model=\"" + fld + '" ';
// html += "ng-keydown=\"keydown($event)\" ";
html += 'name="' + fld + '" ';
html += "class=\"password-field form-control input-sm\" ";
html += (field.associated) ? "ng-change=\"clearPWConfirm('" + field.associated + "')\" " : "";
@ -430,7 +122,6 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
html += "<label for=\"" + fld + "\"> " + field.label + "</label>\n";
html += "<input type=\"password\" ";
html += "ng-model=\"" + fld + '" ';
// html += "ng-keydown=\"keydown($event)\" ";
html += 'name="' + fld + '" ';
html += "class=\"form-control input-sm\" ";
html += "ng-change=\"checkStatus()\" ";
@ -468,246 +159,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
};
}])
.factory('PromptForVars', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait',
'ParseVariableString', 'ToJSON', 'ProcessErrors', '$stateParams' ,
function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait,
ParseVariableString, ToJSON, ProcessErrors, $stateParams) {
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("</form>", "");
scope.helpContainer = "<div style=\"display:inline-block; font-size: 12px; margin-top: 6px;\" class=\"help-container pull-right\">\n" +
"<a href=\"\" id=\"awp-promote\" href=\"\" aw-pop-over=\"{{ helpText }}\" aw-tool-tip=\"Click for help\" aw-pop-over-watch=\"helpText\" " +
"aw-tip-placement=\"top\" data-placement=\"bottom\" data-container=\"body\" data-title=\"Help\" class=\"help-link\"><i class=\"fa fa-question-circle\">" +
"</i> click for help</a></div>\n";
scope.helpText = "<p>After defining any extra variables, click Continue to start the job. Otherwise, click cancel to abort.</p>" +
"<p>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.</p>" +
"JSON:<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
"YAML:<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\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: ' + $stateParams.id + '. GET status: ' + status });
});
};
}])
.factory('PromptForSurvey', ['$filter', '$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'GetBasePath', 'Rest' , 'Empty',
'GenerateForm', 'ProcessErrors', '$stateParams' ,
function($filter, $compile, Wait, Alert, CredentialForm, CreateLaunchDialog, GetBasePath, Rest, Empty,
GenerateForm, ProcessErrors, $stateParams) {
return function(params) {
var html = params.html || "",
id= params.id,
url = params.url,
callback=params.callback,
scope = params.scope,
i,
requiredAsterisk,
requiredClasses,
defaultValue,
choices,
element,
minlength, maxlength,
min, max,
survey_url = GetBasePath('job_templates') + id + '/survey_spec/' ;
//for toggling the input on password inputs
scope.toggleInput = function(id) {
var buttonId = id + "_show_input_button",
inputId = id,
buttonInnerHTML = $(buttonId).html();
if (buttonInnerHTML.indexOf("Show") > -1) {
$(buttonId).html("Hide");
$(inputId).attr("type", "text");
} else {
$(buttonId).html("Show");
$(inputId).attr("type", "password");
}
};
function buildHtml(question, index){
question.index = index;
question.question_name = $filter('sanitize')(question.question_name);
question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined;
requiredAsterisk = (question.required===true) ? "prepend-asterisk" : "";
requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : "";
html+='<div id="taker_'+question.index+'" class="form-group">';
html += '<label for="'+question.variable+'"><span class="label-text '+requiredAsterisk+'"> '+question.question_name+'</span></label>';
// html += '<label for="'+question.variable+'"> '+ question.question_name+'</label>\n';
if(!Empty(question.question_description)){
html += '<div class="survey_taker_description"><i>'+question.question_description+'</i></div>\n';
}
// if(question.default && question.default.indexOf('<') !== -1){
// question.default = (question.default) ? question.default.replace(/</g, "&lt;") : undefined;
// }
// if (question.default && question.default.indexOf('>') !== -1){
// question.default = (question.default) ? question.default.replace(/>/g, "&gt;") : 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+='<input type="text" id="'+question.variable+'" ng-model="'+question.variable+'" '+
'name=" '+question.variable+' " ' +
'ng-minlength="'+minlength+'" ng-maxlength="'+maxlength+'" '+
'class="form-control" ng-required='+question.required+'>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && ' +
'job_launch_form.'+question.variable+'.$error.required\">Please enter an answer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$error.minlength || ' +
'job_launch_form.'+question.variable+'.$error.maxlength\">Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.</div>'+
'<div class=\"error api-error\" ng-bind=\"" + fld + "_api_error\"></div>';
}
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+='<textarea id="'+question.variable+'" name="'+question.variable+'" ng-model="'+question.variable+'" '+
'ng-minlength="'+minlength+'" ng-maxlength="'+maxlength+'" '+
'class="form-control final" ng-required="'+question.required+'" rows="3"></textarea>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && ' +
'job_launch_form.'+question.variable+'.$error.required\">Please enter an answer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$error.minlength || ' +
'job_launch_form.'+question.variable+'.$error.maxlength\">Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.</div>'+
'<div class=\"error api-error\" ng-bind=\"" + fld + "_api_error\"></div>';
}
if(question.type === 'password' ){
minlength = (!Empty(question.min)) ? Number(question.min) : "";
maxlength =(!Empty(question.max)) ? Number(question.max) : "" ;
html+= '<div class="input-group">'+
'<span class="input-group-btn">'+
'<button class="btn btn-default show_input_button" id="'+question.variable +'_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="toggleInput(&quot;#'+question.variable+'&quot;)" data-original-title="" title="">Show</button>'+
'</span>'+
'<input id="'+question.variable+'" type="password" ng-model="'+question.variable+'" name="'+question.variable+'" '+
'ng-required="'+question.required+'"'+
'ng-minlength="'+minlength+'" ng-maxlength="'+maxlength+'" '+
'class="form-control ng-pristine ng-valid-api-error ng-invalid" autocomplete="false">'+
'</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && ' +
'job_launch_form.'+question.variable+'.$error.required\">Please enter an answer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$error.minlength || ' +
'job_launch_form.'+question.variable+'.$error.maxlength\">Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.</div>'+
'<div class=\"error api-error\" ng-bind=\"" + fld + "_api_error\"></div>';
}
if(question.type === 'multiplechoice'){
choices = question.choices.split(/\n/);
element = (question.type==="multiselect") ? "checkbox" : 'radio';
if (question.default) {
scope[question.variable] = question.default;
} else {
scope[question.variable] = '';
}
html+='<div class="survey_taker_input" > ';
html += '<survey-question type="' + question.type + '" index="' + question.index + '" survey-questions="survey_questions" ng-model="' + question.variable + '" ng-required="' + question.required + '"></survey-question>';
// html+= '<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && ' +
// 'job_launch_form.'+question.variable+'.$error.required\">Please select an answer.</div>'+
// '<div class=\"error api-error\" ng-bind=\"" + fld + "_api_error\"></div>';
html+= '</div>'; //end survey_taker_input
}
if(question.type === "multiselect"){
//seperate the choices out into an array
choices = question.choices.split(/\n/);
//ensure that the default answers are in an array
if (question.default) {
scope[question.variable] = question.default.split(/\n/);
} else {
scope[question.variable] = '';
}
//create a new object to be used by the surveyCheckboxes directive
html += '<survey-question type="' + question.type + '" index="' + question.index + '" survey-questions="survey_questions" ng-model="' + question.variable + '" ng-required="' + question.required + '"></survey-question>';
// html += '<survey-checkboxes name="'+question.variable+'" ng-model=" '+question.variable + '_object " ng-required="'+question.required+'">'+
// '</survey-checkboxes>{{job_launch_form.'+question.variable+'_object.$error.checkbox}}'+
// '<div class="error survey_error" ng-show="job_launch_form.'+question.variable+'.$error.checkbox">Please select at least one answer.</div>';
}
if(question.type === 'integer'){
min = (!Empty(question.min)) ? Number(question.min) : "";
max = (!Empty(question.max)) ? Number(question.max) : "" ;
html+='<input type="number" id="'+question.variable+'" ng-model="'+question.variable+'" class="form-control" name="'+question.variable+'" ng-required="'+question.required+'" integer aw-min="'+min+'" aw-max="'+max+'" />'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && job_launch_form.'+question.variable+'.$error.required">Please enter an answer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+question.variable+'.$error.number || job_launch_form.'+question.variable+'.$error.integer" >Please enter an answer that is a valid integer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+question.variable+'.$error.awMin || job_launch_form.'+question.variable+'.$error.awMax">Please enter an answer between {{'+min+'}} and {{'+max+'}}.</div>';
}
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+='<input type="number" id="'+question.variable+'" ng-model="'+question.variable+'" class=" form-control" name="'+question.variable+'" ng-required="'+question.required+'" smart-float aw-min="'+min+'" aw-max="'+max+'"/>'+
'<div class="error survey_error" ng-show="job_launch_form.'+ question.variable + '.$dirty && job_launch_form.'+question.variable+'.$error.required">Please enter an answer.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+question.variable+'.$error.number || job_launch_form.'+question.variable+'.$error.float">Please enter an answer that is a decimal number.</div>'+
'<div class="error survey_error" ng-show="job_launch_form.'+question.variable+'.$error.awMin || job_launch_form.'+question.variable+'.$error.awMax">Please enter a decimal number between {{'+min+'}} and {{'+max+'}}.</div>';
}
html+='</div>';
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; i<scope.survey_questions.length; i++){
buildHtml(scope.survey_questions[i], i);
}
}
})
.error(function (data, status) {
ProcessErrors(scope, data, status, { hdr: 'Error!',
msg: 'Failed to retrieve organization: ' + $stateParams.id + '. GET status: ' + status });
});
};
}])
.factory('CheckPasswords', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait',
'ParseVariableString', 'ToJSON', 'ProcessErrors', '$stateParams', 'Empty',
function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait,
ParseVariableString, ToJSON, ProcessErrors, $stateParams, Empty) {
.factory('CheckPasswords', ['Rest', 'GetBasePath', 'ProcessErrors', 'Empty',
function(Rest, GetBasePath, ProcessErrors, Empty) {
return function(params) {
var scope = params.scope,
callback = params.callback,
@ -743,224 +196,9 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
};
}])
/**
* @ngdoc method
* @name helpers.function:JobSubmission#PlaybookRun
* @methodOf helpers.function:JobSubmission
* @description The playbook Run function is run when the user clicks the launch button
*
*/
// Submit request to run a playbook
.factory('PlaybookRun', ['$location', '$state', '$stateParams', 'LaunchJob', 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', 'Wait', 'Empty',
'PromptForCredential', 'PromptForVars', 'PromptForSurvey' , 'CreateLaunchDialog',
function ($location, $state, $stateParams, LaunchJob, PromptForPasswords, Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty,
PromptForCredential, PromptForVars, PromptForSurvey, CreateLaunchDialog) {
return function (params) {
var //parent_scope = params.scope,
scope = params.scope.$new(),
id = params.id,
system_job = params.system_job || false,
base = $location.path().replace(/^\//, '').split('/')[0],
url,
extra_vars,
new_job_id,
launch_url,
html;
scope.job_template_id = id;
if (base === 'job_templates' || base === 'portal' || base === 'inventories' || base === 'home') {
url = GetBasePath('job_templates') + id + '/launch/';
}
else {
url = GetBasePath('jobs') + id + '/relaunch/';
}
if(!Empty(system_job) && system_job === 'launch'){
url = GetBasePath('system_job_templates') + id + '/launch/';
}
if (scope.removeCancelJob) {
scope.removeCancelJob();
}
scope.removeCancelJob = scope.$on('CancelJob', function() {
// Delete the job
Wait('start');
Rest.setUrl(GetBasePath('jobs') + new_job_id + '/');
Rest.destroy()
.success(function() {
Wait('stop');
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
});
if (scope.removePlaybookLaunchFinished) {
scope.removePlaybookLaunchFinished();
}
scope.removePlaybookLaunchFinished = scope.$on('PlaybookLaunchFinished', function(e, data) {
var job = data.job || data.system_job;
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
(base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in
$state.go('jobDetail', {id: job}, {reload: true});
}
});
if (scope.removeStartPlaybookRun) {
scope.removeStartPlaybookRun();
}
scope.removeStartPlaybookRun = scope.$on('StartPlaybookRun', function() {
LaunchJob({
scope: scope,
url: url,
callback: 'PlaybookLaunchFinished'
});
});
if (scope.removePromptForPasswords) {
scope.removePromptForPasswords();
}
scope.removePromptForPasswords = scope.$on('PromptForPasswords', function(e, passwords_needed_to_start,html, url) {
PromptForPasswords({ scope: scope,
passwords: passwords_needed_to_start,
callback: 'PromptForVars',
html: html,
url: url
});
});
if (scope.removePromptForCredential) {
scope.removePromptForCredential();
}
scope.removePromptForCredential = scope.$on('PromptForCredential', function(e, data) {
PromptForCredential({ scope: scope, template: data });
});
if (scope.removePromptForVars) {
scope.removePromptForVars();
}
scope.removePromptForVars = scope.$on('PromptForVars', function(e, html, url) {
// passwords = pwds;
if (scope.prompt_for_vars) {
// call prompt with callback of StartPlaybookRun, passwords
PromptForVars({
scope: scope,
job: {id:scope.job_template_id},
variables: extra_vars,
callback: 'PromptForSurvey',
html: html,
url: url
});
}
else {
scope.$emit('PromptForSurvey', html, url);
}
});
if (scope.removePromptForSurvey) {
scope.removePromptForSurvey();
}
scope.removePromptForSurvey = scope.$on('PromptForSurvey', function(e, html, url) {
if (scope.survey_enabled) {
// call prompt with callback of StartPlaybookRun, passwords
PromptForSurvey({
scope: scope,
id: scope.job_template_id,
variables: extra_vars,
callback: 'CreateModal',
url: url,
html: html
});
}
else {
scope.$emit('CreateModal', html, url);
// CreateLaunchDialog({scope: scope, html: html, url: url});
}
});
if (scope.removeCreateModal) {
scope.removeCreateModal();
}
scope.removeCreateModal = scope.$on('CreateModal', function(e, html, url) {
CreateLaunchDialog({
scope: scope,
html: html,
url: url,
callback: 'StartPlaybookRun'
});
});
if (scope.removeContinueCred) {
scope.removeContinueCred();
}
scope.removeContinueCred = scope.$on('ContinueCred', function(e, passwords) {
if(passwords.length>0){
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);
}
});
// 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 = '<form class="ng-valid ng-valid-required" name="job_launch_form" id="job_launch_form" autocomplete="off" nonvalidate>';
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) {
.factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait',
function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) {
return function (params) {
var scope = params.scope,
project_id = params.project_id,
@ -1028,10 +266,9 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
}
])
// Submit Inventory Update request
.factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait',
function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) {
.factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait',
function (PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) {
return function (params) {
var scope = params.scope,

View File

@ -561,11 +561,11 @@ export default
};
}])
.factory('RelaunchPlaybook', ['PlaybookRun', function(PlaybookRun) {
.factory('RelaunchPlaybook', ['InitiatePlaybookRun', function(InitiatePlaybookRun) {
return function(params) {
var scope = params.scope,
id = params.id;
PlaybookRun({ scope: scope, id: id });
InitiatePlaybookRun({ scope: scope, id: id });
};
}])

View File

@ -15,7 +15,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
ParseVariableString, RelatedSearchInit, RelatedPaginateInit,
Prompt, PlaybookRun, CreateDialog, deleteJobTemplate, $state) {
Prompt, InitiatePlaybookRun, CreateDialog, deleteJobTemplate, $state) {
ClearScope();
@ -153,7 +153,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
};
$scope.launchScanJob = function(){
PlaybookRun({ scope: $scope, id: this.scan_job_template.id });
InitiatePlaybookRun({ scope: $scope, id: this.scan_job_template.id });
};
$scope.scheduleScanJob = function(){
@ -327,6 +327,6 @@ export default ['$scope', '$rootScope', '$compile', '$location',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString',
'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt',
'PlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state',
'InitiatePlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state',
InventoriesEdit,
];

View File

@ -14,7 +14,7 @@ export default
[ '$location', '$rootScope', '$filter', '$scope', '$compile',
'$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait',
'ProcessErrors', 'SelectPlay', 'SelectTask', 'GetElapsed',
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'InitiatePlaybookRun',
'LoadPlays', 'LoadTasks', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule', 'ParseTypeChange', 'JobDetailService',
@ -24,7 +24,7 @@ export default
SelectPlay, SelectTask, GetElapsed,
JobIsFinished,
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
PlaybookRun, LoadPlays, LoadTasks,
InitiatePlaybookRun, LoadPlays, LoadTasks,
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService
) {
@ -879,7 +879,7 @@ export default
};
scope.relaunchJob = function() {
PlaybookRun({
InitiatePlaybookRun({
scope: scope,
id: scope.job.id
});

View File

@ -0,0 +1,77 @@
export default
function GetSurveyQuestions($filter, GetBasePath, Rest, Empty, ProcessErrors, $stateParams) {
// This factory goes out and gets a job template's survey questions and attaches
// them to scope so that they can be ng-repeated in the job submission template
return function(params) {
var id= params.id,
scope = params.scope,
i,
survey_url = GetBasePath('job_templates') + id + '/survey_spec/';
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; i<scope.survey_questions.length; i++){
var question = scope.survey_questions[i];
question.index = i;
question.question_name = $filter('sanitize')(question.question_name);
question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined;
if(question.type === "textarea" && !Empty(question.default_textarea)) {
question.model = angular.copy(question.default_textarea);
}
else if(question.type === "multiselect") {
question.model = question.default.split(/\n/);
question.choices = question.choices.split(/\n/);
}
else if(question.type === "multiplechoice") {
question.model = question.default ? angular.copy(question.default) : "";
question.choices = question.choices.split(/\n/);
}
else if(question.type === "float"){
question.model = (!Empty(question.default)) ? angular.copy(question.default) : (!Empty(question.default_float)) ? angular.copy(question.default_float) : "";
}
else {
question.model = question.default ? angular.copy(question.default) : "";
}
if(question.type === "text" || question.type === "textarea" || question.type === "password") {
question.minlength = (!Empty(question.min)) ? Number(question.min) : "";
question.maxlength = (!Empty(question.max)) ? Number(question.max) : "" ;
}
else if(question.type === "integer") {
question.minValue = (!Empty(question.min)) ? Number(question.min) : "";
question.maxValue = (!Empty(question.max)) ? Number(question.max) : "" ;
}
else if(question.type === "float") {
question.minValue = (!Empty(question.min)) ? question.min : "";
question.maxValue = (!Empty(question.max)) ? question.max : "" ;
}
}
return;
}
})
.error(function (data, status) {
ProcessErrors(scope, data, status, { hdr: 'Error!',
msg: 'Failed to retrieve organization: ' + $stateParams.id + '. GET status: ' + status });
});
};
}
GetSurveyQuestions.$inject =
[ '$filter',
'GetBasePath',
'Rest' ,
'Empty',
'ProcessErrors',
'$stateParams'
];

View File

@ -0,0 +1,25 @@
export default
function InitiatePlaybookRun($location, GetBasePath, Empty, $compile) {
// This factory drops the submit-job directive into the dom which
// either launches the job (when no user input is needed) or shows
// the user a job sumbission modal with varying steps based on what
// is being prompted/what passwords are needed.
return function (params) {
var scope = params.scope.$new(),
id = params.id,
system_job = params.system_job || false;
scope.job_template_id = id;
var el = $compile( "<submit-job data-submit-job-id=" + id + " data-submit-job-system=" + system_job + "></submit-job>" )( scope );
$('#content-container').remove('submit-job').append( el );
};
}
InitiatePlaybookRun.$inject =
[ '$location',
'GetBasePath',
'Empty',
'$compile'
];

View File

@ -0,0 +1,164 @@
export default
function LaunchJob(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath, $state, $location) {
// This factory gathers up all the job launch data and POST's it.
// TODO: outline how these things are gathered
return function (params) {
var scope = params.scope,
job_launch_data = {},
url = params.url,
vars_url = GetBasePath('job_templates')+scope.job_template_id + '/',
base = $location.path().replace(/^\//, '').split('/')[0],
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 = {};
//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
var buildData = function() {
if(scope.ssh_password_required) {
job_launch_data.ssh_password = scope.passwords.ssh_password;
}
if(scope.ssh_key_unlock_required) {
job_launch_data.ssh_key_unlock = scope.passwords.ssh_key_unlock;
}
if(scope.become_password_required) {
job_launch_data.become_password = scope.passwords.become_password;
}
if(scope.vault_password_required) {
job_launch_data.vault_password = scope.passwords.vault_password;
}
if(scope.ask_variables_on_launch){
extra_vars = ToJSON(scope.parseType, scope.jobLaunchVariables, false);
if(!Empty(extra_vars)){
$.each(extra_vars, function(key,value){
job_launch_data.extra_vars[key] = value;
});
}
}
if(scope.ask_tags_on_launch && scope.other_prompt_data && scope.other_prompt_data.job_tags){
job_launch_data.job_tags = scope.other_prompt_data.job_tags;
}
if(scope.ask_limit_on_launch && scope.other_prompt_data && scope.other_prompt_data.limit){
job_launch_data.limit = scope.other_prompt_data.limit;
}
if(scope.ask_job_type_on_launch && scope.other_prompt_data && scope.other_prompt_data.job_type) {
job_launch_data.job_type = scope.other_prompt_data.job_type;
}
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.survey_questions[i].model.toString()!=="")) {
job_launch_data.extra_vars[fld] = scope.survey_questions[i].model;
}
if(scope.survey_questions[i].required === false && _.isEmpty(scope.survey_questions[i].model)) {
switch (scope.survey_questions[i].type) {
// for optional text and text-areas, submit a blank string if min length is 0
// -- this is confusing, for an explanation see:
// http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions
//
case "text":
case "textarea":
if (scope.survey_questions[i].min === 0) {
job_launch_data.extra_vars[fld] = "";
}
break;
// for optional select lists, if they are left blank make sure we submit
// a value that the API will consider "empty"
//
case "multiplechoice":
job_launch_data.extra_vars[fld] = "";
break;
case "multiselect":
job_launch_data.extra_vars[fld] = [];
break;
}
}
}
}
// include the inventory used if the user was prompted to choose a cred
if(scope.ask_inventory_on_launch && !Empty(scope.selected_inventory)){
job_launch_data.inventory_id = scope.selected_inventory.id;
}
// include the credential used if the user was prompted to choose a cred
if(scope.ask_credential_on_launch && !Empty(scope.selected_credential)){
job_launch_data.credential_id = scope.selected_credential.id;
}
// 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');
var job = data.job || data.system_job;
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) || (base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in
$state.go('jobDetail', {id: job}, {reload: true});
}
scope.clearDialog();
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status });
});
};
//gather the extra vars from the job template if survey is enabled and prompt for vars is false
var 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;
});
}
buildData();
})
.error(function (data, status) {
ProcessErrors(scope, data, status, { hdr: 'Error!',
msg: 'Failed to retrieve job template extra variables.' });
});
};
// 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){
getExtraVars();
}
else {
buildData();
}
};
}
LaunchJob.$inject =
[ 'Rest',
'Wait',
'ProcessErrors',
'ToJSON',
'Empty',
'GetBasePath',
'$state',
'$location'
];

View File

@ -0,0 +1,220 @@
@import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less';
.JobSubmission {
padding: 20px!important;
display: none;
height: auto!important;
min-height: 400px!important;
}
.JobSubmission-container {
flex-direction: column;
display: flex;
height: auto;
min-height: 360px;
}
.JobSubmission-dialog {
padding: 0px;
margin-bottom: 20px;
.ui-dialog-buttonpane, .ui-dialog-titlebar {
display:none;
}
}
.JobSubmission-header {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.JobSubmission-title {
align-items: center;
flex: 1 0 auto;
display: flex;
}
.JobSubmission-titleText {
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.JobSubmission-titleLockup {
margin-left: 4px;
margin-right: 6px;
display: inline-block;
margin-top: 0px;
padding-bottom: 2px;
vertical-align: bottom;
}
.JobSubmission-titleLockup:before {
content: "\007C";
color: @default-icon-hov;
display: block;
font-size: 13px;
}
.JobSubmission-close {
justify-content: flex-end;
display: flex;
}
.JobSubmission-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;
}
.JobSubmission-exit:hover{
color:@default-icon;
}
.JobSubmission-stepsContainer {
display: flex;
flex: 0 0 auto;
margin-top: 25px;
}
.JobSubmission-steps {
display: flex;
margin-bottom: 20px;
min-height: 30px;
}
.JobSubmission-step {
color: @default-interface-txt;
background-color: @default-bg;
font-size: 12px;
border: 1px solid @default-border;
height: 30px;
border-radius: 5px;
margin-right: 20px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
padding-top: 5px;
transition: background-color 0.2s;
text-transform: uppercase;
line-height: 20px;
white-space: nowrap;
}
.JobSubmission-step:hover {
color: @btn-txt;
background-color: @btn-bg-hov;
cursor: pointer;
}
.JobSubmission-step--active {
color: @default-bg!important;
background-color: @d7grey!important;
border-color: @d7grey!important;
cursor: default!important;
}
.JobSubmission-step--disabled {
opacity: 0.4;
cursor: not-allowed!important;
}
.JobSubmission-formContainer {
display: flex;
flex: 1 0 auto;
}
.JobSubmission-form {
display: flex;
flex: 1 0 auto;
max-width: 100%;
flex-direction: column;
}
.JobSubmission-footerContainer {
display: flex;
flex: 0 0 auto;
margin-top: 15px;
}
.JobSubmission-footerPreview {
display: flex;
flex: 1 0 auto;
}
.JobSubmission-footerButtons {
justify-content: flex-end;
display: flex;
}
.JobSubmission-previewItem {
min-width: 150px;
font-weight: normal;
font-size: small;
}
.JobSubmission-previewItemTitle {
color: @default-interface-txt;
}
.JobSubmission-previewItemNone {
color: @default-icon;
}
.JobSubmission-actionButton {
background-color: @submit-button-bg;
color: @submit-button-text;
height: 30px;
padding-left:15px;
padding-right: 15px;
width: 85px;
}
.JobSubmission-actionButton:hover,
.JobSubmission-actionButton:focus {
color: @submit-button-text;
background-color: @submit-button-bg-hov;
}
.JobSubmission-defaultButton{
background-color: @default-bg;
color: @btn-txt;
text-transform: uppercase;
border-radius: 5px;
border: 1px solid @btn-bord;
padding-left:15px;
padding-right: 15px;
height: 30px;
min-width: 85px;
margin-right: 20px;
}
.JobSubmission-defaultButton:hover{
background-color: @btn-bg-hov;
color: @btn-txt;
}
.JobSubmission-revertButton {
background-color: @default-link;
color: @default-bg;
text-transform: uppercase;
padding-left:15px;
padding-right: 15px;
font-size: 9px;
}
.JobSubmission-revertButton:hover{
background-color: @default-link-hov;
color: @default-bg;
}
.JobSubmission-selectedItem {
display: flex;
flex: 1 0 auto;
margin-bottom: 15px;
}
.JobSubmission-selectedItemInfo {
display: flex;
flex: 1 0 auto;
}
.JobSubmission-selectedItemRevert {
display: flex;
flex: 0 0 auto;
}
.JobSubmission-selectedItemLabel {
color: @default-interface-txt;
margin-right: 10px;
}
.JobSubmission-selectedItemNone {
color: @default-icon;
}
.JobSubmission-selectedItemContainer {
display: block;
width: 100%;
}
.JobSubmission-instructions {
color: @default-interface-txt;
margin-top: 25px;
margin-bottom: 15px;
}
.JobSubmission-passwordButton {
padding: 5px 13px!important;
}

View File

@ -0,0 +1,530 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:JobSubmission
* @description This controller's for the Job Submission Modal
* The job-submission directive is intended to handle job launch/relaunch from a playbook. There are 4 potential steps involved in launching a job:
*
* Select an Inventory
* Select a Credential
* Extra prompts (extra vars, limit, job type, job tags)
* Fill in survey
*
* #Workflow when user hits launch button
*
* A 'get' call is made to the API's 'job_templates/:job_template_id/launch' endpoint for that job template. The response from the API will specify
*
*```
* "credential_needed_to_start": true,
* "can_start_without_user_input": false,
* "ask_variables_on_launch": false,
* "passwords_needed_to_start": [],
* "variables_needed_to_start": [],
* "survey_enabled": false
*```
* #Step 1 - Hit the launch/relaunch endpoint
*
* The launch/relaunch endpoint(s) let us know what the default values are for a particular job template. It also lets us know what the creator of
* the job template selected as "promptable" fields.
*
* #Step 2 - Gather inv/credential lists and job template survey questions
*
* If the job template allows for inventory or credential prompting then we need to go out and get the available inventories and credentials for the
* launching user. We also go out and get the survey from its endpoint if a survey has been created and is enabled for this job template (getsurveyquestions.factory).
*
* #Step 3 - Fill out the job launch form
*
* No server calls needed as a user fills out the form. Note that if no user input is required (no prompts, no passwords) then we skip ahead to the next
* step.
*
* #Step 4 - Launch the job: LaunchJob
*
* This is maybe the most crucial step. We have setup everything we need in order to gather information from the user and now we want to be sure
* we handle it correctly. And there are many scenarios to take into account. The first scenario we check for is is ``survey_enabled=true`` and
* ``prompt_for_vars=false``, in which case we want to make sure to include the extra_vars from the job template in the data being
* sent to the API (it is important to note that anything specified in the extra vars on job submission will override vars specified in the job template.
* Likewise, any variables specified in the extra vars that are duplicated by the survey vars, will get overridden by the survey vars).
* If the previous scenario is NOT the case, then we continue to gather the modal's answers regularly: gather the passwords, then the extra_vars, then
* any survey results. Also note that we must gather any required survey answers, as well as any optional survey answers that happened to be provided
* by the user. We also include the credential that was chosen if the user was prompted to select a credential.
* At this point we have all the info we need and we are almost ready to perform a POST to the '/launch' endpoint. We must lastly check
* if the user was not prompted for anything and therefore we don't want to pass any extra_vars to the POST. Once this is done we
* make the REST POST call and provide all the data to hte API. The response from the API will be the job ID, which is used to redirect the user
* to the job detail page for that job run.
*
* @Usage
* This is usage information.
*/
export default
[ '$scope', '$location', 'GetBasePath', 'Empty', 'Wait', 'Rest', 'ProcessErrors',
'LaunchJob', '$state', 'generateList', 'InventoryList', 'SearchInit', 'PaginateInit', 'CredentialList', 'ParseTypeChange', 'GetSurveyQuestions',
function($scope, $location, GetBasePath, Empty, Wait, Rest, ProcessErrors,
LaunchJob, $state, GenerateList, InventoryList, SearchInit, PaginateInit, CredentialList, ParseTypeChange, GetSurveyQuestions) {
var launch_url;
var clearRequiredPasswords = function() {
$scope.ssh_password_required = false;
$scope.ssh_key_unlock_required = false;
$scope.become_password_required = false;
$scope.vault_password_required = false;
$scope.ssh_password = "";
$scope.ssh_key_unlock = "";
$scope.become_password = "";
$scope.vault_password = "";
};
var updateRequiredPasswords = function() {
if($scope.selected_credential) {
if($scope.selected_credential.id === $scope.defaults.credential.id) {
clearRequiredPasswords();
for(var i=0; i<$scope.passwords_needed_to_start.length; i++) {
var password = $scope.passwords_needed_to_start[i];
switch(password) {
case "ssh_password":
$scope.ssh_password_required = true;
break;
case "ssh_key_unlock":
$scope.ssh_key_unlock_required = true;
break;
case "become_password":
$scope.become_password_required = true;
break;
case "vault_password":
$scope.vault_password_required = true;
break;
}
}
}
else {
if($scope.selected_credential.kind === "ssh"){
$scope.ssh_password_required = ($scope.selected_credential.password === "ASK") ? true : false;
$scope.ssh_key_unlock_required = ($scope.selected_credential.ssh_key_unlock === "ASK") ? true : false;
$scope.become_password_required = ($scope.selected_credential.become_password === "ASK") ? true : false;
$scope.vault_password_required = ($scope.selected_credential.vault_password === "ASK") ? true : false;
}
else {
clearRequiredPasswords();
}
}
}
};
var launchJob = function() {
LaunchJob({
scope: $scope,
url: launch_url
});
};
// This gets things started - goes out and hits the launch endpoint (based on launch/relaunch) and
// prepares the form fields, defauts, etc.
$scope.init = function() {
$scope.forms = {};
$scope.passwords = {};
var base = $location.path().replace(/^\//, '').split('/')[0],
isRelaunch = !(base === 'job_templates' || base === 'portal' || base === 'inventories' || base === 'home');
if (!isRelaunch) {
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
}
else {
launch_url = GetBasePath('jobs') + $scope.submitJobId + '/relaunch/';
}
// Get the job or job_template record
Wait('start');
Rest.setUrl(launch_url);
Rest.get()
.success(function (data) {
// Put all the data that we get back about the launch onto scope
angular.extend($scope, data);
// General catch-all for "other prompts" - used in this link function and to hide the Other Prompts tab when
// it should be hidden
$scope.has_other_prompts = (data.ask_job_type_on_launch || data.ask_limit_on_launch || data.ask_tags_on_launch || data.ask_variables_on_launch) ? true : false;
$scope.password_needed = data.passwords_needed_to_start && data.passwords_needed_to_start.length > 0;
$scope.has_default_inventory = data.defaults && data.defaults.inventory && data.defaults.inventory.id;
$scope.has_default_credential = data.defaults && data.defaults.credential && data.defaults.credential.id;
$scope.other_prompt_data = {};
if($scope.ask_job_type_on_launch) {
$scope.other_prompt_data.job_type = (data.defaults && data.defaults.job_type) ? data.defaults.job_type : "";
}
if($scope.ask_limit_on_launch) {
$scope.other_prompt_data.limit = (data.defaults && data.defaults.limit) ? data.defaults.limit : "";
}
if($scope.ask_tags_on_launch) {
$scope.other_prompt_data.job_tags = (data.defaults && data.defaults.job_tags) ? data.defaults.job_tags : "";
}
if($scope.ask_variables_on_launch) {
$scope.jobLaunchVariables = (data.defaults && data.defaults.extra_vars) ? data.defaults.extra_vars : "---";
$scope.other_prompt_data.parseType = 'yaml';
$scope.parseType = 'yaml';
}
if($scope.has_default_inventory) {
$scope.selected_inventory = angular.copy($scope.defaults.inventory);
}
if($scope.has_default_credential) {
$scope.selected_credential = angular.copy($scope.defaults.credential);
updateRequiredPasswords();
}
if($scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts) {
// The job can be launched without any user input
launchJob();
Wait('stop');
}
else {
var initiateModal = function() {
// Figure out which step the user needs to start on
if($scope.ask_inventory_on_launch) {
$scope.setStep("inventory", true);
}
else if($scope.ask_credential_on_launch || $scope.password_needed) {
$scope.setStep("credential", true);
}
else if($scope.has_other_prompts) {
$scope.setStep("otherprompts", true);
}
else if($scope.survey_enabled) {
$scope.setStep("survey", true);
}
$scope.openLaunchModal();
};
if(isRelaunch) {
// Go out and get some of the job details like inv, cred, name
Rest.setUrl(GetBasePath('jobs') + $scope.submitJobId);
Rest.get()
.success(function (jobDetailData) {
$scope.job_template_data = {
name: jobDetailData.name
};
$scope.defaults = {};
if(jobDetailData.summary_fields.inventory) {
$scope.defaults.inventory = angular.copy(jobDetailData.summary_fields.inventory);
$scope.selected_inventory = angular.copy(jobDetailData.summary_fields.inventory);
}
if(jobDetailData.summary_fields.credential) {
$scope.defaults.credential = angular.copy(jobDetailData.summary_fields.credential);
$scope.selected_credential = angular.copy(jobDetailData.summary_fields.credential);
updateRequiredPasswords();
}
initiateModal();
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get job details. GET returned status: ' + status });
});
}
else {
// Move forward with the modal
initiateModal();
}
}
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get job template details. GET returned status: ' + status });
});
};
$scope.setStep = function(step, initialStep) {
$scope.step = step;
if(step === "credential") {
$scope.credentialTabEnabled = true;
}
else if(step === "otherprompts") {
$scope.otherPromptsTabEnabled = true;
if(!initialStep && $scope.step === 'otherprompts' && $scope.ask_variables_on_launch && !$scope.extra_vars_code_mirror_loaded) {
ParseTypeChange({
scope: $scope,
variable: 'jobLaunchVariables',
field_id: 'job_launch_variables'
});
$scope.extra_vars_code_mirror_loaded = true;
}
}
else if(step === "survey") {
$scope.surveyTabEnabled = true;
}
};
$scope.getListsAndSurvey = function() {
if($scope.ask_inventory_on_launch) {
var inventory_url = GetBasePath('inventory');
GenerateList.inject(InventoryList, {
mode: 'lookup',
id: 'job-submission-inventory-lookup',
scope: $scope,
input_type: 'radio'
});
SearchInit({
scope: $scope,
set: InventoryList.name,
list: InventoryList,
url: inventory_url
});
PaginateInit({
scope: $scope,
list: InventoryList,
url: inventory_url,
mode: 'lookup'
});
$scope.search(InventoryList.iterator);
$scope.$watchCollection('inventories', function () {
if($scope.selected_inventory) {
// Loop across the inventories and see if one of them should be "checked"
$scope.inventories.forEach(function(row, i) {
if (row.id === $scope.selected_inventory.id) {
$scope.inventories[i].checked = 1;
}
else {
$scope.inventories[i].checked = 0;
}
});
}
});
}
if($scope.ask_credential_on_launch) {
var credential_url = GetBasePath('credentials') + '?kind=ssh';
GenerateList.inject(CredentialList, {
mode: 'lookup',
id: 'job-submission-credential-lookup',
scope: $scope,
input_type: 'radio'
});
SearchInit({
scope: $scope,
set: CredentialList.name,
list: CredentialList,
url: credential_url
});
PaginateInit({
scope: $scope,
list: CredentialList,
url: credential_url,
mode: 'lookup'
});
$scope.search(CredentialList.iterator);
$scope.$watchCollection('credentials', function () {
if($scope.selected_credential) {
// Loop across the inventories and see if one of them should be "checked"
$scope.credentials.forEach(function(row, i) {
if (row.id === $scope.selected_credential.id) {
$scope.credentials[i].checked = 1;
}
else {
$scope.credentials[i].checked = 0;
}
});
}
});
}
if($scope.survey_enabled) {
GetSurveyQuestions({
scope: $scope,
id: $scope.submitJobId
});
}
};
$scope.revertToDefaultInventory = function() {
if($scope.has_default_inventory) {
$scope.selected_inventory = angular.copy($scope.defaults.inventory);
// Loop across inventories and set update the "checked" attribute for each row
$scope.inventories.forEach(function(row, i) {
if (row.id === $scope.selected_inventory.id) {
$scope.inventories[i].checked = 1;
} else {
$scope.inventories[i].checked = 0;
}
});
}
};
$scope.revertToDefaultCredential = function() {
if($scope.has_default_credential) {
$scope.selected_credential = angular.copy($scope.defaults.credential);
updateRequiredPasswords();
// Loop across credentials and set update the "checked" attribute for each row
$scope.credentials.forEach(function(row, i) {
if (row.id === $scope.selected_credential.id) {
$scope.credentials[i].checked = 1;
} else {
$scope.credentials[i].checked = 0;
}
});
}
};
$scope.toggle_inventory = function(id) {
$scope.inventories.forEach(function(row, i) {
if (row.id === id) {
$scope.selected_inventory = angular.copy(row);
$scope.inventories[i].checked = 1;
} else {
$scope.inventories[i].checked = 0;
}
});
};
$scope.toggle_credential = function(id) {
$scope.credentials.forEach(function(row, i) {
if (row.id === id) {
$scope.selected_credential = angular.copy(row);
updateRequiredPasswords();
$scope.credentials[i].checked = 1;
} else {
$scope.credentials[i].checked = 0;
}
});
};
$scope.getActionButtonText = function() {
if($scope.step === "inventory") {
return ($scope.ask_credential_on_launch || $scope.password_needed || $scope.has_other_prompts || $scope.survey_enabled) ? "NEXT" : "LAUNCH";
}
else if($scope.step === "credential") {
return ($scope.has_other_prompts || $scope.survey_enabled) ? "NEXT" : "LAUNCH";
}
else if($scope.step === "otherprompts") {
return ($scope.survey_enabled) ? "NEXT" : "LAUNCH";
}
else if($scope.step === "survey") {
return "LAUNCH";
}
};
$scope.actionButtonDisabled = function() {
if($scope.step === "inventory") {
if($scope.selected_inventory) {
return false;
}
else {
$scope.credentialTabEnabled = false;
$scope.otherPromptsTabEnabled = false;
$scope.surveyTabEnabled = false;
return true;
}
}
else if($scope.step === "credential") {
if($scope.selected_credential && $scope.forms.credentialpasswords.$valid) {
return false;
}
else {
$scope.otherPromptsTabEnabled = false;
$scope.surveyTabEnabled = false;
return true;
}
}
else if($scope.step === "otherprompts") {
if($scope.forms.otherprompts.$valid) {
return false;
}
else {
$scope.surveyTabEnabled = false;
return true;
}
}
else if($scope.step === "survey") {
if($scope.forms.survey.$valid) {
return false;
}
else {
return true;
}
}
};
$scope.takeAction = function() {
if($scope.step === "inventory") {
// Check to see if there's another step after this one
if($scope.ask_credential_on_launch || $scope.password_needed) {
$scope.setStep("credential");
}
else if($scope.has_other_prompts) {
$scope.setStep("otherprompts");
}
else if($scope.survey_enabled) {
$scope.setStep("survey");
}
else {
launchJob();
}
}
else if($scope.step === "credential") {
if($scope.has_other_prompts) {
$scope.setStep("otherprompts");
}
else if($scope.survey_enabled) {
$scope.setStep("survey");
}
else {
launchJob();
}
}
else if($scope.step === "otherprompts") {
if($scope.survey_enabled) {
$scope.setStep("survey");
}
else {
launchJob();
}
}
else {
launchJob();
}
};
$scope.updateParseType = function() {
// This is what the ParseTypeChange factory is expecting
// It shares the same scope with this directive and will
// pull the new value of parseType out to determine which
// direction to convert the extra vars
$scope.parseType = $scope.other_prompt_data.parseType;
$scope.parseTypeChange();
};
}
];

View File

@ -0,0 +1,95 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import jobSubmissionController from './job-submission.controller';
export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2', 'ParseTypeChange',
function(templateUrl, CreateDialog, Wait, CreateSelect2, ParseTypeChange) {
return {
scope: {
submitJobId: '=',
submitJobSystem: '='
},
templateUrl: templateUrl('job-submission/job-submission'),
controller: jobSubmissionController,
restrict: 'E',
link: function(scope) {
scope.openLaunchModal = function() {
if (scope.removeLaunchJobModalReady) {
scope.removeLaunchJobModalReady();
}
scope.removeLaunchJobModalReady = scope.$on('LaunchJobModalReady', function() {
// Go get the list/survey data that we need from the server
scope.getListsAndSurvey();
$('#job-launch-modal').dialog('open');
// select2-ify the job type dropdown
CreateSelect2({
element: '#job_launch_job_type',
multiple: false
});
if(scope.step === 'otherprompts' && scope.ask_variables_on_launch) {
ParseTypeChange({
scope: scope,
variable: 'jobLaunchVariables',
field_id: 'job_launch_variables'
});
scope.extra_vars_code_mirror_loaded = true;
}
});
CreateDialog({
id: 'job-launch-modal',
scope: scope,
width: 800,
minWidth: 400,
draggable: false,
dialogClass: 'JobSubmission-dialog',
onOpen: function() {
Wait('stop');
},
callback: 'LaunchJobModalReady'
});
};
scope.clearDialog = function() {
// Destroy the dialog
if($("#job-launch-modal").hasClass('ui-dialog-content')) {
$('#job-launch-modal').dialog('destroy');
}
// Remove the directive from the page
$('#content-container').find('submit-job').remove();
// Clear out the scope (we'll create a new scope the next time
// job launch is called)
scope.$destroy();
};
// This function is used to hide/show the contents of a password
// within a form
scope.togglePassword = function(id) {
var buttonId = id + "_show_input_button",
inputId = id,
buttonInnerHTML = $(buttonId).html();
if (buttonInnerHTML.indexOf("Show") > -1) {
$(buttonId).html("Hide");
$(inputId).attr("type", "text");
} else {
$(buttonId).html("Show");
$(inputId).attr("type", "password");
}
};
scope.init();
}
};
}];

View File

@ -0,0 +1,241 @@
<div id="job-launch-modal" class="JobSubmission">
<div class="JobSubmission-container">
<div class="JobSubmission-header">
<div class="JobSubmission-title">
<div class="JobSubmission-titleText">LAUNCH JOB<div class="JobSubmission-titleLockup"></div>{{job_template_data.name}}</div>
</div>
<div class="JobSubmission-close">
<button class="JobSubmission-exit" ng-click="clearDialog()"><i class="fa fa-times-circle"></i></button>
</div>
</div>
<div class="JobSubmission-stepsContainer">
<div class="JobSubmission-steps">
<div class="JobSubmission-step" ng-class="{'JobSubmission-step--active': step==='inventory'}" ng-if="ask_inventory_on_launch" ng-click="setStep('inventory')">Inventory</div>
<div class="JobSubmission-step" ng-class="{'JobSubmission-step--active': step==='credential', 'JobSubmission-step--disabled': !credentialTabEnabled}" ng-if="ask_credential_on_launch || password_needed" ng-click="!credentialTabEnabled || setStep('credential')">Credential</div>
<div class="JobSubmission-step" ng-class="{'JobSubmission-step--active': step==='otherprompts', 'JobSubmission-step--disabled': !otherPromptsTabEnabled}" ng-if="has_other_prompts" ng-click="!otherPromptsTabEnabled || setStep('otherprompts')">Other Prompts</div>
<div class="JobSubmission-step" ng-class="{'JobSubmission-step--active': step==='survey', 'JobSubmission-step--disabled': !surveyTabEnabled}" ng-if="survey_enabled" ng-click="!surveyTabEnabled || setStep('survey')">Survey</div>
</div>
</div>
<div class="JobSubmission-formContainer">
<div ng-if="ask_inventory_on_launch" ng-show="step === 'inventory'" class="JobSubmission-form">
<div class="JobSubmission-selectedItemContainer">
<div class="JobSubmission-selectedItem">
<div class="JobSubmission-selectedItemInfo">
<span class="JobSubmission-selectedItemLabel">SELECTED INVENTORY:</span>
<span ng-show="selected_inventory" ng-bind="selected_inventory.name"></span>
<span class="JobSubmission-selectedItemNone" ng-show="!selected_inventory">None selected</span>
</div>
<div class="JobSubmission-selectedItemRevert" ng-if="ask_inventory_on_launch && has_default_inventory">
<button class="btn btn-xs JobSubmission-revertButton" ng-disabled="selected_inventory.id === defaults.inventory.id" ng-click="revertToDefaultInventory()">REVERT TO DEFAULT</button>
</div>
</div>
<div id="job-submission-inventory-lookup"></div>
</div>
</div>
<div ng-if="ask_credential_on_launch || password_needed" ng-show="step === 'credential'" class="JobSubmission-form">
<div class="JobSubmission-selectedItemContainer">
<div class="JobSubmission-selectedItem">
<div class="JobSubmission-selectedItemInfo">
<span class="JobSubmission-selectedItemLabel">SELECTED CREDENTIAL:</span>
<span ng-show="selected_inventory" ng-bind="selected_credential.name"></span>
<span class="JobSubmission-selectedItemNone" ng-show="!selected_credential">None selected</span>
</div>
<div class="JobSubmission-selectedItemRevert" ng-if="ask_credential_on_launch && has_default_credential">
<button class="btn btn-xs JobSubmission-revertButton" ng-disabled="selected_credential.id === defaults.credential.id" ng-click="revertToDefaultCredential()">REVERT TO DEFAULT</button>
</div>
</div>
<div id="job-submission-credential-lookup"></div>
<div ng-show="ssh_password_required || ssh_key_unlock_required || become_password_required || vault_password_required">
<div class="JobSubmission-instructions">Launching this job requires the passwords listed below. Enter and confirm each password before continuing.</div>
<form name="forms.credentialpasswords" autocomplete="off" novalidate>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ssh_password_required">
<label for="ssh_password">
<span class="Form-inputLabel prepend-asterisk"> Password</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default show_input_button JobSubmission-passwordButton" id="job-submission-ssh-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#job-submission-ssh-password')" data-original-title="" title="">Show</button>
</span>
<input id="job-submission-ssh-password" type="password" ng-model="passwords.ssh_password" ng-keydown="keydown($event)" name="ssh_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="forms.credentialpasswords.ssh_password.$dirty && forms.credentialpasswords.ssh_password.$error.required">Please enter a password.</div>
<div class="error api-error" ng-bind="ssh_password_api_error"></div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ssh_key_unlock_required">
<label for="ssh_key_unlock">
<span class="Form-inputLabel prepend-asterisk"> Private Key Passphrase</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default show_input_button JobSubmission-passwordButton" id="job-submission-ssh-key-unlock_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#job-submission-ssh-key-unlock')" data-original-title="" title="">Show</button>
</span>
<input id="job-submission-ssh-key-unlock" type="password" ng-model="passwords.ssh_key_unlock" ng-keydown="keydown($event)" name="ssh_key_unlock" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="forms.credentialpasswords.ssh_key_unlock.$dirty && forms.credentialpasswords.ssh_key_unlock.$error.required">Please enter a password.</div>
<div class="error api-error" ng-bind="ssh_key_unlock_api_error"></div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="become_password_required">
<label for="become_password">
<span class="Form-inputLabel prepend-asterisk"> Privilege Escalation Password</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default show_input_button JobSubmission-passwordButton" id="job-submission-become-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#job-submission-become-password')" data-original-title="" title="">Show</button>
</span>
<input id="job-submission-become-password" type="password" ng-model="passwords.become_password" ng-keydown="keydown($event)" name="become_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="forms.credentialpasswords.become_password.$dirty && forms.credentialpasswords.become_password.$error.required">Please enter a password.</div>
<div class="error api-error" ng-bind="become_password_api_error"></div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="vault_password_required">
<label for="vault_password">
<span class="Form-inputLabel prepend-asterisk"> Vault Password</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default show_input_button JobSubmission-passwordButton" id="job-submission-vault-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#job-submission-vault-password')" data-original-title="" title="">Show</button>
</span>
<input id="job-submission-vault-password" type="password" ng-model="passwords.vault_password" ng-keydown="keydown($event)" name="vault_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="forms.credentialpasswords.vault_password.$dirty && forms.credentialpasswords.vault_password.$error.required">Please enter a password.</div>
<div class="error api-error" ng-bind="vault_password_api_error"></div>
</div>
</form>
</div>
</div>
</div>
<div ng-if="has_other_prompts" ng-show="step === 'otherprompts'" class="JobSubmission-form">
<form name="forms.otherprompts" autocomplete="off" novalidate>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ask_variables_on_launch">
<label for="variables">
<span class="Form-inputLabel">Extra Variables</span>
<a id="awp-variables" href="" aw-pop-over="<p>Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.</p>JSON:<br /><blockquote>{<br />&quot;somevar&quot;: &quot;somevalue&quot;,<br />&quot;password&quot;: &quot;magic&quot;<br /> }</blockquote>YAML:<br /><blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>" data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
<i class="fa fa-question-circle"></i>
</a>
<div class="parse-selection" id="job_launch_variables_parse_type">
<input type="radio" ng-model="other_prompt_data.parseType" value="yaml" ng-change="updateParseType()">
<span class="parse-label">YAML</span>
<input type="radio" ng-model="other_prompt_data.parseType" value="json" ng-change="updateParseType()">
<span class="parse-label">JSON</span>
</div>
</label>
<div>
<textarea rows="6" ng-model="other_prompt_data.extra_vars" name="variables" class="form-control Form-textArea Form-textAreaLabel" id="job_launch_variables" aw-watch=""></textarea>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ask_job_type_on_launch">
<label for="job_type">
<span class="Form-inputLabel prepend-asterisk"> Job Type</span>
</label>
<div>
<select ng-model="other_prompt_data.job_type" name="job_type" class="form-control Form-dropDown" id="job_launch_job_type" tabindex="-1" aria-hidden="true" required>
<option value="" class="" selected="selected">Choose a job type</option>
<option label="Run" value="run" selected="selected">Run</option>
<option label="Check" value="check">Check</option>
</select>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ask_limit_on_launch">
<label for="limit">
<span class="Form-inputLabel">Limit</span>
</label>
<div>
<input type="text" ng-model="other_prompt_data.limit" name="limit" class="form-control Form-textInput">
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="ask_tags_on_launch">
<label for="tags">
<span class="Form-inputLabel">Job Tags</span>
</label>
<div>
<textarea rows="1" ng-model="other_prompt_data.job_tags" name="tags" class="form-control Form-textArea Form-textInput"></textarea>
</div>
</div>
</form>
</div>
<div ng-if="survey_enabled" ng-show="step === 'survey'" class="JobSubmission-form">
<form name="forms.survey" autocomplete="off" novalidate>
<div ng-repeat="question in survey_questions" id="taker_'+question.index+'" class="form-group Form-formGroup Form-formGroup--singleColumn">
<label ng-attr-for="{{question.variable}}">
<span class="label-text Form-inputLabel" ng-class="{'prepend-asterisk': question.required===true}"> {{question.question_name}}</span>
</label>
<div class="survey_taker_description" ng-if="question.question_description">
<i ng-bind="question.question_description"></i>
</div>
<div ng-if="question.type === 'text'">
<input type="text" id="survey_question_{{$index}}" ng-model="question.model" name="survey_question_{{$index}}" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" ng-required="question.required">
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength">Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.</div>
</div>
<div ng-if="question.type === 'textarea'">
<textarea id="survey_question_{{$index}}" name="survey_question_{{$index}}" ng-model="question.model" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control final Form-textArea" ng-required="question.required" rows="3"></textarea>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength">Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.</div>
</div>
<div ng-if="question.type === 'password'">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default show_input_button JobSubmission-passwordButton" id="survey_question_{{$index}}_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="togglePassword('#survey_question_' + $index)" data-original-title="" title="">Show</button>
</span>
<input id="survey_question_{{$index}}" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" autocomplete="false">
</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength">Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.</div>
</div>
<div ng-if="question.type === 'integer'">
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" integer aw-min="question.minValue" aw-max="question.maxValue"/>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.number || forms.survey.survey_question_{{$index}}.$error.integer" >Please enter an answer that is a valid integer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awMin || forms.survey.survey_question_{{$index}}.$error.awMax">Please enter an answer between {{question.minValue}} and {{question.maxValue}}.</div>
</div>
<div ng-if="question.type === 'float'">
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" smart-float aw-min="question.minValue" aw-max="question.maxValue"/>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">Please enter an answer.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.number || forms.survey.survey_question_{{$index}}.$error.float">Please enter an answer that is a decimal number.</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awMin || forms.survey.survey_question_{{$index}}.$error.awMax">Please enter an answer between {{question.minValue}} and {{question.maxValue}}.</div>
</div>
<div ng-if="question.type === 'multiplechoice'">
<div class="survey_taker_input">
<multiple-choice
multi-select="false"
question="question"
choices="question.choices"
ng-required="question.required"
ng-model="question.model">
</multiple-choice>
</div>
</div>
<div ng-if="question.type === 'multiselect'">
<multiple-choice
multi-select="true"
question="question"
choices="question.choices"
ng-required="question.required"
ng-model="question.model">
</multiple-choice>
</div>
</div>
</form>
</div>
</div>
<div class="JobSubmission-footerContainer">
<div class="JobSubmission-footerPreview">
<div class="JobSubmission-previewItem">
<div class="JobSubmission-previewItemTitle">INVENTORY</div>
<div ng-show="selected_inventory" ng-bind="selected_inventory.name"></div>
<div class="JobSubmission-previewItemNone" ng-show="!selected_inventory">None selected</div>
</div>
<div class="JobSubmission-previewItem">
<div class="JobSubmission-previewItemTitle">CREDENTIAL</div>
<div ng-show="selected_credential" ng-bind="selected_credential.name"></div>
<div class="JobSubmission-previewItemNone" ng-show="!selected_credential">None selected</div>
</div>
</div>
<div class="JobSubmission-footerButtons">
<button id="job-submission-close-button" class="btn btn-sm JobSubmission-defaultButton" ng-click="clearDialog()">CANCEL</button>
<button id="job-submission-action-button" class="btn btn-sm JobSubmission-actionButton" ng-click="takeAction()" ng-disabled="actionButtonDisabled()">{{getActionButtonText()}}</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import InitiatePlaybookRun from './job-submission-factories/initiateplaybookrun.factory';
import LaunchJob from './job-submission-factories/launchjob.factory';
import GetSurveyQuestions from './job-submission-factories/getsurveyquestions.factory';
import submitJob from './job-submission.directive';
export default
angular.module('jobSubmission', [])
.factory('InitiatePlaybookRun', InitiatePlaybookRun)
.factory('LaunchJob', LaunchJob)
.factory('GetSurveyQuestions', GetSurveyQuestions)
.directive('submitJob', submitJob);

View File

@ -20,7 +20,7 @@ export default
'Empty', 'Prompt', 'ParseVariableString', 'ToJSON',
'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate',
'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit',
'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2',
'InitiatePlaybookRun' , 'initSurvey', '$state', 'CreateSelect2',
'ToggleNotification', 'NotificationsListInit', '$q',
function(
$filter, $scope, $rootScope, $compile,
@ -30,7 +30,7 @@ export default
GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait,
Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit,
JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit,
SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state,
SchedulesList, CallbackHelpInit, InitiatePlaybookRun, SurveyControllerInit, $state,
CreateSelect2, ToggleNotification, NotificationsListInit, $q
) {
@ -653,7 +653,7 @@ export default
}
else {
PlaybookRun({
InitiatePlaybookRun({
scope: $scope,
id: id
});

View File

@ -9,14 +9,14 @@ export default
'$stateParams', 'Rest', 'Alert', 'JobTemplateList', 'generateList',
'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope',
'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList',
'LookUpInit', 'PlaybookRun', 'Wait', '$compile',
'LookUpInit', 'InitiatePlaybookRun', 'Wait', '$compile',
'$state',
function(
$scope, $rootScope, $location, $log,
$stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt,
SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors,
GetBasePath, JobTemplateForm, CredentialList, LookUpInit, PlaybookRun,
GetBasePath, JobTemplateForm, CredentialList, LookUpInit, InitiatePlaybookRun,
Wait, $compile, $state
) {
@ -95,7 +95,7 @@ export default
};
$scope.submitJob = function (id) {
PlaybookRun({ scope: $scope, id: id });
InitiatePlaybookRun({ scope: $scope, id: id });
};
$scope.scheduleJob = function (id) {

View File

@ -102,7 +102,6 @@
text-transform: uppercase;
padding-left:15px;
padding-right: 15px;
margin-left: 20px;
}
.SurveyMaker-deleteButton:hover {
background-color: @default-err-hov;

View File

@ -26,7 +26,7 @@ export default
key: true,
label: 'Name',
columnClass: 'col-md-3 col-sm-9 col-xs-9',
modalColumnClass: 'col-md-8'
modalColumnClass: 'col-md-11'
},
description: {
label: 'Description',

View File

@ -46,7 +46,7 @@ export default
key: true,
label: 'Name',
columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent',
modalColumnClass: 'col-md-8',
modalColumnClass: 'col-md-11',
linkTo: '/#/inventories/{{inventory.id}}/manage'
},
organization: {

View File

@ -14,8 +14,8 @@
<div class="Prompt-bodyTarget">{{survey_questions[questionToBeDeleted].question_name}}</div>
</div>
<div class="Modal-footer">
<button ng-click="deleteMode === 'survey' ? deleteSurvey() : (deleteMode === 'question' ? deleteQuestion(questionToBeDeleted) : '')" class="btn Modal-footerButton ng-binding Modal-errorButton">DELETE</a>
<button ng-click="hideDeleteOverlay()" class="btn Modal-defaultButton Modal-footerButton">CANCEL</a>
<button ng-click="deleteMode === 'survey' ? deleteSurvey() : (deleteMode === 'question' ? deleteQuestion(questionToBeDeleted) : '')" class="btn Modal-footerButton ng-binding Modal-errorButton">DELETE</a>
</div>
</div>
</div>
@ -81,9 +81,9 @@
</div>
<div class="SurveyMaker-panelFooter">
<div class="Form-buttons">
<button id="survey-save-button" class="btn btn-sm Form-saveButton" ng-click="saveSurvey()" ng-disabled="survey_questions.length < 1 || !can_edit || editQuestionIndex !== null">SAVE</button>
<button id="survey-close-button" class="btn btn-sm Form-buttonDefault" ng-click="closeSurvey('survey-modal-dialog')">CANCEL</button>
<button id="survey-delete-button" class="btn btn-sm SurveyMaker-deleteButton" ng-show="survey_exists" ng-click="showDeleteOverlay('survey')">DELETE SURVEY</button>
<button id="survey-close-button" class="btn btn-sm Form-buttonDefault" ng-click="closeSurvey('survey-modal-dialog')">CANCEL</button>
<button id="survey-save-button" class="btn btn-sm Form-saveButton" ng-click="saveSurvey()" ng-disabled="survey_questions.length < 1 || !can_edit || editQuestionIndex !== null">SAVE</button>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
* All Rights Reserved
*************************************************/
export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePath, GenerateList, PortalJobTemplateList, SearchInit, PaginateInit, PlaybookRun){
export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePath, GenerateList, PortalJobTemplateList, SearchInit, PaginateInit, InitiatePlaybookRun){
var list = PortalJobTemplateList,
@ -13,7 +13,7 @@ export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePat
pageSize = 12;
$scope.submitJob = function (id) {
PlaybookRun({ scope: $scope, id: id });
InitiatePlaybookRun({ scope: $scope, id: id });
};
var init = function(){
@ -42,5 +42,5 @@ export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePat
init();
}
PortalModeJobTemplatesController.$inject = ['$scope', '$rootScope', 'GetBasePath', 'generateList', 'PortalJobTemplateList', 'SearchInit', 'PaginateInit', 'PlaybookRun'
PortalModeJobTemplatesController.$inject = ['$scope', '$rootScope', 'GetBasePath', 'generateList', 'PortalJobTemplateList', 'SearchInit', 'PaginateInit', 'InitiatePlaybookRun'
];

View File

@ -454,14 +454,14 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
// Change layout if a lookup list, place radio buttons before labels
if (options.mode === 'lookup') {
if(options.input_type==="radio"){ //added by JT so that lookup forms can be either radio inputs or check box inputs
innerTable += "<td class=\"List-tableCell\"><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
innerTable += "<td class=\"List-tableCell\"><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_" + list.iterator + "_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
"ng-false-value=\"0\" id=\"check_" + list.iterator + "_{{" + list.iterator + ".id}}\" /></td>";
}
else { // its assumed that options.input_type = checkbox
innerTable += "<td class=\"List-tableCell\"><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
innerTable += "<td class=\"List-tableCell\"><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_" + list.iterator + "_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
"ng-false-value=\"0\" id=\"check_" + list.iterator + "_{{" + list.iterator + ".id}}\" /></td>";
}
}
@ -619,7 +619,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
html += buildSelectAll().prop('outerHTML');
}
else if (options.mode === 'lookup') {
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\"></th>";
html += "<th class=\"List-tableHeader List-staticColumn--smallStatus col-lg-1 col-md-1 col-sm-2 col-xs-2\"></th>";
}
for (fld in list.fields) {
if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) &&

View File

@ -218,8 +218,6 @@
</form>
</div>
<div class="overlay"></div>
<div class="spinny"><i class="fa fa-cog fa-spin fa-2x"></i> <p>working...</p></div>
</div>