From 7fd24987d71e4fa6c1dc080b559dffc9b52e04a4 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 26 Aug 2015 11:35:26 -0400 Subject: [PATCH] update adhoc feature, add tests and modularize --- awx/ui/client/src/adhoc/adhoc.controller.js | 317 ++++++++++++++++++ awx/ui/client/src/adhoc/adhoc.form.js | 143 ++++++++ .../adhoc.html => adhoc/adhoc.partial.html} | 0 awx/ui/client/src/adhoc/adhoc.route.js | 19 ++ awx/ui/client/src/adhoc/main.js | 12 + awx/ui/client/src/app.js | 18 +- awx/ui/client/src/controllers/Adhoc.js | 248 -------------- awx/ui/client/src/forms.js | 2 - awx/ui/client/src/forms/Adhoc.js | 143 -------- awx/ui/client/src/helpers/Adhoc.js | 2 +- awx/ui/client/src/helpers/Jobs.js | 2 +- .../tests/adhoc/adhoc.controller-test.js | 220 ++++++++++++ awx/ui/client/tests/support/node/index.js | 2 + .../tests/support/node/setup/angular-route.js | 1 + .../tests/support/node/setup/jquery-ui.js | 4 + 15 files changed, 722 insertions(+), 411 deletions(-) create mode 100644 awx/ui/client/src/adhoc/adhoc.controller.js create mode 100644 awx/ui/client/src/adhoc/adhoc.form.js rename awx/ui/client/src/{partials/adhoc.html => adhoc/adhoc.partial.html} (100%) create mode 100644 awx/ui/client/src/adhoc/adhoc.route.js create mode 100644 awx/ui/client/src/adhoc/main.js delete mode 100644 awx/ui/client/src/controllers/Adhoc.js delete mode 100644 awx/ui/client/src/forms/Adhoc.js create mode 100644 awx/ui/client/tests/adhoc/adhoc.controller-test.js create mode 100644 awx/ui/client/tests/support/node/setup/angular-route.js create mode 100644 awx/ui/client/tests/support/node/setup/jquery-ui.js diff --git a/awx/ui/client/src/adhoc/adhoc.controller.js b/awx/ui/client/src/adhoc/adhoc.controller.js new file mode 100644 index 0000000000..bb78c5564a --- /dev/null +++ b/awx/ui/client/src/adhoc/adhoc.controller.js @@ -0,0 +1,317 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Adhoc + * @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran. +*/ +function adhocController($q, $scope, $rootScope, $location, $routeParams, + CheckPasswords, PromptForPasswords, CreateLaunchDialog, adhocForm, + GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, + KindChange, LookUpInit, CredentialList, Empty, Wait) { + + ClearScope(); + + // this is done so that we can access private functions for testing, but + // we don't want to populate the "public" scope with these internal + // functions + var privateFn = {}; + this.privateFn = privateFn; + + // note: put any urls that the controller will use in here!!!! + privateFn.setAvailableUrls = function() { + return { + adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/', + inventoryUrl: GetBasePath('inventory') + id + '/', + machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh' + }; + }; + + var id = $routeParams.inventory_id, + urls = privateFn.setAvailableUrls(), + hostPattern = $rootScope.hostPatterns || "all"; + + // set the default options for the selects of the adhoc form + privateFn.setFieldDefaults = function(verbosity_options, forks_default) { + var verbosity; + for (verbosity in verbosity_options) { + if (verbosity_options[verbosity].isDefault) { + $scope.verbosity = verbosity_options[verbosity]; + } + } + $("#forks-number").spinner("value", forks_default); + $scope.forks = forks_default; + }; + + // set when "working" starts and stops + privateFn.setLoadingStartStop = function() { + var asyncHelper = {}, + formReadyPromise = 0; + + Wait('start'); + + if (asyncHelper.removeChoicesReady) { + asyncHelper.removeChoicesReady(); + } + asyncHelper.removeChoicesReady = $scope.$on('adhocFormReady', + isFormDone); + + // check to see if all requests have completed + function isFormDone() { + formReadyPromise++; + + if (formReadyPromise === 2) { + privateFn.setFieldDefaults($scope.adhoc_verbosity_options, + $scope.forks_field.default); + Wait('stop'); + } + } + }; + + privateFn.getInventoryNameForBreadcrumbs = function(url) { + + Rest.setUrl(url); + var promise = Rest.get(); + promise.then(function (response) { + $scope.inv_name = response.data.name; + }); + promise.catch(function (response) { + ProcessErrors($rootScope, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory name. GET returned status: ' + + response.status }); + $location.path("/inventories/"); + }); + return promise; + }; + + // set the arguments help to watch on change of the module + privateFn.instantiateArgumentHelp = function() { + $scope.$watch('module_name', function(val) { + if (val) { + // give the docs for the selected module in the popover + $scope.argsPopOver = '

These arguments are used with the ' + + 'specified module. You can find information about the ' + + val.value + ' module here.

'; + } else { + // no module selected + $scope.argsPopOver = "

These arguments are used with the" + + " specified module.

"; + } + }, true); + + // initially set to the same as no module selected + $scope.argsPopOver = "

These arguments are used with the " + + "specified module.

"; + }; + + // pre-populate host patterns from the inventory page and + // delete the value off of rootScope + privateFn.instantiateHostPatterns = function(hostPattern) { + $scope.limit = hostPattern; + $scope.providedHostPatterns = $scope.limit; + delete $rootScope.hostPatterns; + }; + + // call helpers to initialize lookup and select fields through get + // requests + privateFn.initializeFields = function(machineCredentialUrl, adhocUrl) { + // setup machine credential lookup + LookUpInit({ + url: machineCredentialUrl, + scope: $scope, + form: adhocForm, + current_item: (!Empty($scope.credential_id)) ? + $scope.credential_id : null, + list: CredentialList, + field: 'credential', + input_type: 'radio' + }); + + // setup module name select + GetChoices({ + scope: $scope, + url: adhocUrl, + field: 'module_name', + variable: 'adhoc_module_options', + callback: 'adhocFormReady' + }); + + // setup verbosity options select + GetChoices({ + scope: $scope, + url: adhocUrl, + field: 'verbosity', + variable: 'adhoc_verbosity_options', + callback: 'adhocFormReady' + }); + }; + + // instantiate all variables on scope for display in the partial + privateFn.initializeForm = function(id, urls, hostPattern) { + // inject the adhoc command form + GenerateForm.inject(adhocForm, + { mode: 'edit', related: true, scope: $scope }); + + // set when "working" starts and stops + privateFn.setLoadingStartStop(); + + // put the inventory id on scope for the partial to use + $scope.inv_id = id; + + // get the inventory name + privateFn.getInventoryNameForBreadcrumbs(urls.inventoryUrl); + + // set the arguments help to watch on change of the module + privateFn.instantiateArgumentHelp(); + + // pre-populate host patterns from the inventory page and + // delete the value off of rootScope + privateFn.instantiateHostPatterns(hostPattern); + + privateFn.initializeFields(urls.machineCredentialUrl, urls.adhocUrl); + }; + + privateFn.initializeForm(id, urls, hostPattern); + + // remove all data input into the form and reset the form back to defaults + $scope.formReset = function () { + GenerateForm.reset(); + + // pre-populate host patterns from the inventory page and + // delete the value off of rootScope + privateFn.instantiateHostPatterns($scope.providedHostPatterns); + + KindChange({ scope: $scope, form: adhocForm, reset: false }); + + // set the default options for the selects of the adhoc form + privateFn.setFieldDefaults($scope.adhoc_verbosity_options, + $scope.forks_default); + }; + + // launch the job with the provided form data + $scope.launchJob = function () { + var adhocUrl = GetBasePath('inventory') + $routeParams.inventory_id + + '/ad_hoc_commands/', fld, data={}, html; + + html = '
'; + + // stub the payload with defaults from DRF + data = { + "job_type": "run", + "limit": "", + "credential": null, + "module_name": "command", + "module_args": "", + "forks": 0, + "verbosity": 0, + "privilege_escalation": "" + }; + + GenerateForm.clearApiErrors(); + + // populate data with the relevant form values + for (fld in adhocForm.fields) { + if (adhocForm.fields[fld].type === 'select') { + data[fld] = $scope[fld].value; + } else { + data[fld] = $scope[fld]; + } + } + + Wait('start'); + + if ($scope.removeStartAdhocRun) { + $scope.removeStartAdhocRun(); + } + $scope.removeStartAdhocRun = $scope.$on('StartAdhocRun', function() { + var password; + for (password in $scope.passwords) { + data[$scope.passwords[password]] = $scope[ + $scope.passwords[password] + ]; + } + // Launch the adhoc job + Rest.setUrl(GetBasePath('inventory') + + $routeParams.inventory_id + '/ad_hoc_commands/'); + Rest.post(data) + .success(function (data) { + Wait('stop'); + $location.path("/ad_hoc_commands/" + data.id); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, adhocForm, { + hdr: 'Error!', + msg: 'Failed to launch adhoc command. POST ' + + 'returned status: ' + status }); + }); + }); + + if ($scope.removeCreateLaunchDialog) { + $scope.removeCreateLaunchDialog(); + } + $scope.removeCreateLaunchDialog = $scope.$on('CreateLaunchDialog', + function(e, html, url) { + CreateLaunchDialog({ + scope: $scope, + html: html, + url: url, + callback: 'StartAdhocRun' + }); + }); + + 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: 'CreateLaunchDialog', + html: html, + url: url + }); + }); + + if ($scope.removeContinueCred) { + $scope.removeContinueCred(); + } + $scope.removeContinueCred = $scope.$on('ContinueCred', function(e, + passwords) { + if(passwords.length>0){ + $scope.passwords_needed_to_start = passwords; + // only go through the password prompting steps if there are + // passwords to prompt for + $scope.$emit('PromptForPasswords', passwords, html, adhocUrl); + } else { + // if not, go straight to trying to run the job. + $scope.$emit('StartAdhocRun', adhocUrl); + } + }); + + // start adhoc launching routine + CheckPasswords({ + scope: $scope, + credential: $scope.credential, + callback: 'ContinueCred' + }); + }; + + +} + +export default ['$q', '$scope', '$rootScope', '$location', '$routeParams', + 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'adhocForm', + 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', + 'GetChoices', 'KindChange', 'LookUpInit', 'CredentialList', 'Empty', 'Wait', + adhocController]; diff --git a/awx/ui/client/src/adhoc/adhoc.form.js b/awx/ui/client/src/adhoc/adhoc.form.js new file mode 100644 index 0000000000..c494f3d311 --- /dev/null +++ b/awx/ui/client/src/adhoc/adhoc.form.js @@ -0,0 +1,143 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Adhoc + * @description This form is for executing an adhoc command +*/ + +export default function() { + return { + editTitle: 'Execute Command', + name: 'adhoc', + well: true, + forceListeners: true, + + fields: { + module_name: { + label: 'Module', + excludeModal: true, + type: 'select', + ngOptions: 'module.label for module in adhoc_module_options' + + ' track by module.value', + ngChange: 'moduleChange()', + editRequired: true, + awPopOver:'

These are the modules that Tower supports ' + + 'running commands against.', + dataTitle: 'Module', + dataPlacement: 'right', + dataContainer: 'body' + }, + module_args: { + label: 'Arguments', + type: 'text', + awPopOverWatch: 'argsPopOver', + awPopOver: '{{ argsPopOver }}', + dataTitle: 'Arguments', + dataPlacement: 'right', + dataContainer: 'body', + editRequired: false, + autocomplete: false + }, + limit: { + label: 'Host Pattern', + type: 'text', + addRequired: false, + editRequired: false, + awPopOver: '

The pattern used to target hosts in the ' + + 'inventory. Leaving the field blank, all, and * will ' + + 'all target all hosts in the inventory. You can find ' + + 'more information about Ansible\'s host patterns ' + + 'here.

', + dataTitle: 'Host Pattern', + dataPlacement: 'right', + dataContainer: 'body' + }, + credential: { + label: 'Machine Credential', + type: 'lookup', + sourceModel: 'credential', + sourceField: 'name', + ngClick: 'lookUpCredential()', + class: 'squeeze', + awPopOver: '

Select the credential you want to use when ' + + 'accessing the remote hosts to run the command. ' + + 'Choose the credential containing ' + + 'the username and SSH key or password that Ansbile ' + + 'will need to log into the remote hosts.

', + dataTitle: 'Credential', + dataPlacement: 'right', + dataContainer: 'body', + awRequiredWhen: { + variable: 'credRequired', + init: 'false' + } + }, + become_enabled: { + label: 'Enable Privilege Escalation', + type: 'checkbox', + addRequired: false, + editRequired: false, + column: 2, + awPopOver: "

If enabled, run this playbook as an administrator. This is the equivalent of passing the --become option to the ansible command.

", + dataPlacement: 'right', + dataTitle: 'Become Privilege Escalation', + dataContainer: "body" + }, + verbosity: { + label: 'Verbosity', + excludeModal: true, + type: 'select', + ngOptions: 'verbosity.label for verbosity in ' + + 'adhoc_verbosity_options ' + + 'track by verbosity.value', + editRequired: true, + awPopOver:'

These are the verbosity levels for standard ' + + 'out of the command run that are supported.', + dataTitle: 'Module', + dataPlacement: 'right', + dataContainer: 'body', + "default": 1 + }, + forks: { + label: 'Forks', + id: 'forks-number', + type: 'number', + integer: true, + min: 0, + spinner: true, + "default": 0, + addRequired: false, + editRequired: true, + 'class': "input-small", + column: 1, + awPopOver: '

The number of parallel or simultaneous processes to use while executing the command. 0 signifies ' + + 'the default value from the ansible configuration file.

', + dataTitle: 'Forks', + dataPlacement: 'right', + dataContainer: "body" + }, + }, + + buttons: { + launch: { + label: 'Launch', + ngClick: 'launchJob()', + ngDisabled: true + }, + reset: { + ngClick: 'formReset()', + ngDisabled: true + } + }, + + related: {} + }; +} diff --git a/awx/ui/client/src/partials/adhoc.html b/awx/ui/client/src/adhoc/adhoc.partial.html similarity index 100% rename from awx/ui/client/src/partials/adhoc.html rename to awx/ui/client/src/adhoc/adhoc.partial.html diff --git a/awx/ui/client/src/adhoc/adhoc.route.js b/awx/ui/client/src/adhoc/adhoc.route.js new file mode 100644 index 0000000000..f5fa7e9639 --- /dev/null +++ b/awx/ui/client/src/adhoc/adhoc.route.js @@ -0,0 +1,19 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import {templateUrl} from '../shared/template-url/template-url.factory'; + +export default { + route: '/inventories/:inventory_id/adhoc', + name: 'inventoryAdhoc', + templateUrl: templateUrl('adhoc/adhoc'), + controller: 'adhocController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/adhoc/main.js b/awx/ui/client/src/adhoc/main.js new file mode 100644 index 0000000000..e4d8d26bf7 --- /dev/null +++ b/awx/ui/client/src/adhoc/main.js @@ -0,0 +1,12 @@ +import route from './adhoc.route'; +import adhocController from './adhoc.controller'; +import form from './adhoc.form'; + +export default angular.module('adhoc', ["ngRoute"]) + .controller('adhocController', adhocController) + .config(['$routeProvider', function($routeProvider) { + var url = route.route; + delete route.route; + $routeProvider.when(url, route); + }]) + .factory('adhocForm', form); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index d5fc78bfb2..cce9620fef 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -43,6 +43,7 @@ import browserData from './browser-data/main'; import dashboard from './dashboard/main'; import moment from './shared/moment/main'; import templateUrl from './shared/template-url/main'; +import adhoc from './adhoc/main'; import {JobDetailController} from './controllers/JobDetail'; import {JobStdoutController} from './controllers/JobStdout'; @@ -52,7 +53,6 @@ import {ScheduleEditController} from './controllers/Schedules'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects'; import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from './controllers/Organizations'; import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from './controllers/Inventories'; -import {AdhocCtrl} from './controllers/Adhoc'; import {AdminsList} from './controllers/Admins'; import {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams'; @@ -93,6 +93,7 @@ var tower = angular.module('Tower', [ dashboard.name, moment.name, templateUrl.name, + adhoc.name, 'templates', 'AuthService', 'Utilities', @@ -112,7 +113,6 @@ var tower = angular.module('Tower', [ 'RefreshHelper', 'AdminListDefinition', 'AWDirectives', - 'AdhocFormDefinition', 'InventoriesListDefinition', 'InventoryFormDefinition', 'InventoryHelper', @@ -182,9 +182,6 @@ var tower = angular.module('Tower', [ 'SocketHelper', 'AboutAnsibleHelpModal', 'PortalJobsListDefinition', - - - 'AdhocHelper', 'features', 'longDateFilter' ]) @@ -468,17 +465,6 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/:inventory_id/adhoc', { - name: 'inventoryAdhoc', - templateUrl: urlPrefix + 'partials/adhoc.html', - controller: AdhocCtrl, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - when('/organizations', { name: 'organizations', templateUrl: urlPrefix + 'partials/organizations.html', diff --git a/awx/ui/client/src/controllers/Adhoc.js b/awx/ui/client/src/controllers/Adhoc.js deleted file mode 100644 index 404cfe4167..0000000000 --- a/awx/ui/client/src/controllers/Adhoc.js +++ /dev/null @@ -1,248 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Adhoc - * @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran. -*/ -export function AdhocCtrl($scope, $rootScope, $location, $routeParams, - CheckPasswords, PromptForPasswords, CreateLaunchDialog, AdhocForm, GenerateForm, Rest, ProcessErrors, ClearScope, - GetBasePath, GetChoices, KindChange, LookUpInit, CredentialList, Empty, - Wait) { - - ClearScope(); - - var url = GetBasePath('inventory') + $routeParams.inventory_id + - '/ad_hoc_commands/', - generator = GenerateForm, - form = AdhocForm, - master = {}, - id = $routeParams.inventory_id, - choicesReadyCount = 0, - data; - - $scope.inv_id = id; - - Rest.setUrl(GetBasePath('inventory') + $routeParams.inventory_id + '/'); - Rest.get(data) - .success(function (data) { - $scope.inv_name = data.name; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to get inventory name. POST returned ' + - 'status: ' + status }); - }); - - // inject the adhoc command form - generator.inject(form, { mode: 'edit', related: true, scope: $scope }); - generator.reset(); - - // BEGIN: populate scope with the things needed to make the adhoc form - // display - Wait('start'); - $scope.id = id; - $scope.argsPopOver = "

These arguments are used with the" + - " specified module.

"; - - // fix arguments help popover based on the module selected - $scope.moduleChange = function () { - // NOTE: for selenium testing link - - // link will be displayed with id adhoc_module_arguments_docs_link - // only when a module is selected - if ($scope.module_name) { - // give the docs for the selected module - $scope.argsPopOver = "

These arguments are used with the" + - " specified module. You can find information about the " + - $scope.module_name.value + " module " + - " here.

"; - } else { - // no module selected - $scope.argsPopOver = "

These arguments are used with the" + - " specified module.

"; - } - }; - - // pre-populate hostPatterns from the inventory page and - // delete the value off of rootScope - $scope.limit = $rootScope.hostPatterns || "all"; - $scope.providedHostPatterns = $scope.limit; - delete $rootScope.hostPatterns; - - LookUpInit({ - url: GetBasePath('credentials') + '?kind=ssh', - scope: $scope, - form: form, - current_item: (!Empty($scope.credential_id)) ? $scope.credential_id : null, - list: CredentialList, - field: 'credential', - input_type: 'radio' - }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReadyAdhoc', function () { - choicesReadyCount++; - - if (choicesReadyCount === 2) { - var verbosity; - // this sets the default options for the selects as specified by the controller. - for (verbosity in $scope.adhoc_verbosity_options) { - if ($scope.adhoc_verbosity_options[verbosity].isDefault) { - $scope.verbosity = $scope.adhoc_verbosity_options[verbosity]; - } - } - $("#forks-number").spinner("value", $scope.forks_field.default); - $scope.forks = $scope.forks_field.default; - Wait('stop'); // END: form population - } - }); - - // setup Machine Credential lookup - GetChoices({ - scope: $scope, - url: url, - field: 'module_name', - variable: 'adhoc_module_options', - callback: 'choicesReadyAdhoc' - }); - - // setup verbosity options lookup - GetChoices({ - scope: $scope, - url: url, - field: 'verbosity', - variable: 'adhoc_verbosity_options', - callback: 'choicesReadyAdhoc' - }); - - // launch the job with the provided form data - $scope.launchJob = function () { - var fld, data={}, html; - - html = ''; - - // stub the payload with defaults from DRF - data = { - "job_type": "run", - "limit": "", - "credential": null, - "module_name": "command", - "module_args": "", - "forks": 0, - "verbosity": 0, - "privilege_escalation": "" - }; - - generator.clearApiErrors(); - - // populate data with the relevant form values - for (fld in form.fields) { - if (form.fields[fld].type === 'select') { - data[fld] = $scope[fld].value; - } else { - data[fld] = $scope[fld]; - } - } - - Wait('start'); - - if ($scope.removeStartAdhocRun) { - $scope.removeStartAdhocRun(); - } - $scope.removeStartAdhocRun = $scope.$on('StartAdhocRun', function() { - var password; - for (password in $scope.passwords) { - data[$scope.passwords[password]] = $scope[$scope.passwords[password]]; - } - // Launch the adhoc job - Rest.setUrl(GetBasePath('inventory') + - $routeParams.inventory_id + '/ad_hoc_commands/'); - Rest.post(data) - .success(function (data) { - Wait('stop'); - $location.path("/ad_hoc_commands/" + data.id); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to launch adhoc command. POST returned ' + - 'status: ' + status }); - }); - }); - - if ($scope.removeCreateLaunchDialog) { - $scope.removeCreateLaunchDialog(); - } - $scope.removeCreateLaunchDialog = $scope.$on('CreateLaunchDialog', - function(e, html, url) { - CreateLaunchDialog({ - scope: $scope, - html: html, - url: url, - callback: 'StartAdhocRun' - }); - }); - - 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: 'CreateLaunchDialog', - html: html, - url: url - }); - }); - - if ($scope.removeContinueCred) { - $scope.removeContinueCred(); - } - $scope.removeContinueCred = $scope.$on('ContinueCred', function(e, passwords) { - if(passwords.length>0){ - $scope.passwords_needed_to_start = passwords; - // only go through the password prompting steps if there are - // passwords to prompt for - $scope.$emit('PromptForPasswords', passwords, html, url); - } else { - // if not, go straight to trying to run the job. - $scope.$emit('StartAdhocRun', url); - } - }); - - // start adhoc launching routine - CheckPasswords({ - scope: $scope, - credential: $scope.credential, - callback: 'ContinueCred' - }); - }; - - // Remove all data input into the form - $scope.formReset = function () { - generator.reset(); - for (var fld in master) { - $scope[fld] = master[fld]; - } - $scope.limit = $scope.providedHostPatterns; - KindChange({ scope: $scope, form: form, reset: false }); - $scope.verbosity = $scope.adhoc_verbosity_options[$scope.verbosity_field.default]; - $("#forks-number").spinner("value", $scope.forks_field.default); - $scope.forks = $scope.forks_field.default; - }; -} - -AdhocCtrl.$inject = ['$scope', '$rootScope', '$location', '$routeParams', - 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'AdhocForm', - 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', - 'GetChoices', 'KindChange', 'LookUpInit', 'CredentialList', 'Empty', 'Wait']; diff --git a/awx/ui/client/src/forms.js b/awx/ui/client/src/forms.js index 3bd8170ee7..6b03381c22 100644 --- a/awx/ui/client/src/forms.js +++ b/awx/ui/client/src/forms.js @@ -6,7 +6,6 @@ import ActivityDetail from "./forms/ActivityDetail"; import Credentials from "./forms/Credentials"; -import Adhoc from "./forms/Adhoc"; import EventsViewer from "./forms/EventsViewer"; import Groups from "./forms/Groups"; import HostGroups from "./forms/HostGroups"; @@ -33,7 +32,6 @@ import Users from "./forms/Users"; export { ActivityDetail, Credentials, - Adhoc, EventsViewer, Groups, HostGroups, diff --git a/awx/ui/client/src/forms/Adhoc.js b/awx/ui/client/src/forms/Adhoc.js deleted file mode 100644 index 137bd66624..0000000000 --- a/awx/ui/client/src/forms/Adhoc.js +++ /dev/null @@ -1,143 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name forms.function:Adhoc - * @description This form is for executing an adhoc command -*/ - -export default - angular.module('AdhocFormDefinition', []) - .value('AdhocForm', { - editTitle: 'Execute Command', - name: 'adhoc', - well: true, - forceListeners: true, - - fields: { - module_name: { - label: 'Module', - excludeModal: true, - type: 'select', - ngOptions: 'module.label for module in adhoc_module_options' + - ' track by module.value', - ngChange: 'moduleChange()', - editRequired: true, - awPopOver:'

These are the modules that Tower supports ' + - 'running commands against.', - dataTitle: 'Module', - dataPlacement: 'right', - dataContainer: 'body' - }, - module_args: { - label: 'Arguments', - type: 'text', - awPopOverWatch: 'argsPopOver', - awPopOver: '{{ argsPopOver }}', - dataTitle: 'Arguments', - dataPlacement: 'right', - dataContainer: 'body', - editRequired: false, - autocomplete: false - }, - limit: { - label: 'Host Pattern', - type: 'text', - addRequired: false, - editRequired: false, - awPopOver: '

The pattern used to target hosts in the ' + - 'inventory. Leaving the field blank, all, and * will ' + - 'all target all hosts in the inventory. You can find ' + - 'more information about Ansible\'s host patterns ' + - 'here.

', - dataTitle: 'Host Pattern', - dataPlacement: 'right', - dataContainer: 'body' - }, - credential: { - label: 'Machine Credential', - type: 'lookup', - sourceModel: 'credential', - sourceField: 'name', - ngClick: 'lookUpCredential()', - class: 'squeeze', - awPopOver: '

Select the credential you want to use when ' + - 'accessing the remote hosts to run the command. ' + - 'Choose the credential containing ' + - 'the username and SSH key or password that Ansbile ' + - 'will need to log into the remote hosts.

', - dataTitle: 'Credential', - dataPlacement: 'right', - dataContainer: 'body', - awRequiredWhen: { - variable: 'credRequired', - init: 'false' - } - }, - become_enabled: { - label: 'Enable Privilege Escalation', - type: 'checkbox', - addRequired: false, - editRequird: false, - column: 2, - awPopOver: "

If enabled, run this playbook as an administrator. This is the equivalent of passing the --become option to the ansible command.

", - dataPlacement: 'right', - dataTitle: 'Become Privilege Escalation', - dataContainer: "body" - }, - verbosity: { - label: 'Verbosity', - excludeModal: true, - type: 'select', - ngOptions: 'verbosity.label for verbosity in ' + - 'adhoc_verbosity_options ' + - 'track by verbosity.value', - editRequired: true, - awPopOver:'

These are the verbosity levels for standard ' + - 'out of the command run that are supported.', - dataTitle: 'Module', - dataPlacement: 'right', - dataContainer: 'body', - "default": 1 - }, - forks: { - label: 'Forks', - id: 'forks-number', - type: 'number', - integer: true, - min: 0, - spinner: true, - "default": 0, - addRequired: false, - editRequired: false, - 'class': "input-small", - column: 1, - awPopOver: '

The number of parallel or simultaneous processes to use while executing the command. 0 signifies ' + - 'the default value from the ansible configuration file.

', - dataTitle: 'Forks', - dataPlacement: 'right', - dataContainer: "body" - }, - }, - - buttons: { - launch: { - label: 'Launch', - ngClick: 'launchJob()', - ngDisabled: true - }, - reset: { - ngClick: 'formReset()', - ngDisabled: true - } - }, - - related: {} - }); diff --git a/awx/ui/client/src/helpers/Adhoc.js b/awx/ui/client/src/helpers/Adhoc.js index 3924416d98..4fc00c19d4 100644 --- a/awx/ui/client/src/helpers/Adhoc.js +++ b/awx/ui/client/src/helpers/Adhoc.js @@ -3,7 +3,7 @@ * * All Rights Reserved *************************************************/ - + /** * @ngdoc function * @name helpers.function:Adhoc diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index 153678b2f7..dac06e03c6 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -3,7 +3,7 @@ * * All Rights Reserved *************************************************/ - + /** * @ngdoc function * @name helpers.function:Jobs diff --git a/awx/ui/client/tests/adhoc/adhoc.controller-test.js b/awx/ui/client/tests/adhoc/adhoc.controller-test.js new file mode 100644 index 0000000000..a91f9cc1b8 --- /dev/null +++ b/awx/ui/client/tests/adhoc/adhoc.controller-test.js @@ -0,0 +1,220 @@ +import '../support/node'; + +import adhocModule from 'adhoc/main'; +import RestStub from '../support/rest-stub'; + +describe("adhoc.controller", function() { + var $scope, $rootScope, $location, $routeParams, + CheckPasswords, PromptForPasswords, CreateLaunchDialog, AdhocForm, + GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, + KindChange, LookUpInit, CredentialList, Empty, Wait; + + var $controller, ctrl, generateFormCallback, waitCallback, locationCallback, + getBasePath, processErrorsCallback, restCallback; + + beforeEach("instantiate the adhoc module", function() { + angular.mock.module(adhocModule.name); + }); + + before("create spies", function() { + getBasePath = function(path) { + return '/' + path + '/'; + }; + generateFormCallback = { + inject: angular.noop + }; + waitCallback = sinon.spy(); + locationCallback = { + path: sinon.spy() + }; + processErrorsCallback = sinon.spy(); + restCallback = new RestStub(); + }); + + beforeEach("mock dependencies", angular.mock.module(['$provide', function(_provide_) { + var $provide = _provide_; + + $provide.value('$location', locationCallback); + $provide.value('CheckPasswords', angular.noop); + $provide.value('PromptForPasswords', angular.noop); + $provide.value('CreateLaunchDialog', angular.noop); + $provide.value('AdhocForm', angular.noop); + $provide.value('GenerateForm', generateFormCallback); + $provide.value('Rest', restCallback); + $provide.value('ProcessErrors', processErrorsCallback); + $provide.value('ClearScope', angular.noop); + $provide.value('GetBasePath', getBasePath); + $provide.value('GetChoices', angular.noop); + $provide.value('KindChange', angular.noop); + $provide.value('LookUpInit', angular.noop); + $provide.value('CredentialList', angular.noop); + $provide.value('Empty', angular.noop); + $provide.value('Wait', waitCallback); + }])); + + beforeEach("put $q in scope", window.inject(['$q', function($q) { + restCallback.$q = $q; + }])); + + beforeEach("put the controller in scope", inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $controller = $injector.get('$controller'); + $scope = $rootScope.$new(); + ctrl = $controller('adhocController', {$scope: $scope}); + })); + + describe("setAvailableUrls", function() { + it('should only have the specified urls ' + + 'available for adhoc commands', function() { + var urls = ctrl.privateFn.setAvailableUrls(); + expect(urls).to.have.keys('adhocUrl', 'inventoryUrl', + 'machineCredentialUrl'); + + var count = 0; + var i; + + for (i in urls) { + if (urls.hasOwnProperty(i)) { + count++; + } + } + expect(count).to.equal(3); + }); + }); + + describe("setFieldDefaults", function() { + it('should set the select form field defaults' + + 'based on user settings', function() { + var verbosity_options = [ + {label: "0 (Foo)", value: 0, name: "0 (Foo)", + isDefault: false}, + {label: "1 (Bar)", value: 1, name: "1 (Bar)", + isDefault: true}, + ], + forks_field = {}; + + forks_field.default = 3; + + $scope.$apply(function() { + ctrl.privateFn.setFieldDefaults(verbosity_options, + forks_field.default); + }); + + expect($scope.forks).to.equal(forks_field.default); + expect($scope.verbosity.value).to.equal(1); + }); + }); + + describe("setLoadingStartStop", function() { + it('should start the controller working state when the form is ' + + 'loading', function() { + waitCallback.reset(); + ctrl.privateFn.setLoadingStartStop(); + expect(waitCallback).to.have.been.calledWith("start"); + }); + it('should stop the indicator after all REST calls in the form load have ' + + 'completed', function() { + var forks_field = {}, + adhoc_verbosity_options = {}; + forks_field.default = "1"; + $scope.$apply(function() { + $scope.forks_field = forks_field; + $scope.adhoc_verbosity_options = adhoc_verbosity_options; + }); + waitCallback.reset(); + $scope.$emit('adhocFormReady'); + $scope.$emit('adhocFormReady'); + expect(waitCallback).to.have.been.calledWith("stop"); + }); + }); + + describe("getInventoryNameForBreadcrumbs", function() { + it('should set the inventory name on scope', function() { + var req = ctrl.privateFn.getInventoryNameForBreadcrumbs("foo"); + var response = { data: { name: "foo" } }; + + restCallback.succeed(response); + restCallback.flush(); + + return req.then(function() { + expect($scope.inv_name).to.equal('foo'); + }); + }); + + it('should navigate to the inventory manage page when the inventory ' + + 'can not be found', function() { + var req = ctrl.privateFn.getInventoryNameForBreadcrumbs("foo"); + var response = { "detail": "Not found", status: "bad" }; + + restCallback.fail(response); + restCallback.flush(); + + return req.catch(function() { + expect(processErrorsCallback).to.have.been.called; + expect(locationCallback.path).to.have.been.calledWith("/inventories/"); + }); + }); + }); + + describe("instantiateArgumentHelp", function() { + it("should initially provide a canned argument help response", function() { + expect($scope.argsPopOver).to.equal('

These arguments are used ' + + 'with the specified module.

'); + }); + + it("should change the help response when the module changes", function() { + $scope.$apply(function () { + $scope.module_name = {value: 'foo'}; + }); + expect($scope.argsPopOver).to.equal('

These arguments are used ' + + 'with the specified module. You can find information about ' + + 'the foo module here.

'); + }); + + it("should change the help response when the module changes again", function() { + $scope.$apply(function () { + $scope.module_name = {value: 'bar'}; + }); + expect($scope.argsPopOver).to.equal('

These arguments are used ' + + 'with the specified module. You can find information about ' + + 'the bar module here.

'); + }); + + it("should change the help response back to the canned response " + + "when no module is selected", function() { + $scope.$apply(function () { + $scope.module_name = null; + }); + expect($scope.argsPopOver).to.equal('

These arguments are used ' + + 'with the specified module.

'); + }); + }); + + describe("instantiateHostPatterns", function() { + it("should initialize the limit object based on the provided host " + + "pattern", function() { + ctrl.privateFn.instantiateHostPatterns("foo:bar"); + expect($scope.limit).to.equal("foo:bar"); + }); + + it("should set the providedHostPatterns variable to the provided host " + + "pattern so it is accesible on form reset", function() { + ctrl.privateFn.instantiateHostPatterns("foo:bar"); + expect($scope.providedHostPatterns).to.equal("foo:bar"); + }); + + it("should remove the hostPattern from rootScope after it has been " + + "utilized", function() { + $rootScope.hostPatterns = "foo"; + expect($rootScope.hostPatterns).to.exist; + ctrl.privateFn.instantiateHostPatterns("foo"); + expect($rootScope.hostPatterns).to.not.exist; + }); + }); +}); diff --git a/awx/ui/client/tests/support/node/index.js b/awx/ui/client/tests/support/node/index.js index 91ef218cf6..21d5639eb5 100644 --- a/awx/ui/client/tests/support/node/index.js +++ b/awx/ui/client/tests/support/node/index.js @@ -11,8 +11,10 @@ require('./setup/jsdom'); require('./setup/mocha'); require('./setup/jquery'); + require('./setup/jquery-ui'); require('./setup/angular'); require('./setup/angular-mocks'); + require('./setup/angular-route'); require('./setup/angular-templates'); require('./setup/sinon'); require('./setup/chai'); diff --git a/awx/ui/client/tests/support/node/setup/angular-route.js b/awx/ui/client/tests/support/node/setup/angular-route.js new file mode 100644 index 0000000000..b2fddfe1a1 --- /dev/null +++ b/awx/ui/client/tests/support/node/setup/angular-route.js @@ -0,0 +1 @@ +require('angular-route/angular-route'); diff --git a/awx/ui/client/tests/support/node/setup/jquery-ui.js b/awx/ui/client/tests/support/node/setup/jquery-ui.js new file mode 100644 index 0000000000..d94865084b --- /dev/null +++ b/awx/ui/client/tests/support/node/setup/jquery-ui.js @@ -0,0 +1,4 @@ +var exportGlobal = require('../export-global'); +global.document = window.document; +global.navigator = window.navigator; +require('jquery-ui/jquery-ui');