Job launch ui overhaul

This commit is contained in:
Michael Abashian
2016-05-17 15:48:31 -04:00
parent 9ba98d9cac
commit d1e6a3323d
27 changed files with 1393 additions and 820 deletions

View File

@@ -439,11 +439,12 @@ input[type='radio']:checked:before {
background-color: @default-bg; background-color: @default-bg;
color: @default-interface-txt; color: @default-interface-txt;
border-color: @default-border; border-color: @default-border;
margin-left: 20px;
} }
.Form-saveButton{ .Form-saveButton{
background-color: @submit-button-bg; background-color: @submit-button-bg;
margin-right: 20px; margin-left: 20px;
color: @submit-button-text; color: @submit-button-text;
text-transform: uppercase; text-transform: uppercase;
transition: background-color 0.2s; transition: background-color 0.2s;
@@ -469,6 +470,7 @@ input[type='radio']:checked:before {
transition: background-color 0.2s; transition: background-color 0.2s;
padding-left:15px; padding-left:15px;
padding-right: 15px; padding-right: 15px;
margin-left: 20px;
} }
.Form-cancelButton:hover{ .Form-cancelButton:hover{
@@ -493,6 +495,7 @@ input[type='radio']:checked:before {
.Form-formGroup--singleColumn { .Form-formGroup--singleColumn {
width: 100% !important; width: 100% !important;
padding-right: 0px; padding-right: 0px;
max-width: 100% !important;
} }
.Form-subCheckbox { .Form-subCheckbox {

View File

@@ -161,6 +161,8 @@ function adhocController($q, $scope, $rootScope, $location, $stateParams,
privateFn.initializeFields(urls.machineCredentialUrl, urls.adhocUrl); privateFn.initializeFields(urls.machineCredentialUrl, urls.adhocUrl);
}; };
var urls = privateFn.setAvailableUrls();
privateFn.initializeForm(id, urls, hostPattern); privateFn.initializeForm(id, urls, hostPattern);
$scope.formCancel = function(){ $scope.formCancel = function(){

View File

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

View File

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

View File

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

View File

@@ -1,306 +1,17 @@
/************************************************* /*************************************************
* Copyright (c) 2015 Ansible, Inc. * Copyright (c) 2016 Ansible, Inc.
* *
* All Rights Reserved * 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'; 'use strict';
export default export default
angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition',
'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition'])
.factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', 'ToJSON', 'Empty', 'GetBasePath', .factory('CreateLaunchDialog', ['$compile', 'CreateDialog', 'Wait', 'ParseTypeChange',
function(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath) { function($compile, CreateDialog, Wait, ParseTypeChange) {
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) {
return function(params) { return function(params) {
var buttons, var buttons,
scope = params.scope, scope = params.scope,
@@ -363,43 +74,25 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
$('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' );
e = angular.element(document.getElementById('password-accept-button')); e = angular.element(document.getElementById('password-accept-button'));
$compile(e)(scope); $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', ['CredentialForm',
function(CredentialForm) {
.factory('PromptForPasswords', ['$compile', 'Wait', 'Alert', 'CredentialForm',
function($compile, Wait, Alert, CredentialForm) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
callback = params.callback || 'PasswordsAccepted', callback = params.callback || 'PasswordsAccepted',
url = params.url, url = params.url,
form = CredentialForm, form = CredentialForm,
// acceptedPasswords = {},
fld, field, fld, field,
html=params.html || ""; html=params.html || "";
scope.passwords = params.passwords; 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 += "<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) { scope.passwords.forEach(function(password) {
// Prompt for password // Prompt for password
field = form.fields[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 += "<label for=\"" + fld + "\">" + field.label + "</label>\n";
html += "<input type=\"password\" "; html += "<input type=\"password\" ";
html += "ng-model=\"" + fld + '" '; html += "ng-model=\"" + fld + '" ';
// html += "ng-keydown=\"keydown($event)\" ";
html += 'name="' + fld + '" '; html += 'name="' + fld + '" ';
html += "class=\"password-field form-control input-sm\" "; html += "class=\"password-field form-control input-sm\" ";
html += (field.associated) ? "ng-change=\"clearPWConfirm('" + field.associated + "')\" " : ""; 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 += "<label for=\"" + fld + "\"> " + field.label + "</label>\n";
html += "<input type=\"password\" "; html += "<input type=\"password\" ";
html += "ng-model=\"" + fld + '" '; html += "ng-model=\"" + fld + '" ';
// html += "ng-keydown=\"keydown($event)\" ";
html += 'name="' + fld + '" '; html += 'name="' + fld + '" ';
html += "class=\"form-control input-sm\" "; html += "class=\"form-control input-sm\" ";
html += "ng-change=\"checkStatus()\" "; 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', .factory('CheckPasswords', ['Rest', 'GetBasePath', 'ProcessErrors', 'Empty',
'ParseVariableString', 'ToJSON', 'ProcessErrors', '$stateParams' , function(Rest, GetBasePath, ProcessErrors, Empty) {
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) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
callback = params.callback, 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 // Submit SCM Update request
.factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait',
'ProjectsForm', 'Wait', function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) {
function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, ProjectsForm, Wait) {
return function (params) { return function (params) {
var scope = params.scope, var scope = params.scope,
project_id = params.project_id, project_id = params.project_id,
@@ -1028,10 +266,9 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
} }
]) ])
// Submit Inventory Update request // Submit Inventory Update request
.factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait',
function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { function (PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) {
return function (params) { return function (params) {
var scope = params.scope, 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) { return function(params) {
var scope = params.scope, var scope = params.scope,
id = params.id; 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, ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON, PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
ParseVariableString, RelatedSearchInit, RelatedPaginateInit, ParseVariableString, RelatedSearchInit, RelatedPaginateInit,
Prompt, PlaybookRun, CreateDialog, deleteJobTemplate, $state) { Prompt, InitiatePlaybookRun, CreateDialog, deleteJobTemplate, $state) {
ClearScope(); ClearScope();
@@ -153,7 +153,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
}; };
$scope.launchScanJob = function(){ $scope.launchScanJob = function(){
PlaybookRun({ scope: $scope, id: this.scan_job_template.id }); InitiatePlaybookRun({ scope: $scope, id: this.scan_job_template.id });
}; };
$scope.scheduleScanJob = function(){ $scope.scheduleScanJob = function(){
@@ -327,6 +327,6 @@ export default ['$scope', '$rootScope', '$compile', '$location',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString',
'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt',
'PlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state', 'InitiatePlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state',
InventoriesEdit, InventoriesEdit,
]; ];

View File

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

View File

@@ -0,0 +1,518 @@
/*************************************************
* 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.variables = (data.defaults && data.defaults.extra_vars) ? data.defaults.extra_vars : "---";
$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");
}
else 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");
}
$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) {
$scope.step = step;
if(step === "credential") {
$scope.credentialTabEnabled = true;
}
else if(step === "otherprompts") {
$scope.otherPromptsTabEnabled = true;
if(!$scope.extra_vars_code_mirror_loaded) {
ParseTypeChange({
scope: $scope,
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();
}
};
}
];

View File

@@ -0,0 +1,85 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import jobSubmissionController from './job-submission.controller';
export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2',
function(templateUrl, CreateDialog, Wait, CreateSelect2) {
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
});
});
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,229 @@
<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>
<input type="password" ng-model="passwords.ssh_password" ng-keydown="keydown($event)" name="ssh_password" class="password-field form-control input-sm" required>
<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>
<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>
<input type="password" ng-model="passwords.ssh_key_unlock" ng-keydown="keydown($event)" name="ssh_key_unlock" class="password-field form-control input-sm" required>
<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>
<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>
<input type="password" ng-model="passwords.become_password" ng-keydown="keydown($event)" name="become_password" class="password-field form-control input-sm" required>
<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>
<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>
<input type="password" ng-model="passwords.vault_password" ng-keydown="keydown($event)" name="vault_password" class="password-field form-control input-sm" required>
<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>
</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="parseType" value="yaml" ng-change="parseTypeChange()">
<span class="parse-label">YAML</span>
<input type="radio" ng-model="parseType" value="json" ng-change="parseTypeChange()">
<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" 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" 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 ng-pristine ng-valid-api-error ng-invalid" 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" 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" 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', 'Empty', 'Prompt', 'ParseVariableString', 'ToJSON',
'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate',
'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', 'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit',
'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', 'InitiatePlaybookRun' , 'initSurvey', '$state', 'CreateSelect2',
'ToggleNotification', 'NotificationsListInit', '$q', 'ToggleNotification', 'NotificationsListInit', '$q',
function( function(
$filter, $scope, $rootScope, $compile, $filter, $scope, $rootScope, $compile,
@@ -30,7 +30,7 @@ export default
GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait, GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait,
Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit,
JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit, JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit,
SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state, SchedulesList, CallbackHelpInit, InitiatePlaybookRun, SurveyControllerInit, $state,
CreateSelect2, ToggleNotification, NotificationsListInit, $q CreateSelect2, ToggleNotification, NotificationsListInit, $q
) { ) {
@@ -653,7 +653,7 @@ export default
} }
else { else {
PlaybookRun({ InitiatePlaybookRun({
scope: $scope, scope: $scope,
id: id id: id
}); });

View File

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

View File

@@ -304,17 +304,17 @@ export default
} }
}, },
buttons: { buttons: {
submit_question: {
ngClick: 'submitQuestion($event)',
ngDisabled: true,
'class': 'btn btn-sm Form-saveButton',
label: '{{editQuestionIndex === null ? "ADD" : "UPDATE"}}'
},
question_cancel : { question_cancel : {
label: 'Cancel', label: 'Cancel',
'class' : 'btn btn-default Form-cancelButton', 'class' : 'btn btn-default Form-cancelButton',
ngClick: 'generateAddQuestionForm()', ngClick: 'generateAddQuestionForm()',
ngDisabled: 'survey_question_form.$pristine' ngDisabled: 'survey_question_form.$pristine'
},
submit_question: {
ngClick: 'submitQuestion($event)',
ngDisabled: true,
'class': 'btn btn-sm Form-saveButton',
label: '{{editQuestionIndex === null ? "ADD" : "UPDATE"}}'
} }
} }

View File

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

View File

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

View File

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

View File

@@ -14,8 +14,8 @@
<div class="Prompt-bodyTarget">{{survey_questions[questionToBeDeleted].question_name}}</div> <div class="Prompt-bodyTarget">{{survey_questions[questionToBeDeleted].question_name}}</div>
</div> </div>
<div class="Modal-footer"> <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="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> </div>
</div> </div>
@@ -81,9 +81,9 @@
</div> </div>
<div class="SurveyMaker-panelFooter"> <div class="SurveyMaker-panelFooter">
<div class="Form-buttons"> <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-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> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
* All Rights Reserved * 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, var list = PortalJobTemplateList,
@@ -13,7 +13,7 @@ export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePat
pageSize = 12; pageSize = 12;
$scope.submitJob = function (id) { $scope.submitJob = function (id) {
PlaybookRun({ scope: $scope, id: id }); InitiatePlaybookRun({ scope: $scope, id: id });
}; };
var init = function(){ var init = function(){
@@ -42,5 +42,5 @@ export function PortalModeJobTemplatesController($scope, $rootScope, GetBasePat
init(); 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 // Change layout if a lookup list, place radio buttons before labels
if (options.mode === 'lookup') { 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 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\" " + 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 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\" " + 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'); html += buildSelectAll().prop('outerHTML');
} }
else if (options.mode === 'lookup') { 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) { for (fld in list.fields) {
if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) && if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) &&

View File

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