diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 5580df4119..96b24aeb69 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -203,7 +203,7 @@ table, tbody { .List-buttonDefault { background-color: @btn-bg; color: @btn-txt; - border-color: @btn-bord; + border-color: @b7grey; } .List-buttonDefault:hover, @@ -212,6 +212,11 @@ table, tbody { color: @btn-txt; } +.List-buttonDefault[disabled] { + color: @d7grey; + border-color: @d7grey; +} + .List-searchDropdown { border-top-left-radius: 5px!important; border-bottom-left-radius: 5px!important; diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js new file mode 100644 index 0000000000..4b8e25c051 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js @@ -0,0 +1,308 @@ +/************************************************* + * Copyright (c) 2016 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, $stateParams, + $state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm, + GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, + KindChange, Wait, ParseTypeChange) { + + 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; + + var id = $stateParams.inventory_id, + hostPattern = $stateParams.pattern; + + // 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 urls = privateFn.setAvailableUrls(); + + // 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); + + CreateSelect2({ + element: '#adhoc_module_name', + multiple: false + }); + + CreateSelect2({ + element: '#adhoc_verbosity', + multiple: false + }); + + Wait('stop'); + } + } + }; + + // 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; + }; + + // call helpers to initialize lookup and select fields through get + // requests + privateFn.initializeFields = function(machineCredentialUrl, adhocUrl) { + + // 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: 'add', 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; + + // 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); + + // init codemirror + $scope.extra_vars = '---'; + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + ParseTypeChange({ scope: $scope, field_id: 'adhoc_extra_vars' , variable: "extra_vars"}); + + $scope.formCancel = function(){ + $state.go('inventoryManage'); + }; + + // 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') + $stateParams.inventory_id + + '/ad_hoc_commands/', fld, data={}, html; + + html = '
'; + + // stub the payload with defaults from DRF + data = { + "job_type": "run", + "limit": "", + "credential": "", + "module_name": "command", + "module_args": "", + "forks": 0, + "verbosity": 0, + "extra_vars": "", + "privilege_escalation": "" + }; + + GenerateForm.clearApiErrors($scope); + + // populate data with the relevant form values + for (fld in adhocForm.fields) { + if (adhocForm.fields[fld].type === 'select') { + data[fld] = $scope[fld].value; + } else if ($scope[fld]) { + 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') + + $stateParams.inventory_id + '/ad_hoc_commands/'); + Rest.post(data) + .success(function (data) { + Wait('stop'); + $state.go('adHocJobStdout', {id: 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', '$stateParams', + '$state', 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'CreateSelect2', + 'adhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', + 'GetChoices', 'KindChange', 'Wait', 'ParseTypeChange', + adhocController]; diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.form.js b/awx/ui/client/src/inventories/adhoc/adhoc.form.js new file mode 100644 index 0000000000..c58068deb6 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.form.js @@ -0,0 +1,159 @@ +/************************************************* + * 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 ['i18n', function(i18n) { + return { + addTitle: '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()', + required: 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', + autocomplete: false + }, + limit: { + label: 'Limit', + type: 'text', + + 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: 'Limit', + dataPlacement: 'right', + dataContainer: 'body' + }, + credential: { + label: 'Machine Credential', + type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + sourceModel: 'credential', + sourceField: 'name', + 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: { + reqExpression: 'credRequired', + init: 'false' + } + }, + become_enabled: { + label: 'Enable Privilege Escalation', + type: 'checkbox', + + 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', + required: true, + awPopOver:'

These are the verbosity levels for standard ' + + 'out of the command run that are supported.', + dataTitle: 'Verbosity', + dataPlacement: 'right', + dataContainer: 'body', + "default": 1 + }, + forks: { + label: 'Forks', + id: 'forks-number', + type: 'number', + integer: true, + min: 0, + spinner: true, + "default": 0, + required: 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" + }, + extra_vars: { + label: i18n._('Extra Variables'), + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + "default": "---", + column: 2, + awPopOver: "

" + i18n.sprintf(i18n._("Pass extra command line variables. This is the %s or %s command line parameter " + + "for %s. Provide key/value pairs using either YAML or JSON."), '-e', '--extra-vars', 'ansible') + "

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n", + dataTitle: i18n._('Extra Variables'), + dataPlacement: 'right', + dataContainer: "body" + } + }, + buttons: { + reset: { + ngClick: 'formReset()', + ngDisabled: true, + label: 'Reset', + 'class': 'btn btn-sm Form-cancelButton' + }, + launch: { + label: 'Save', + ngClick: 'launchJob()', + ngDisabled: true, + 'class': 'btn btn-sm List-buttonSubmit launchButton' + } + }, + + related: {} + }; +}]; diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.partial.html b/awx/ui/client/src/inventories/adhoc/adhoc.partial.html new file mode 100644 index 0000000000..7d2a014836 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.partial.html @@ -0,0 +1 @@ +
diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.route.js b/awx/ui/client/src/inventories/adhoc/adhoc.route.js new file mode 100644 index 0000000000..392de3965e --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.route.js @@ -0,0 +1,28 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import {templateUrl} from '../../shared/template-url/template-url.factory'; + import { N_ } from '../../i18n'; + +export default { + url: '/adhoc', + params:{ + pattern: { + value: 'all', + squash: true + } + }, + name: 'inventories.edit.adhoc', + views: { + 'adhocForm@inventories': { + templateUrl: templateUrl('inventories/adhoc/adhoc'), + controller: 'adhocController' + } + }, + ncyBreadcrumb: { + label: N_("RUN COMMAND") + } +}; diff --git a/awx/ui/client/src/inventories/adhoc/main.js b/awx/ui/client/src/inventories/adhoc/main.js new file mode 100644 index 0000000000..1931e8f1d1 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/main.js @@ -0,0 +1,7 @@ +import adhocController from './adhoc.controller'; +import form from './adhoc.form'; + +export default + angular.module('adhoc', []) + .controller('adhocController', adhocController) + .factory('adhocForm', form); diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index ed95f5a394..76c847c01b 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -48,9 +48,8 @@ export default { }, launch: { mode: 'all', - // $scope.$parent is governed by InventoryManageController, - ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', - ngClick: '$parent.setAdhocPattern()', + ngDisabled: '!groupsSelected', + ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", dataTipWatch: "adhocCommandTooltip", actionClass: 'btn List-buttonDefault', diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 2889b745f9..003e9ef65c 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -16,14 +16,14 @@ init(); function init(){ - $scope.inventory_id = $stateParams.inventory_id; - $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; - $scope.canAdd = false; + $scope.inventory_id = $stateParams.inventory_id; + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + $scope.canAdd = false; - rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); // Search init $scope.list = list; @@ -43,6 +43,22 @@ $scope.inventory_id = $stateParams.inventory_id; _.forEach($scope[list.name], buildStatusIndicators); + $scope.$on('selectedOrDeselected', function(e, value) { + let item = value.value; + + if (value.isSelected) { + if(!$scope.groupsSelected) { + $scope.groupsSelected = []; + } + $scope.groupsSelected.push(item); + } else { + _.remove($scope.groupsSelected, { id: item.id }); + if($scope.groupsSelected.length === 0) { + $scope.groupsSelected = null; + } + } + }); + } function buildStatusIndicators(group){ @@ -195,11 +211,6 @@ groupsArr.push(id); $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); }; - // $scope.$parent governed by InventoryManageController, for unified multiSelect options - $scope.$on('multiSelectList.selectionChanged', (event, selection) => { - $scope.$parent.groupsSelected = selection.length > 0 ? true : false; - $scope.$parent.groupsSelectedItems = selection.selectedItems; - }); $scope.copyMoveGroup = function(id){ $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); @@ -221,4 +232,13 @@ cleanUpStateChangeListener(); }); + $scope.setAdhocPattern = function(){ + var pattern = _($scope.groupsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 67007647b5..7f7425ee84 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -221,4 +221,13 @@ cleanUpStateChangeListener(); }); + $scope.setAdhocPattern = function(){ + var pattern = _($scope.groupsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js index 1e28c83454..ba958b0175 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -50,9 +50,8 @@ export default { }, launch: { mode: 'all', - // $scope.$parent is governed by InventoryManageController, - ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', - ngClick: '$parent.setAdhocPattern()', + ngDisabled: '!groupsSelected', + ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", dataTipWatch: "adhocCommandTooltip", actionClass: 'btn List-buttonDefault', diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 032a4ec352..767cd197fa 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,4 +1,5 @@
+
diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 0d9c4c3f84..e4dc100382 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -4,6 +4,7 @@ * All Rights Reserved *************************************************/ +import adhoc from './adhoc/main'; import host from './hosts/main'; import group from './groups/main'; import sources from './sources/main'; @@ -17,8 +18,10 @@ import { N_ } from '../i18n'; import InventoryList from './inventory.list'; import InventoryForm from './inventory.form'; import InventoryManageService from './inventory-manage.service'; +import adHocRoute from './adhoc/adhoc.route'; export default angular.module('inventory', [ + adhoc.name, host.name, group.name, sources.name, @@ -81,6 +84,60 @@ angular.module('inventory', [ } }); + let adhocCredentialLookup = { + searchPrefix: 'credential', + name: 'inventories.edit.adhoc.credential', + url: '/credential', + data: { + formChildState: true + }, + params: { + credential_search: { + value: { + page_size: '5' + }, + squash: true, + dynamic: true + } + }, + ncyBreadcrumb: { + skip: true + }, + views: { + 'related': { + templateProvider: function(ListDefinition, generateList) { + let list_html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + return `${list_html}`; + + } + } + }, + resolve: { + ListDefinition: ['CredentialList', function(CredentialList) { + let list = _.cloneDeep(CredentialList); + list.lookupConfirmText = 'SELECT'; + return list; + }], + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', + (list, qs, $stateParams, GetBasePath) => { + let path = GetBasePath(list.name) || GetBasePath(list.basePath); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#form-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + } + }; + return Promise.all([ basicInventoryAdd, basicInventoryEdit, @@ -121,7 +178,9 @@ angular.module('inventory', [ } ] } - }) + }), + stateExtender.buildDefinition(adHocRoute), + stateExtender.buildDefinition(adhocCredentialLookup) ]) }; }); diff --git a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js index f89b6c6606..e9eaea0973 100644 --- a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -143,8 +143,17 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa var hostIds = _.map($scope.hostsSelected, (host) => host.id); $state.go('systemTracking', { inventoryId: $state.params.inventory_id, - hosts: $scope.$parent.hostsSelectedItems, + hosts: $scope.hostsSelected, hostIds: hostIds }); }; + + $scope.setAdhocPattern = function(){ + var pattern = _($scope.hostsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; }]; diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js index 3b4140e74a..e936e3c590 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -83,6 +83,19 @@ export default { }, actions: { + launch: { + mode: 'all', + ngDisabled: '!hostsSelected', + ngClick: 'setAdhocPattern()', + awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", + dataTipWatch: "adhocCommandTooltip", + actionClass: 'btn List-buttonDefault', + buttonContent: 'RUN COMMANDS', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + // TODO: we don't always want to show this + ngShow: true + }, system_tracking: { buttonContent: 'System Tracking', ngClick: 'systemTracking()',