From 5f41b8df76bec66101f657ddf0ee6442fca1668e Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Mon, 12 May 2014 23:28:49 -0400 Subject: [PATCH] AC-1262 support for basic variable prompting on job submission. --- awx/ui/static/js/forms/JobTemplates.js | 41 +++-- awx/ui/static/js/forms/JobVarsPrompt.js | 38 ++++ awx/ui/static/js/helpers/JobSubmission.js | 192 ++++++++++++++++++-- awx/ui/static/lib/ansible/form-generator.js | 11 ++ awx/ui/templates/ui/index.html | 1 + 5 files changed, 252 insertions(+), 31 deletions(-) create mode 100644 awx/ui/static/js/forms/JobVarsPrompt.js diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index 85fbcdf1d8..200355ef47 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -180,6 +180,24 @@ angular.module('JobTemplateFormDefinition', ['SchedulesListDefinition', 'Complet dataPlacement: 'right', dataContainer: "body" }, + job_tags: { + label: 'Job Tags', + type: 'textarea', + rows: 1, + addRequired: false, + editRequired: false, + 'class': 'span12', + column: 2, + awPopOver: "

Provide a comma separated list of tags.

\n" + + "

Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.

" + + "

For example, you might have a task consisiting of a long list of actions. Tag values can be assigned to each action. " + + "Suppose the actions have been assigned tag values of "configuration", "packages" and "install".

" + + "

If you just want to run the "configuration" and "packages" actions, you would enter the following here " + + "in the Job Tags field:

\n
configuration,packages
\n", + dataTitle: "Job Tags", + dataPlacement: "right", + dataContainer: "body" + }, variables: { label: 'Extra Variables', type: 'textarea', @@ -199,22 +217,17 @@ angular.module('JobTemplateFormDefinition', ['SchedulesListDefinition', 'Complet dataPlacement: 'right', dataContainer: "body" }, - job_tags: { - label: 'Job Tags', - type: 'textarea', - rows: 1, + vars_prompt_on_launch: { + label: 'Prompt for Extra Variables', + type: 'checkbox', addRequired: false, - editRequired: false, - 'class': 'span12', + editRequird: false, + trueValue: 'true', + falseValue: 'false', column: 2, - awPopOver: "

Provide a comma separated list of tags.

\n" + - "

Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.

" + - "

For example, you might have a task consisiting of a long list of actions. Tag values can be assigned to each action. " + - "Suppose the actions have been assigned tag values of "configuration", "packages" and "install".

" + - "

If you just want to run the "configuration" and "packages" actions, you would enter the following here " + - "in the Job Tags field:

\n
configuration,packages
\n", - dataTitle: "Job Tags", - dataPlacement: "right", + awPopOver: "

If checked, user will be prompted at job launch with a dialog allowing override of the extra variables setting.

", + dataPlacement: 'right', + dataTitle: 'Prompt for Extra Variables', dataContainer: "body" }, allow_callbacks: { diff --git a/awx/ui/static/js/forms/JobVarsPrompt.js b/awx/ui/static/js/forms/JobVarsPrompt.js new file mode 100644 index 0000000000..be7addc648 --- /dev/null +++ b/awx/ui/static/js/forms/JobVarsPrompt.js @@ -0,0 +1,38 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * JobVarsPrompt.js + * + * Form definition used during job submission to prompt for extra vars + * + */ + +'use strict'; + +angular.module('JobVarsPromptFormDefinition', []) + + .value ('JobVarsPromptForm', { + + addTitle: '', + editTitle: '', + name: 'job', + well: false, + + actions: { }, + + fields: { + variables: { + label: null, + type: 'textarea', + rows: 6, + addRequired: false, + editRequired: false, + "default": "---" + } + }, + + buttons: { }, + + related: { } + + }); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index bcbafb3579..10a8af6ad6 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -8,7 +8,7 @@ 'use strict'; angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', - 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog']) + 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) .factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', function(Rest, Wait, ProcessErrors) { return function(params) { @@ -241,14 +241,146 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi }; }]) +.factory('PromptForVars', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', + 'ParseVariableString', 'ToJSON', 'ProcessErrors', + function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, JobVarsPromptForm, Wait, ParseVariableString, ToJSON, + ProcessErrors) { + return function(params) { + var buttons, + parent_scope = params.scope, + scope = parent_scope.$new(), + callback = params.callback, + job = params.job, + e, helpContainer, html; + + html = GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); + helpContainer = "
\n" + + "" + + " click for help
\n"; + + scope.helpText = "

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

" + + "

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

" + + "JSON:
\n" + + "
{
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + "
esc or click to close
\n"; + + scope.variables = ParseVariableString(params.variables); + scope.parseType = 'yaml'; + + // Reuse password modal + $('#password-modal').empty().html(html); + $('#password-modal').find('textarea').before(helpContainer); + e = angular.element(document.getElementById('password-modal')); + $compile(e)(scope); + + buttons = [{ + label: "Cancel", + onClick: function() { + scope.varsCancel(); + }, + icon: "fa-times", + "class": "btn btn-default", + "id": "vars-cancel-button" + },{ + label: "Continue", + onClick: function() { + scope.varsAccept(); + }, + icon: "fa-check", + "class": "btn btn-primary", + "id": "vars-accept-button" + }]; + + if (scope.removeDialogReady) { + scope.removeDialogReady(); + } + scope.removeDialogReady = scope.$on('DialogReady', function() { + Wait('stop'); + $('#password-modal').dialog('open'); + setTimeout(function() { + TextareaResize({ + scope: scope, + textareaId: 'job_variables', + modalId: 'password-modal', + formId: 'job_form', + parse: true + }); + }, 300); + }); + + CreateDialog({ + id: 'password-modal', + scope: scope, + buttons: buttons, + width: 575, + height: 530, + minWidth: 450, + title: 'Extra Variables', + onResizeStop: function() { + TextareaResize({ + scope: scope, + textareaId: 'job_variables', + modalId: 'password-modal', + formId: 'job_form', + parse: true + }); + }, + beforeDestroy: function() { + if (scope.codeMirror) { + scope.codeMirror.destroy(); + } + $('#password-modal').empty(); + }, + onOpen: function() { + $('#job_variables').focus(); + }, + callback: 'DialogReady' + }); + + scope.varsCancel = function() { + $('#password-modal').dialog('close'); + parent_scope.$emit('CancelJob'); + scope.$destroy(); + }; + + scope.varsAccept = function() { + job.extra_vars = ToJSON(scope.parseType, scope.variables, true); + Wait('start'); + Rest.setUrl(GetBasePath('jobs') + job.id + '/'); + Rest.put(job) + .success(function() { + Wait('stop'); + $('#password-modal').dialog('close'); + parent_scope.$emit(callback); + scope.$destroy(); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed updating job ' + job.id + ' with variables. PUT returned: ' + status }); + }); + }; + + }; +}]) + // Submit request to run a playbook -.factory('PlaybookRun', ['$location','$routeParams', 'LaunchJob', 'PromptForPasswords', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'Empty', 'PromptForCredential', - function ($location, $routeParams, LaunchJob, PromptForPasswords, Rest, GetBasePath, ProcessErrors, Wait, Empty, PromptForCredential) { +.factory('PlaybookRun', ['$location','$routeParams', 'LaunchJob', 'PromptForPasswords', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'Empty', 'PromptForCredential', 'PromptForVars', + function ($location, $routeParams, LaunchJob, PromptForPasswords, Rest, GetBasePath, ProcessErrors, Wait, Empty, PromptForCredential, PromptForVars) { return function (params) { var scope = params.scope, id = params.id, base = $location.path().replace(/^\//, '').split('/')[0], - url, job_template, new_job_id, launch_url; + url, + job_template, + new_job_id, + new_job, + launch_url, + prompt_for_vars = false, + passwords; if (!Empty($routeParams.template_id)) { // launching a job from job_template detail page @@ -268,10 +400,16 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi Rest.post(job_template).success(function (data) { new_job_id = data.id; launch_url = data.related.start; + prompt_for_vars = data.vars_prompt_on_launch; + new_job = data; if (data.passwords_needed_to_start.length > 0) { scope.$emit('PromptForPasswords', data.passwords_needed_to_start); - } else { - scope.$emit('StartPlaybookRun', {}); + } + else if (data.vars_prompt_on_launch) { + scope.$emit('PromptForVars'); + } + else { + scope.$emit('StartPlaybookRun'); } }).error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -279,10 +417,10 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi }); }); - if (scope.removePasswordsCanceled) { - scope.removePasswordsCanceled(); + if (scope.removeCancelJob) { + scope.removeCancelJob(); } - scope.removePasswordsCanceled = scope.$on('PasswordsCanceled', function() { + scope.removeCancelJob = scope.$on('CancelJob', function() { // Delete the job Wait('start'); Rest.setUrl(GetBasePath('jobs') + new_job_id + '/'); @@ -301,18 +439,17 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi } scope.removePlaybookLaunchFinished = scope.$on('PlaybookLaunchFinished', function() { var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base !== 'jobs') { + if (base === 'jobs') { + scope.refreshJobs(); + } else { $location.path('/jobs'); } - else { - Wait('stop'); - } }); if (scope.removeStartPlaybookRun) { scope.removeStartPlaybookRun(); } - scope.removeStartPlaybookRun = scope.$on('StartPlaybookRun', function(e, passwords) { + scope.removeStartPlaybookRun = scope.$on('StartPlaybookRun', function() { LaunchJob({ scope: scope, url: launch_url, @@ -324,8 +461,11 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi if (scope.removePromptForPasswords) { scope.removePromptForPasswords(); } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function(e, passwords) { - PromptForPasswords({ scope: scope, passwords: passwords, callback: 'StartPlaybookRun' }); + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function(e, passwords_needed_to_start) { + PromptForPasswords({ scope: scope, + passwords: passwords_needed_to_start, + callback: 'PromptForVars' + }); }); if (scope.removePromptForCredential) { @@ -335,6 +475,25 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi PromptForCredential({ scope: scope, template: data }); }); + if (scope.removePromptForVars) { + scope.removePromptForVars(); + } + scope.removePromptForVars = scope.$on('PromptForVars', function(e, pwds) { + passwords = pwds; + if (prompt_for_vars) { + // call prompt with callback of StartPlaybookRun, passwords + PromptForVars({ + scope: scope, + job: new_job, + variables: job_template.extra_vars, + callback: 'StartPlaybookRun' + }); + } + else { + scope.$emit('StartPlaybookRun'); + } + }); + if (scope.removeCredentialReady) { scope.removeCredentialReady(); } @@ -367,7 +526,6 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi } ]) - // Submit SCM Update request .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'ProjectsForm', 'Wait', diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 01b8a521ec..e1da3c2480 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -197,6 +197,17 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator }, + buildHTML: function(form, options) { + // Get HTML without actually injecting into DOM. Caller is responsible for any injection. + // Example: + // html = GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); + + this.mode = options.mode; + this.modal = (options.modal) ? true : false; + this.setForm(form); + return this.build(options); + }, + applyDefaults: function () { for (var fld in this.form.fields) { if (this.form.fields[fld]['default'] || this.form.fields[fld]['default'] === 0) { diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 8b95ffc1d3..44592a451f 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -100,6 +100,7 @@ +