diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 52b9d1e524..4a7471c060 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -1450,7 +1450,6 @@ input[type="checkbox"].checkbox-no-label { margin-bottom: 0; } - #inventories_table i[class*="icon-job-"], #home_groups_table i[class*="icon-job-"] { margin-left: 5px; } diff --git a/awx/ui/client/legacy-styles/forms.less b/awx/ui/client/legacy-styles/forms.less index dcf9154154..ce7043c7e6 100644 --- a/awx/ui/client/legacy-styles/forms.less +++ b/awx/ui/client/legacy-styles/forms.less @@ -130,6 +130,10 @@ .noselect; } +.Form-tab--notitle { + margin-bottom: 0px; +} + .Form-tab:hover { color: @btn-txt; background-color: @btn-bg-hov; diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 7e36f610cc..96b24aeb69 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -162,10 +162,16 @@ table, tbody { // float: right; } +.List-actionHolder--leftAlign { + width: 50%; + margin-left: 50%; + justify-content: flex-start; +} + .List-actions { display: flex; - margin-bottom: -32px; margin-top: 18px; + margin-bottom: -34px; } .List-auxAction { @@ -197,7 +203,7 @@ table, tbody { .List-buttonDefault { background-color: @btn-bg; color: @btn-txt; - border-color: @btn-bord; + border-color: @b7grey; } .List-buttonDefault:hover, @@ -206,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/legacy-styles/main-layout.less b/awx/ui/client/legacy-styles/main-layout.less index ac74aa8e22..cb6884b798 100644 --- a/awx/ui/client/legacy-styles/main-layout.less +++ b/awx/ui/client/legacy-styles/main-layout.less @@ -93,6 +93,10 @@ body { margin-top: 20px; } +.Panel-hidden { + display: none; +} + .btn{ text-transform: uppercase; } diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index 16f40901db..76e00ce8b7 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -75,9 +75,6 @@ export default [ if(key === "AD_HOC_COMMANDS"){ $scope[key] = data[key].toString(); } - else if(key === "AUTH_LDAP_USER_SEARCH" || key === "AUTH_LDAP_GROUP_SEARCH"){ - $scope[key] = JSON.stringify(data[key]); - } else { $scope[key] = ConfigurationUtils.arrayToList(data[key], key); } @@ -356,38 +353,27 @@ export default [ clearApiErrors(); _.each(keys, function(key) { if($scope.configDataResolve[key].type === 'choice' || multiselectDropdowns.indexOf(key) !== -1) { - - // Handle AD_HOC_COMMANDS - if(multiselectDropdowns.indexOf(key) !== -1) { - let newModules = $("#configuration_jobs_template_AD_HOC_COMMANDS > option") - .filter("[data-select2-tag=true]") - .map((i, val) => ({value: $(val).text()})); - newModules.each(function(i, val) { - $scope[key].push(val); - }); - - payload[key] = ConfigurationUtils.listToArray(_.map($scope[key], 'value').join(',')); - } - //Parse dropdowns and dropdowns labeled as lists - else if($scope[key] === null) { + if($scope[key] === null) { payload[key] = null; } else if($scope[key][0] && $scope[key][0].value !== undefined) { - payload[key] = _.map($scope[key], 'value').join(','); + if(multiselectDropdowns.indexOf(key) !== -1) { + // Handle AD_HOC_COMMANDS + payload[key] = ConfigurationUtils.listToArray(_.map($scope[key], 'value').join(',')); + } else { + payload[key] = _.map($scope[key], 'value').join(','); + } } else { - payload[key] = $scope[key].value; + if(multiselectDropdowns.indexOf(key) !== -1) { + // Default AD_HOC_COMMANDS to an empty list + payload[key] = $scope[key].value || []; + } else { + payload[key] = $scope[key].value; + } } } else if($scope.configDataResolve[key].type === 'list' && $scope[key] !== null) { - - if(key === "AUTH_LDAP_USER_SEARCH" || key === "AUTH_LDAP_GROUP_SEARCH"){ - payload[key] = $scope[key] === "{}" ? [] : ToJSON($scope.parseType, - $scope[key]); - } - else { - // Parse lists - payload[key] = ConfigurationUtils.listToArray($scope[key], key); - } - + // Parse lists + payload[key] = ConfigurationUtils.listToArray($scope[key], key); } else if($scope.configDataResolve[key].type === 'nested object') { if($scope[key] === '') { diff --git a/awx/ui/client/src/inventories/add/inventory-add.controller.js b/awx/ui/client/src/inventories/add/inventory-add.controller.js index 4287cfb404..7fb71a9edb 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -49,9 +49,9 @@ function InventoriesAdd($scope, $location, $scope.parseType = 'yaml'; ParseTypeChange({ scope: $scope, - variable: 'variables', + variable: 'inventory_variables', parse_variable: 'parseType', - field_id: 'inventory_variables' + field_id: 'inventory_inventory_variables' }); } @@ -59,9 +59,7 @@ function InventoriesAdd($scope, $location, $scope.formSave = function() { Wait('start'); try { - var fld, json_data, data; - - json_data = ToJSON($scope.parseType, $scope.variables, true); + var fld, data; data = {}; for (fld in form.fields) { @@ -77,7 +75,7 @@ function InventoriesAdd($scope, $location, .success(function(data) { var inventory_id = data.id; Wait('stop'); - $location.path('/inventories/' + inventory_id + '/manage'); + $state.go('inventories.edit', {inventory_id: inventory_id}, {reload: true}); }) .error(function(data, status) { ProcessErrors($scope, data, status, form, { diff --git a/awx/ui/client/src/inventories/add/main.js b/awx/ui/client/src/inventories/add/main.js index fc9c82e6fd..2e477aa96c 100644 --- a/awx/ui/client/src/inventories/add/main.js +++ b/awx/ui/client/src/inventories/add/main.js @@ -7,5 +7,5 @@ import controller from './inventory-add.controller'; export default -angular.module('inventoryAdd', []) +angular.module('InventoryAdd', []) .controller('InventoryAddController', controller); diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js similarity index 99% rename from awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js rename to awx/ui/client/src/inventories/adhoc/adhoc.controller.js index 4b8e25c051..fcff3cb3a1 100644 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js +++ b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js @@ -169,7 +169,7 @@ function adhocController($q, $scope, $stateParams, ParseTypeChange({ scope: $scope, field_id: 'adhoc_extra_vars' , variable: "extra_vars"}); $scope.formCancel = function(){ - $state.go('inventoryManage'); + $state.go('^'); }; // remove all data input into the form and reset the form back to defaults diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js b/awx/ui/client/src/inventories/adhoc/adhoc.form.js similarity index 100% rename from awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js rename to awx/ui/client/src/inventories/adhoc/adhoc.form.js 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/manage/adhoc/adhoc.route.js b/awx/ui/client/src/inventories/adhoc/adhoc.route.js similarity index 62% rename from awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js rename to awx/ui/client/src/inventories/adhoc/adhoc.route.js index 05a8d6dcad..392de3965e 100644 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js +++ b/awx/ui/client/src/inventories/adhoc/adhoc.route.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ - import {templateUrl} from '../../../shared/template-url/template-url.factory'; - import { N_ } from '../../../i18n'; + import {templateUrl} from '../../shared/template-url/template-url.factory'; + import { N_ } from '../../i18n'; export default { url: '/adhoc', @@ -15,10 +15,10 @@ export default { squash: true } }, - name: 'inventoryManage.adhoc', + name: 'inventories.edit.adhoc', views: { - 'form@inventoryManage': { - templateUrl: templateUrl('inventories/manage/adhoc/adhoc'), + 'adhocForm@inventories': { + templateUrl: templateUrl('inventories/adhoc/adhoc'), controller: 'adhocController' } }, diff --git a/awx/ui/client/src/inventories/manage/adhoc/main.js b/awx/ui/client/src/inventories/adhoc/main.js similarity index 100% rename from awx/ui/client/src/inventories/manage/adhoc/main.js rename to awx/ui/client/src/inventories/adhoc/main.js diff --git a/awx/ui/client/src/inventories/ansible_facts/ansible_facts.controller.js b/awx/ui/client/src/inventories/ansible_facts/ansible_facts.controller.js new file mode 100644 index 0000000000..0fea8b1772 --- /dev/null +++ b/awx/ui/client/src/inventories/ansible_facts/ansible_facts.controller.js @@ -0,0 +1,27 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +function AnsibleFacts($scope, Facts, ParseTypeChange, ParseVariableString) { + + function init() { + $scope.facts = ParseVariableString(Facts.data); + + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'facts', + parse_variable: 'parseType', + field_id: 'host_facts', + readOnly: true + }); + } + + init(); + +} + +export default ['$scope', 'Facts', 'ParseTypeChange', 'ParseVariableString', AnsibleFacts +]; diff --git a/awx/ui/client/src/inventories/ansible_facts/ansible_facts.partial.html b/awx/ui/client/src/inventories/ansible_facts/ansible_facts.partial.html new file mode 100644 index 0000000000..3a8cbce18b --- /dev/null +++ b/awx/ui/client/src/inventories/ansible_facts/ansible_facts.partial.html @@ -0,0 +1,10 @@ + diff --git a/awx/ui/client/src/inventories/ansible_facts/main.js b/awx/ui/client/src/inventories/ansible_facts/main.js new file mode 100644 index 0000000000..dffa3f9c69 --- /dev/null +++ b/awx/ui/client/src/inventories/ansible_facts/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './ansible_facts.controller'; + +export default +angular.module('AnsibleFacts', []) + .controller('AnsibleFactsController', controller); diff --git a/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js b/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js new file mode 100644 index 0000000000..df06c8ede9 --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js @@ -0,0 +1,80 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import JobsListController from '../../jobs/jobs-list.controller'; +export default ['InventoryCompletedJobsList', '$stateExtender', 'templateUrl', '$injector', + function(InventoryCompletedJobsList, $stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + // searchPrefix: `${list.iterator}`, + name: `${formStateDefinition.name}.${list.iterator}s`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + completed_job_search: { + value: { + or__job__inventory: '', + or__adhoccommand__inventory: '', + or__inventoryupdate__inventory_source__inventory: '' + }, + squash: '' + } + }, + views: { + 'related': { + templateProvider: function(FormDefinition, GenerateForm) { + let html = GenerateForm.buildCollection({ + mode: 'edit', + related: `${list.iterator}s`, + form: typeof(FormDefinition) === 'function' ? + FormDefinition() : FormDefinition + }); + return html; + }, + controller: JobsListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + + $stateParams[`${list.iterator}_search`].or__job__inventory = $stateParams.inventory_id; + $stateParams[`${list.iterator}_search`].or__adhoccommand__inventory = $stateParams.inventory_id; + $stateParams[`${list.iterator}_search`].or__inventoryupdate__inventory_source__inventory = $stateParams.inventory_id; + + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + if (field.search) { + state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + } + + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js b/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js new file mode 100644 index 0000000000..53d91793ba --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js @@ -0,0 +1,88 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +export default ['i18n', function(i18n) { + return { + // These tooltip fields are consumed to build disabled related tabs tooltips in the form > add view + awToolTip: i18n._('Please save and run a job to view'), + dataPlacement: 'top', + name: 'completed_jobs', + basePath: 'unified_jobs', + iterator: 'completed_job', + search: { + "or__job__inventory": '' + }, + editTitle: i18n._('COMPLETED JOBS'), + index: false, + hover: true, + well: false, + emptyListText: i18n._('No completed jobs'), + + fields: { + status: { + label: '', + columnClass: 'List-staticColumn--smallStatus', + awToolTip: "{{ completed_job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ completed_job.status_popover_title }}", + icon: 'icon-job-{{ completed_job.status }}', + iconOnly: true, + ngClick:"viewjobResults(completed_job)", + }, + id: { + label: 'ID', + ngClick:"viewjobResults(completed_job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ completed_job.status_tip }}", + dataPlacement: 'top' + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6', + ngClick: "viewjobResults(completed_job)", + awToolTip: "{{ completed_job.name | sanitize }}", + dataPlacement: 'top' + }, + type: { + label: i18n._('Type'), + ngBind: 'completed_job.type_label', + link: false, + columnClass: "col-lg-2 col-md-2 hidden-sm hidden-xs", + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-3 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true + } + }, + + actions: { }, + + fieldActions: { + + columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', + + submit: { + icon: 'icon-rocket', + mode: 'all', + ngClick: 'relaunchJob($event, completed_job.id)', + awToolTip: i18n._('Relaunch using the same parameters'), + dataPlacement: 'top', + ngShow: "!completed_job.type == 'system_job' || completed_job.summary_fields.user_capabilities.start" + }, + "delete": { + mode: 'all', + ngClick: 'deleteJob(completed_job.id)', + awToolTip: i18n._('Delete the job'), + dataPlacement: 'top', + ngShow: 'completed_job.summary_fields.user_capabilities.delete' + } + } + };}]; diff --git a/awx/ui/client/src/inventories/completed_jobs/main.js b/awx/ui/client/src/inventories/completed_jobs/main.js new file mode 100644 index 0000000000..b34f919a78 --- /dev/null +++ b/awx/ui/client/src/inventories/completed_jobs/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import list from './completed_jobs.list'; +import buildInventoryCompletedJobsState from './build-inventory-completed-jobs-state.factory'; + +export default + angular.module('inventoryCompletedJobs', []) + .factory('InventoryCompletedJobsList', list) + .factory('buildInventoryCompletedJobsState', buildInventoryCompletedJobsState); diff --git a/awx/ui/client/src/inventories/copy-move/copy-move-groups.controller.js b/awx/ui/client/src/inventories/copy-move/copy-move-groups.controller.js new file mode 100644 index 0000000000..2ebf91cc93 --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move-groups.controller.js @@ -0,0 +1,72 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default + ['$scope', '$state', '$stateParams', 'GroupManageService', 'CopyMoveGroupList', 'group', 'Dataset', + function($scope, $state, $stateParams, GroupManageService, CopyMoveGroupList, group, Dataset){ + var list = CopyMoveGroupList; + + $scope.item = group; + $scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy'; + $scope.toggle_row = function(id){ + // toggle off anything else currently selected + _.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;}); + // yoink the currently selected thing + $scope.selected = _.find($scope.groups, (item) => {return item.id === id;}); + }; + $scope.formCancel = function(){ + $state.go('^'); + }; + $scope.formSave = function(){ + switch($scope.submitMode) { + case 'copy': + GroupManageService.associateGroup(group, $scope.selected.id).then(() => $state.go('^', null, {reload: true})); + break; + case 'move': + switch($scope.targetRootGroup){ + case true: + // disassociating group will bubble it to the root group level + GroupManageService.disassociateGroup(group.id, _.last($stateParams.group)).then(() => $state.go('^', null, {reload: true})); + break; + default: + // at the root group level, no dissassociation is needed + if (!$stateParams.group){ + GroupManageService.associateGroup(group, $scope.selected.id).then(() => $state.go('^', null, {reload: true})); + } + else{ + // unsure if orphaned resources get garbage collected, safe bet is to associate before disassociate + GroupManageService.associateGroup(group, $scope.selected.id).then(() => { + GroupManageService.disassociateGroup(group.id, _.last($stateParams.group)) + .then(() => $state.go('^', null, {reload: true})); + }); + } + break; + } + } + }; + $scope.toggleTargetRootGroup = function(){ + $scope.selected = !$scope.selected; + // cannot perform copy operations to root group level + $scope.submitMode = 'move'; + // toggle off anything currently selected in the list, for clarity + _.forEach($scope.groups, (item) => {item.checked = null;}); + // disable list selections + $('#copyMove-list :input').each((idx, el) => { + $(el).prop('disabled', (idx, value) => !value); + }); + }; + + function init(){ + $scope.atRootLevel = $stateParams.group ? false : true; + + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + init(); + }]; diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js b/awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js similarity index 88% rename from awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js rename to awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js index 5a98b24591..a207fd8e06 100644 --- a/awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js +++ b/awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js @@ -20,5 +20,5 @@ export default { label: 'Target Group Name' } }, - basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/groups' + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/groups' }; diff --git a/awx/ui/client/src/inventories/copy-move/copy-move-hosts.controller.js b/awx/ui/client/src/inventories/copy-move/copy-move-hosts.controller.js new file mode 100644 index 0000000000..8e347dc2c8 --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move-hosts.controller.js @@ -0,0 +1,50 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default + ['$scope', '$state', '$stateParams', 'HostManageService', 'CopyMoveGroupList', 'host', 'Dataset', + function($scope, $state, $stateParams, HostManageService, CopyMoveGroupList, host, Dataset){ + var list = CopyMoveGroupList; + + $scope.item = host; + $scope.submitMode = 'copy'; + $scope.toggle_row = function(id){ + // toggle off anything else currently selected + _.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;}); + // yoink the currently selected thing + $scope.selected = _.find($scope.groups, (item) => {return item.id === id;}); + }; + $scope.formCancel = function(){ + $state.go('^'); + }; + $scope.formSave = function(){ + switch($scope.submitMode) { + case 'copy': + HostManageService.associateGroup(host, $scope.selected.id).then(() => $state.go('^')); + break; + case 'move': + // at the root group level, no dissassociation is needed + if (!$stateParams.group){ + HostManageService.associateGroup(host, $scope.selected.id).then(() => $state.go('^', null, {reload: true})); + } + else{ + HostManageService.associateGroup(host, $scope.selected.id).then(() => { + HostManageService.disassociateGroup(host, _.last($stateParams.group)) + .then(() => $state.go('^', null, {reload: true})); + }); + } + break; + } + }; + var init = function(){ + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + }; + init(); + }]; diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less b/awx/ui/client/src/inventories/copy-move/copy-move.block.less similarity index 100% rename from awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less rename to awx/ui/client/src/inventories/copy-move/copy-move.block.less diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html b/awx/ui/client/src/inventories/copy-move/copy-move.partial.html similarity index 100% rename from awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html rename to awx/ui/client/src/inventories/copy-move/copy-move.partial.html diff --git a/awx/ui/client/src/inventories/copy-move/copy-move.route.js b/awx/ui/client/src/inventories/copy-move/copy-move.route.js new file mode 100644 index 0000000000..e68c6de633 --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move.route.js @@ -0,0 +1,98 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ +import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { N_ } from '../../i18n'; + +import CopyMoveGroupsController from './copy-move-groups.controller'; +import CopyMoveHostsController from './copy-move-hosts.controller'; + +var copyMoveGroupRoute = { + name: 'inventories.edit.groups.copyMoveGroup', + url: '/copy-move-group/{group_id:int}', + searchPrefix: 'copy', + data: { + group_id: 'group_id', + }, + params: { + copy_search: { + value: { + not__id__in: null + }, + dynamic: true, + squash: '' + } + }, + ncyBreadcrumb: { + label: N_("COPY OR MOVE") + " {{item.name}}" + }, + resolve: { + Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', 'group', + function(list, qs, $stateParams, GetBasePath, group) { + $stateParams.copy_search.not__id__in = ($stateParams.group && $stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id.toString()); + let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/'; + return qs.search(path, $stateParams.copy_search); + } + ], + group: ['GroupManageService', '$stateParams', function(GroupManageService, $stateParams){ + return GroupManageService.get({id: $stateParams.group_id}).then(res => res.data.results[0]); + }] + }, + views: { + 'copyMove@inventories' : { + controller: CopyMoveGroupsController, + templateUrl: templateUrl('inventories/copy-move/copy-move'), + }, + 'copyMoveList@inventories.edit.groups.copyMoveGroup': { + templateProvider: function(CopyMoveGroupList, generateList) { + let html = generateList.build({ + list: CopyMoveGroupList, + mode: 'lookup', + input_type: 'radio' + }); + return html; + } + } + } +}; +var copyMoveHostRoute = { + name: 'inventories.edit.hosts.copyMoveHost', + url: '/copy-move-host/{host_id}', + searchPrefix: 'copy', + ncyBreadcrumb: { + label: N_("COPY OR MOVE") + " {{item.name}}" + }, + resolve: { + Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/'; + return qs.search(path, $stateParams.copy_search); + } + ], + host: ['HostManageService', '$stateParams', function(HostManageService, $stateParams){ + return HostManageService.get({id: $stateParams.host_id}).then(res => res.data.results[0]); + }] + }, + views: { + 'copyMove@inventories': { + templateUrl: templateUrl('inventories/copy-move/copy-move'), + controller: CopyMoveHostsController, + }, + 'copyMoveList@inventories.edit.hosts.copyMoveHost': { + templateProvider: function(CopyMoveGroupList, generateList, $stateParams, GetBasePath) { + let list = CopyMoveGroupList; + list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/'; + let html = generateList.build({ + list: CopyMoveGroupList, + mode: 'lookup', + input_type: 'radio' + }); + return html; + } + } + } +}; + +export {copyMoveGroupRoute, copyMoveHostRoute}; diff --git a/awx/ui/client/src/inventories/manage/copy-move/main.js b/awx/ui/client/src/inventories/copy-move/main.js similarity index 100% rename from awx/ui/client/src/inventories/manage/copy-move/main.js rename to awx/ui/client/src/inventories/copy-move/main.js diff --git a/awx/ui/client/src/inventories/edit/inventory-edit.controller.js b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js index 78d35d311f..0558be203a 100644 --- a/awx/ui/client/src/inventories/edit/inventory-edit.controller.js +++ b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js @@ -20,7 +20,7 @@ function InventoriesEdit($scope, $location, form = InventoryForm, inventory_id = $stateParams.inventory_id, master = {}, - fld, json_data, data; + fld, data; ClearScope(); init(); @@ -45,9 +45,9 @@ function InventoriesEdit($scope, $location, .success(function(data) { var fld; for (fld in form.fields) { - if (fld === 'variables') { - $scope.variables = ParseVariableString(data.variables); - master.variables = $scope.variables; + if (fld === 'inventory_variables') { + $scope.inventory_variables = ParseVariableString(data.variables); + master.inventory_variables = $scope.variables; } else if (fld === 'inventory_name') { $scope[fld] = data.name; master[fld] = $scope[fld]; @@ -71,9 +71,9 @@ function InventoriesEdit($scope, $location, $scope.parseType = 'yaml'; ParseTypeChange({ scope: $scope, - variable: 'variables', + variable: 'inventory_variables', parse_variable: 'parseType', - field_id: 'inventory_variables' + field_id: 'inventory_inventory_variables' }); OrgAdminLookup.checkForAdminAccess({organization: data.organization}) @@ -83,8 +83,6 @@ function InventoriesEdit($scope, $location, $scope.inventory_obj = data; $scope.name = data.name; - - $scope.$emit('inventoryLoaded'); }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { @@ -96,9 +94,6 @@ function InventoriesEdit($scope, $location, $scope.formSave = function() { Wait('start'); - // Make sure we have valid variable data - json_data = ToJSON($scope.parseType, $scope.variables); - data = {}; for (fld in form.fields) { if (form.fields[fld].realName) { @@ -122,10 +117,6 @@ function InventoriesEdit($scope, $location, }); }; - $scope.manageInventory = function() { - $location.path($location.path() + '/manage'); - }; - $scope.formCancel = function() { $state.go('inventories'); }; diff --git a/awx/ui/client/src/inventories/edit/main.js b/awx/ui/client/src/inventories/edit/main.js index f8792fe442..130a5e8b4b 100644 --- a/awx/ui/client/src/inventories/edit/main.js +++ b/awx/ui/client/src/inventories/edit/main.js @@ -7,5 +7,5 @@ import controller from './inventory-edit.controller'; export default - angular.module('inventoryEdit', []) + angular.module('InventoryEdit', []) .controller('InventoryEditController', controller); diff --git a/awx/ui/client/src/inventories/groups/add/build-groups-add-state.factory.js b/awx/ui/client/src/inventories/groups/add/build-groups-add-state.factory.js new file mode 100644 index 0000000000..e44130ce22 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/add/build-groups-add-state.factory.js @@ -0,0 +1,46 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import GroupAddController from './groups-add.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.add`, + url: `/add`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'groupForm@inventories': { + templateProvider: function(GenerateForm, GroupForm) { + let form = GroupForm; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: GroupAddController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js new file mode 100644 index 0000000000..ae37692ba1 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -0,0 +1,62 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'GroupForm', + 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', + 'rbacUiControlService', 'ToJSON', + function($state, $stateParams, $scope, GroupForm, ParseTypeChange, + GenerateForm, inventoryData, GroupManageService, GetChoices, + GetBasePath, CreateSelect2, rbacUiControlService, + ToJSON) { + + let form = GroupForm; + init(); + + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + field_id: 'group_variables', + variable: 'variables', + }); + } + + $scope.formCancel = function() { + $state.go('^'); + }; + + $scope.formSave = function() { + var json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + var group = { + variables: json_data, + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id + }; + + GroupManageService.post(group).then(res => { + if ($stateParams.group_id) { + return GroupManageService.associateGroup(res.data, $stateParams.group_id) + .then(() => $state.go('^', null, { reload: true })); + } else { + $state.go('^.edit', { group_id: res.data.id }, { reload: true }); + } + }); + + }; + } +]; diff --git a/awx/ui/client/src/inventories/groups/add/main.js b/awx/ui/client/src/inventories/groups/add/main.js new file mode 100644 index 0000000000..4cbb4e5c66 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/add/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildGroupAddState from './build-groups-add-state.factory'; +import controller from './groups-add.controller'; + +export default +angular.module('groupAdd', []) + .factory('buildGroupsAddState', buildGroupAddState) + .controller('GroupAddController', controller); diff --git a/awx/ui/client/src/inventories/groups/edit/build-groups-edit-state.factory.js b/awx/ui/client/src/inventories/groups/edit/build-groups-edit-state.factory.js new file mode 100644 index 0000000000..3687face76 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/edit/build-groups-edit-state.factory.js @@ -0,0 +1,93 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import GroupEditController from './groups-edit.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + 'stateDefinitions','GroupForm','nestedGroupListState', + 'nestedHostsListState', 'buildHostAddState', 'buildHostEditState', + 'nestedGroupAddState', + function($stateExtender, templateUrl, $injector, stateDefinitions, GroupForm, + nestedGroupListState, nestedHostsListState, buildHostAddState, + buildHostEditState, nestedGroupAddState){ + var val = function(field, formStateDefinition, params) { + let state, states = [], + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.edit`, + url: `/edit/:group_id`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'groupForm@inventories': { + templateProvider: function(GenerateForm, GroupForm) { + let form = GroupForm; + return GenerateForm.buildHTML(form, { + mode: 'edit', + related: false + }); + }, + controller: GroupEditController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }], + groupData: ['$stateParams', 'GroupManageService', function($stateParams, GroupManageService) { + return GroupManageService.get({ id: $stateParams.group_id }).then(res => res.data.results[0]); + }] + } + }; + state = $stateExtender.buildDefinition(stateConfig); + + let relatedGroupListState = nestedGroupListState(GroupForm.related.nested_groups, state, params); + let relatedGroupsAddState = nestedGroupAddState(GroupForm.related.nested_groups, state, params); + relatedGroupListState = $stateExtender.buildDefinition(relatedGroupListState); + relatedGroupsAddState = $stateExtender.buildDefinition(relatedGroupsAddState); + + let relatedHostsListState = nestedHostsListState(GroupForm.related.nested_hosts, state, params); + let relatedHostsAddState = buildHostAddState(GroupForm.related.nested_hosts, state, params); + let relatedHostsEditState = buildHostEditState(GroupForm.related.nested_hosts, state, params); + relatedHostsListState = $stateExtender.buildDefinition(relatedHostsListState); + relatedHostsAddState = $stateExtender.buildDefinition(relatedHostsAddState); + if(Array.isArray(relatedHostsEditState)) + { + relatedHostsEditState[0] = $stateExtender.buildDefinition(relatedHostsEditState[0]); + relatedHostsEditState[1] = $stateExtender.buildDefinition(relatedHostsEditState[1]); + states.push(state, + relatedGroupListState, + relatedGroupsAddState, + relatedHostsListState, + relatedHostsAddState, + relatedHostsEditState[0], + relatedHostsEditState[1]); + } + else { + relatedHostsEditState = $stateExtender.buildDefinition(relatedHostsEditState); + states.push(state, + relatedGroupListState, + relatedGroupsAddState, + relatedHostsListState, + relatedHostsAddState, + relatedHostsEditState); + } + + // states.push(state, + // relatedGroupListState, + // relatedGroupsAddState, + // relatedHostsListState, + // relatedHostsAddState, + // relatedHostsEditState[0], + // relatedHostsEditState[1]); + return states; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js new file mode 100644 index 0000000000..1335b4bb80 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js @@ -0,0 +1,58 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'groupData', + function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON, + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, groupData) { + + init(); + + function init() { + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + $scope = angular.extend($scope, groupData); + + $scope.$watch('summary_fields.user_capabilities.edit', function(val) { + $scope.canAdd = val; + }); + + // init codemirror(s) + $scope.variables = $scope.variables === null || $scope.variables === '' ? '---' : ParseVariableString($scope.variables); + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + + ParseTypeChange({ + scope: $scope, + field_id: 'group_variables', + variable: 'variables', + }); + + } + + $scope.formCancel = function() { + $state.go('^'); + }; + + $scope.formSave = function() { + var json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + // group fields + var group = { + variables: json_data, + name: $scope.name, + description: $scope.description, + inventory: $scope.inventory, + id: groupData.id + }; + GroupManageService.put(group).then(() => $state.go($state.current, null, { reload: true })); + }; + + } +]; diff --git a/awx/ui/client/src/inventories/groups/edit/main.js b/awx/ui/client/src/inventories/groups/edit/main.js new file mode 100644 index 0000000000..532d4b03ba --- /dev/null +++ b/awx/ui/client/src/inventories/groups/edit/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildGroupsEditState from './build-groups-edit-state.factory'; +import controller from './groups-edit.controller'; + +export default +angular.module('groupEdit', []) + .factory('buildGroupsEditState', buildGroupsEditState) + .controller('GroupEditController', controller); diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js similarity index 100% rename from awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js rename to awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js diff --git a/awx/ui/client/src/inventories/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js new file mode 100644 index 0000000000..4f6a178a71 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -0,0 +1,106 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Groups + * @description This form is for adding/editing a Group on the inventory page +*/ + +export default ['i18n', 'nestedGroupListState', 'nestedHostsListState', + 'buildHostAddState', +function(i18n, nestedGroupListState, nestedHostsListState, buildHostAddState){ + return { + addTitle: 'CREATE GROUP', + editTitle: '{{ name }}', + showTitle: true, + name: 'group', + basePath: 'groups', + parent: 'inventories.edit.groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.edit', + detailsClick: "$state.go('inventories.edit.groups.edit')", + well: false, + tabs: true, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "Variables defined here apply to all child groups and hosts.
" + + "Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body', + tab: 'properties' + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + }, + related: { + nested_groups: { + name: 'nested_groups', + ngClick: "$state.go('inventories.edit.groups.edit.nested_groups')", + include: "NestedGroupListDefinition", + includeForm: "NestedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'nested_group', + listState: nestedGroupListState + }, + nested_hosts: { + name: 'nested_hosts', + ngClick: "$state.go('inventories.edit.groups.edit.nested_hosts')", + include: "NestedHostsListDefinition", + title: i18n._('Hosts'), + iterator: 'nested_hosts', + listState: nestedHostsListState, + addState: buildHostAddState, + // editState: buildGroupsEditState + }, + + } + }; +}]; diff --git a/awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js similarity index 56% rename from awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js rename to awx/ui/client/src/inventories/groups/groups.list.js index 3c4c12c9c1..412dd737ee 100644 --- a/awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -1,5 +1,5 @@ /************************************************* - * Copyright (c) 2015 Ansible, Inc. + * Copyright (c) 2017 Ansible, Inc. * * All Rights Reserved *************************************************/ @@ -8,30 +8,15 @@ export default { name: 'groups', iterator: 'group', editTitle: '{{ inventory.name }}', - listTitle: 'GROUPS', - searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', - showTitle: false, well: true, + wellOverride: true, index: false, hover: true, - 'class': 'table-no-border', multiSelect: true, trackBy: 'group.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', fields: { - sync_status: { - label: '', - nosort: true, - mode: 'all', - iconOnly: true, - ngClick: 'viewUpdateStatus(group.id)', - awToolTip: "{{ group.status_tooltip }}", - dataTipWatch: "group.status_tooltip", - icon: "{{ 'fa icon-cloud-' + group.status_class }}", - ngClass: "group.status_class", - dataPlacement: "top", - columnClass: 'status-column List-staticColumn--smallStatus' - }, failed_hosts: { label: '', nosort: true, @@ -39,24 +24,15 @@ export default { iconOnly: true, awToolTip: "{{ group.hosts_status_tip }}", dataPlacement: "top", - ngClick: "showFailedHosts(group)", icon: "{{ 'fa icon-job-' + group.hosts_status_class }}", columnClass: 'status-column List-staticColumn--smallStatus' }, name: { label: 'Groups', key: true, - ngClick: "groupSelect(group.id)", + ngClick: "editGroup(group.id)", columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', class: 'InventoryManage-breakWord', - }, - total_groups: { - nosort: true, - label: '', - type: 'badgeCount', - ngHide: 'group.total_groups == 0', - noLink: true, - awToolTip: "{{group.name | sanitize}} contains {{group.total_groups}} {{group.total_groups === 1 ? 'child' : 'children'}}" } }, @@ -71,11 +47,10 @@ export default { }, launch: { mode: 'all', - // $scope.$parent is governed by InventoryManageController, - ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', - ngClick: '$parent.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", + 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 a selection of multiple groups.", + dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: true, @@ -102,28 +77,28 @@ export default { columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', - group_update: { - //label: 'Sync', - mode: 'all', - ngClick: 'updateGroup(group)', - awToolTip: "{{ group.launch_tooltip }}", - dataTipWatch: "group.launch_tooltip", - ngShow: "(group.status !== 'running' && group.status " + - "!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start", - ngClass: "group.launch_class", - dataPlacement: "top", - }, - cancel: { - //label: 'Cancel', - mode: 'all', - ngClick: "cancelUpdate(group.id)", - awToolTip: "Cancel sync process", - 'class': 'red-txt', - ngShow: "(group.status == 'running' || group.status == 'pending' " + - "|| group.status == 'updating') && group.summary_fields.user_capabilities.start", - dataPlacement: "top", - iconClass: "fa fa-minus-circle" - }, + // group_update: { + // //label: 'Sync', + // mode: 'all', + // ngClick: 'updateGroup(group)', + // awToolTip: "{{ group.launch_tooltip }}", + // dataTipWatch: "group.launch_tooltip", + // ngShow: "(group.status !== 'running' && group.status " + + // "!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start", + // ngClass: "group.launch_class", + // dataPlacement: "top", + // }, + // cancel: { + // //label: 'Cancel', + // mode: 'all', + // ngClick: "cancelUpdate(group.id)", + // awToolTip: "Cancel sync process", + // 'class': 'red-txt', + // ngShow: "(group.status == 'running' || group.status == 'pending' " + + // "|| group.status == 'updating') && group.summary_fields.user_capabilities.start", + // dataPlacement: "top", + // iconClass: "fa fa-minus-circle" + // }, copy: { mode: 'all', ngClick: "copyMoveGroup(group.id)", @@ -131,14 +106,14 @@ export default { ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy", dataPlacement: "top" }, - schedule: { - mode: 'all', - ngClick: "scheduleGroup(group.id)", - awToolTip: "{{ group.group_schedule_tooltip }}", - ngClass: "group.scm_type_class", - dataPlacement: 'top', - ngShow: "!(group.summary_fields.inventory_source.source === '')" - }, + // schedule: { + // mode: 'all', + // ngClick: "scheduleGroup(group.id)", + // awToolTip: "{{ group.group_schedule_tooltip }}", + // ngClass: "group.scm_type_class", + // dataPlacement: 'top', + // ngShow: "!(group.summary_fields.inventory_source.source === '')" + // }, edit: { //label: 'Edit', mode: 'all', diff --git a/awx/ui/client/src/inventories/manage/groups/groups.service.js b/awx/ui/client/src/inventories/groups/groups.service.js similarity index 100% rename from awx/ui/client/src/inventories/manage/groups/groups.service.js rename to awx/ui/client/src/inventories/groups/groups.service.js diff --git a/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js b/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js new file mode 100644 index 0000000000..db175bf3eb --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js @@ -0,0 +1,82 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import GroupsListController from './groups-list.controller'; +export default ['GroupList', '$stateExtender', 'templateUrl', '$injector', + function(GroupList, $stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + searchPrefix: `${list.iterator}`, + name: `${formStateDefinition.name}.${list.iterator}s`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + [list.iterator + '_search']: { + value: { order_by: field.order_by ? field.order_by : 'name' } + }, + }, + views: { + 'related': { + templateProvider: function(GroupList, generateList, $templateRequest, $stateParams, GetBasePath) { + let list = _.cloneDeep(GroupList); + if($stateParams && $stateParams.group) { + list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + + let html = generateList.build({ + list: list, + mode: 'edit' + }); + // Include the custom group delete modal template + return $templateRequest(templateUrl('inventories/groups/list/groups-list')).then((template) => { + return html.concat(template); + }); + }, + controller: GroupsListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { + return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + if (field.search) { + state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + } + return state; + }; + return val; + } +]; 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 new file mode 100644 index 0000000000..7cfd4cdb01 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -0,0 +1,180 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'GroupList', 'InventoryUpdate', + 'GroupManageService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', + 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, GroupList, InventoryUpdate, + GroupManageService, CancelSourceUpdate, rbacUiControlService, GetBasePath, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + + let list = GroupList; + + init(); + + function init(){ + $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; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. + // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: + // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the + // problem that this solves. + $scope.ncyBreadcrumbIgnore = true; + if($state.current.name === "inventories.edit.groups") { + $scope.rowBeingEdited = $state.params.group_id; + $scope.listBeingEdited = "groups"; + } + + $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){ + if (group === undefined || group === null) { + group = {}; + } + + let hosts_status; + + hosts_status = GetHostsStatusMsg({ + active_failures: group.hosts_with_active_failures, + total_hosts: group.total_hosts, + inventory_id: $scope.inventory_id, + group_id: group.id + }); + _.assign(group, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}); + } + + $scope.createGroup = function(){ + $state.go('inventories.edit.groups.add'); + }; + $scope.editGroup = function(id){ + $state.go('inventories.edit.groups.edit', {group_id: id}); + }; + $scope.deleteGroup = function(group){ + $scope.toDelete = {}; + angular.extend($scope.toDelete, group); + if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) { + // This group doesn't have any child groups or hosts - the user is just trying to delete + // the group + $scope.deleteOption = "delete"; + } + $('#group-delete-modal').modal('show'); + }; + $scope.confirmDelete = function(){ + + // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes + // will mean that these two things are running async and the modal may not finish closing before + // the state finishes transitioning. + $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () { + // Remove the event handler so that we don't end up with multiple bindings + $('#group-delete-modal').off('hidden.bs.modal'); + // Reload the inventory manage page and show that the group has been removed + $state.go('.', null, {reload: true}); + }); + + switch($scope.deleteOption){ + case 'promote': + GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id) + .then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("^", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + break; + default: + GroupManageService.delete($scope.toDelete.id).then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("^", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + } + }; + $scope.updateGroup = function(group) { + GroupManageService.getInventorySource({group: group.id}).then(res =>InventoryUpdate({ + scope: $scope, + group_id: group.id, + url: res.data.results[0].related.update, + group_name: group.name, + group_source: res.data.results[0].source + })); + }; + + $scope.cancelUpdate = function (id) { + CancelSourceUpdate({ scope: $scope, id: id }); + }; + + $scope.copyMoveGroup = function(id){ + $state.go('inventories.edit.groups.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventories.edit.groups.edit") { + $scope.rowBeingEdited = toParams.group_id; + $scope.listBeingEdited = "groups"; + } + else { + delete $scope.rowBeingEdited; + delete $scope.listBeingEdited; + } + }); + + // Remove the listener when the scope is destroyed to avoid a memory leak + $scope.$on('$destroy', function() { + 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/manage/groups/groups-list.partial.html b/awx/ui/client/src/inventories/groups/list/groups-list.partial.html similarity index 100% rename from awx/ui/client/src/inventories/manage/groups/groups-list.partial.html rename to awx/ui/client/src/inventories/groups/list/groups-list.partial.html diff --git a/awx/ui/client/src/inventories/groups/list/main.js b/awx/ui/client/src/inventories/groups/list/main.js new file mode 100644 index 0000000000..37e4c974fa --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildGroupsListState from './build-groups-list-state.factory'; +import controller from './groups-list.controller'; + +export default + angular.module('groupsList', []) + .factory('buildGroupsListState', buildGroupsListState) + .controller('GroupsListController', controller); diff --git a/awx/ui/client/src/inventories/groups/main.js b/awx/ui/client/src/inventories/groups/main.js new file mode 100644 index 0000000000..ab66437ace --- /dev/null +++ b/awx/ui/client/src/inventories/groups/main.js @@ -0,0 +1,28 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import groupList from './list/main'; +import groupAdd from './add/main'; +import groupEdit from './edit/main'; +import nestedGroups from './nested-groups/main'; +import nestedHosts from './nested-hosts/main'; +import groupFormDefinition from './groups.form'; +import groupListDefinition from './groups.list'; +import service from './groups.service'; +import GetHostsStatusMsg from './factories/get-hosts-status-msg.factory'; + +export default + angular.module('group', [ + groupList.name, + groupAdd.name, + groupEdit.name, + nestedGroups.name, + nestedHosts.name + ]) + .factory('GroupForm', groupFormDefinition) + .value('GroupList', groupListDefinition) + .factory('GetHostsStatusMsg', GetHostsStatusMsg) + .service('GroupManageService', service); diff --git a/awx/ui/client/src/inventories/groups/nested-groups/main.js b/awx/ui/client/src/inventories/groups/nested-groups/main.js new file mode 100644 index 0000000000..0c02775e75 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/main.js @@ -0,0 +1,19 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import nestedGroupListState from './nested-groups-list-state.factory'; +import nestedGroupAddState from './nested-groups-add-state.factory'; +import nestedGroupListDefinition from './nested-groups.list'; +import nestedGroupFormDefinition from './nested-groups.form'; +import controller from './nested-groups-list.controller'; + +export default + angular.module('nestedGroups', []) + .factory('nestedGroupListState', nestedGroupListState) + .factory('nestedGroupAddState', nestedGroupAddState) + .value('NestedGroupListDefinition', nestedGroupListDefinition) + .factory('NestedGroupFormDefinition', nestedGroupFormDefinition) + .controller('NestedGroupsListController', controller); diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-add-state.factory.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-add-state.factory.js new file mode 100644 index 0000000000..b376436149 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-add-state.factory.js @@ -0,0 +1,46 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import GroupAddController from '../add/groups-add.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.add`, + url: `/add`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'nestedGroupForm@inventories': { + templateProvider: function(GenerateForm, GroupForm) { + let form = GroupForm; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: GroupAddController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list-state.factory.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list-state.factory.js new file mode 100644 index 0000000000..26763ee007 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list-state.factory.js @@ -0,0 +1,92 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import NestedGroupsListController from './nested-groups-list.controller'; +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + searchPrefix: `${list.iterator}`, + squash: '', + name: `${formStateDefinition.name}.nested_groups`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + [list.iterator + '_search']: { + value: { order_by: field.order_by ? field.order_by : 'name' } + }, + }, + views: { + // 'related@inventories.edit.groups.edit': { + 'related': { + templateProvider: function(NestedGroupListDefinition, generateList) { + let list = _.cloneDeep(NestedGroupListDefinition); + + let html = generateList.build({ + list: list, + mode: 'edit' + }); + // Include the custom group delete modal template + // return $templateRequest(templateUrl('inventories/groups/list/groups-list')).then((template) => { + // return html.concat(template); + // }); + return html; + }, + controller: NestedGroupsListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + if($stateParams.group_id){ + path = `api/v2/groups/${$stateParams.group_id}/children`; + } + else if($stateParams.host_id){ + path = GetBasePath('hosts') + $stateParams.host_id + '/all_groups'; + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + host: ['$stateParams', 'HostManageService', function($stateParams, HostManageService) { + if($stateParams.host_id){ + return HostManageService.get({ id: $stateParams.host_id }).then(function(res) { + return res.data.results[0]; + }); + } + }], + inventoryData: ['InventoryManageService', '$stateParams', 'host', function(InventoryManageService, $stateParams, host) { + var id = ($stateParams.inventory_id) ? $stateParams.inventory_id : host.summary_fields.inventory.id; + return InventoryManageService.getInventory(id).then(res => res.data); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + if (field.search) { + state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + } + return state; + }; + return val; + } +]; 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 new file mode 100644 index 0000000000..237757efa8 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -0,0 +1,173 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'NestedGroupListDefinition', 'InventoryUpdate', + 'GroupManageService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', + 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, NestedGroupListDefinition, InventoryUpdate, + GroupManageService, CancelSourceUpdate, rbacUiControlService, GetBasePath, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + + let list = NestedGroupListDefinition; + + init(); + + function init(){ + $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; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. + // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: + // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the + // problem that this solves. + $scope.ncyBreadcrumbIgnore = true; + if($state.current.name === "inventories.edit.groups.edit.nested_groups.edit") { + $scope.rowBeingEdited = $state.params.group_id; + $scope.listBeingEdited = "groups"; + } + + $scope.inventory_id = $stateParams.inventory_id; + _.forEach($scope[list.name], buildStatusIndicators); + + } + + function buildStatusIndicators(group){ + if (group === undefined || group === null) { + group = {}; + } + + let hosts_status; + + hosts_status = GetHostsStatusMsg({ + active_failures: group.hosts_with_active_failures, + total_hosts: group.total_hosts, + inventory_id: $scope.inventory_id, + group_id: group.id + }); + _.assign(group, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}); + } + + $scope.createGroup = function(){ + $state.go('inventories.edit.groups.edit.nested_groups.add'); + }; + $scope.editGroup = function(id){ + $state.go('inventories.edit.groups.edit', {group_id: id}); + }; + // $scope.editGroup = function(id){ + // $state.go('inventories.edit.groups.edit.nested_groups.edit', {nested_group_id: id}); + // }; + $scope.deleteGroup = function(group){ + $scope.toDelete = {}; + angular.extend($scope.toDelete, group); + if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) { + // This group doesn't have any child groups or hosts - the user is just trying to delete + // the group + $scope.deleteOption = "delete"; + } + $('#group-delete-modal').modal('show'); + }; + $scope.confirmDelete = function(){ + + // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes + // will mean that these two things are running async and the modal may not finish closing before + // the state finishes transitioning. + $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () { + // Remove the event handler so that we don't end up with multiple bindings + $('#group-delete-modal').off('hidden.bs.modal'); + // Reload the inventory manage page and show that the group has been removed + $state.go('.', null, {reload: true}); + }); + + switch($scope.deleteOption){ + case 'promote': + GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id) + .then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("^", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + break; + default: + GroupManageService.delete($scope.toDelete.id).then(() => { + if (parseInt($state.params.group_id) === $scope.toDelete.id) { + $state.go("^", null, {reload: true}); + } else { + $state.go($state.current, null, {reload: true}); + } + $('#group-delete-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + } + }; + $scope.updateGroup = function(group) { + GroupManageService.getInventorySource({group: group.id}).then(res =>InventoryUpdate({ + scope: $scope, + group_id: group.id, + url: res.data.results[0].related.update, + group_name: group.name, + group_source: res.data.results[0].source + })); + }; + + $scope.cancelUpdate = function (id) { + CancelSourceUpdate({ scope: $scope, id: id }); + }; + + // $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(){ + // TODO: implement + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventories.edit.groups.edit.nested_groups.edit") { + $scope.rowBeingEdited = toParams.group_id; + $scope.listBeingEdited = "groups"; + } + else { + delete $scope.rowBeingEdited; + delete $scope.listBeingEdited; + } + }); + + // Remove the listener when the scope is destroyed to avoid a memory leak + $scope.$on('$destroy', function() { + 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.form.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js new file mode 100644 index 0000000000..955d25e462 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js @@ -0,0 +1,96 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Groups + * @description This form is for adding/editing a Group on the inventory page +*/ + +export default ['i18n', 'nestedGroupListState', +function(i18n, nestedGroupListState){ + return { + addTitle: 'CREATE GROUP', + editTitle: '{{ name }}', + showTitle: true, + name: 'nested_group', + iterator: "nested_group", + basePath: 'groups', + parent: 'inventories.edit.groups.edit.nested_groups', + // the parent node this generated state definition tree expects to attach to + stateTree: 'inventories', + // form generator inspects the current state name to determine whether or not to set an active (.is-selected) class on a form tab + // this setting is optional on most forms, except where the form's edit state name is not parentStateName.edit + activeEditState: 'inventories.edit.groups.edit.nested_groups.edit', + detailsClick: "$state.go('inventories.edit.groups.edit.nested_groups.edit')", + well: false, + tabs: true, + fields: { + name: { + label: 'Name', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + required: true, + tab: 'properties' + }, + description: { + label: 'Description', + type: 'text', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + tab: 'properties' + }, + variables: { + label: 'Variables', + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + dataTitle: 'Group Variables', + dataPlacement: 'right', + parseTypeName: 'parseType', + awPopOver: "Variables defined here apply to all child groups and hosts.
" + + "Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body', + tab: 'properties' + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + }, + related: { + nested_groups: { + name: 'related_groups', + ngClick: "$state.go('inventories.edit.groups.edit.related_groups')", + include: "RelatedGroupListDefinition", + includeForm: "RelatedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'related_group', + listState: nestedGroupListState + }, + + } + }; +}]; 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 new file mode 100644 index 0000000000..082eb3bfad --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -0,0 +1,144 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'nested_groups', + iterator: 'nested_group', + editTitle: '{{ inventory.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + multiSelect: true, + trackBy: 'nested_group.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', + + fields: { + failed_hosts: { + label: '', + nosort: true, + mode: 'all', + iconOnly: true, + awToolTip: "{{ nested_group.hosts_status_tip }}", + dataPlacement: "top", + icon: "{{ 'fa icon-job-' + nested_group.hosts_status_class }}", + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + label: 'Groups', + key: true, + + // ngClick: "groupSelect(group.id)", + ngClick: "editGroup(nested_group.id)", + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', + class: 'InventoryManage-breakWord', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + launch: { + mode: 'all', + 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.", + dataPlacement: 'top', + actionClass: 'btn List-buttonDefault', + buttonContent: 'RUN COMMANDS', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + ngShow: 'canAdhoc' + // TODO: set up a tip watcher and change text based on when + // things are selected/not selected. This is started and + // commented out in the inventory controller within the watchers. + // awToolTip: "{{ adhocButtonTipContents }}", + // dataTipWatch: "adhocButtonTipContents" + }, + create: { + mode: 'all', + ngClick: "createGroup()", + awToolTip: "Create a new group", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD GROUP', + ngShow: 'canAdd', + dataPlacement: "top", + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', + + // group_update: { + // //label: 'Sync', + // mode: 'all', + // ngClick: 'updateGroup(group)', + // awToolTip: "{{ group.launch_tooltip }}", + // dataTipWatch: "group.launch_tooltip", + // ngShow: "(group.status !== 'running' && group.status " + + // "!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start", + // ngClass: "group.launch_class", + // dataPlacement: "top", + // }, + // cancel: { + // //label: 'Cancel', + // mode: 'all', + // ngClick: "cancelUpdate(group.id)", + // awToolTip: "Cancel sync process", + // 'class': 'red-txt', + // ngShow: "(group.status == 'running' || group.status == 'pending' " + + // "|| group.status == 'updating') && group.summary_fields.user_capabilities.start", + // dataPlacement: "top", + // iconClass: "fa fa-minus-circle" + // }, + copy: { + mode: 'all', + ngClick: "copyMoveGroup(nested_group.id)", + awToolTip: 'Copy or move group', + ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy", + dataPlacement: "top" + }, + // schedule: { + // mode: 'all', + // ngClick: "scheduleGroup(group.id)", + // awToolTip: "{{ group.group_schedule_tooltip }}", + // ngClass: "group.scm_type_class", + // dataPlacement: 'top', + // ngShow: "!(group.summary_fields.inventory_source.source === '')" + // }, + edit: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(nested_group.id)", + awToolTip: 'Edit group', + dataPlacement: "top", + ngShow: "nested_group.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(nested_group.id)", + awToolTip: 'View group', + dataPlacement: "top", + ngShow: "!nested_group.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteGroup(nested_group)", + awToolTip: 'Delete group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/groups/nested-hosts/main.js b/awx/ui/client/src/inventories/groups/nested-hosts/main.js new file mode 100644 index 0000000000..0d1ef630e9 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-hosts/main.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import nestedHostsListState from './nested-hosts-list-state.factory'; +import nestedHostsListDefinition from './nested-hosts.list'; +import nestedHostsFormDefinition from './nested-hosts.form'; +import controller from './nested-hosts-list.controller'; + +export default + angular.module('nestedHosts', []) + .factory('nestedHostsListState', nestedHostsListState) + .value('NestedHostsListDefinition', nestedHostsListDefinition) + .factory('NestedHostsFormDefinition', nestedHostsFormDefinition) + .controller('NestedHostsListController', controller); diff --git a/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list-state.factory.js b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list-state.factory.js new file mode 100644 index 0000000000..96b7402292 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list-state.factory.js @@ -0,0 +1,79 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import NestedHostsListController from './nested-hosts-list.controller'; +export default ['NestedHostsListDefinition', '$stateExtender', 'templateUrl', '$injector', + function(NestedHostsListDefinition, $stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + searchPrefix: `${list.iterator}`, + squash: '', + name: `${formStateDefinition.name}.nested_hosts`, + url: `/${list.iterator}s`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + params: { + [list.iterator + '_search']: { + value: { order_by: field.order_by ? field.order_by : 'name' } + }, + }, + views: { + // 'related@inventories.edit.groups.edit': { + 'related': { + templateProvider: function(NestedHostsListDefinition, generateList) { + let list = _.cloneDeep(NestedHostsListDefinition); + + let html = generateList.build({ + list: list, + mode: 'edit' + }); + // Include the custom group delete modal template + // return $templateRequest(templateUrl('inventories/groups/list/groups-list')).then((template) => { + // return html.concat(template); + // }); + return html; + }, + controller: NestedHostsListController + } + }, + resolve: { + ListDefinition: () => { + return list; + }, + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + path = `api/v2/groups/${$stateParams.group_id}/all_hosts`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { + return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + // appy any default search parameters in form definition + if (field.search) { + state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); + } + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list.controller.js b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list.controller.js new file mode 100644 index 0000000000..20196b1647 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list.controller.js @@ -0,0 +1,162 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePath', + 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', + 'HostManageService', 'SetStatus', + function($scope, NestedHostsListDefinition, $rootScope, GetBasePath, + rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, + HostManageService, SetStatus) { + + let list = NestedHostsListDefinition; + + init(); + + function init(){ + $scope.canAdd = false; + $scope.enableSmartInventoryButton = false; + + rbacUiControlService.canAdd('hosts') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $rootScope.flashMessage = null; + + $scope.$watchCollection(list.name, function() { + $scope[list.name] = _.map($scope.nested_hosts, function(value) { + value.inventory_name = value.summary_fields.inventory.name; + value.inventory_id = value.summary_fields.inventory.id; + return value; + }); + setJobStatus(); + }); + + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if(toState.name === 'hosts.addSmartInventory') { + $scope.enableSmartInventoryButton = false; + } + else { + if(toParams && toParams.host_search) { + let hasMoreThanDefaultKeys = false; + angular.forEach(toParams.host_search, function(value, key) { + if(key !== 'order_by' && key !== 'page_size') { + hasMoreThanDefaultKeys = true; + } + }); + $scope.enableSmartInventoryButton = hasMoreThanDefaultKeys ? true : false; + } + else { + $scope.enableSmartInventoryButton = false; + } + } + }); + + $scope.$on('selectedOrDeselected', function(e, value) { + let item = value.value; + + if (value.isSelected) { + if(!$scope.hostsSelected) { + $scope.hostsSelected = []; + } + $scope.hostsSelected.push(item); + } else { + _.remove($scope.hostsSelected, { id: item.id }); + if($scope.hostsSelected.length === 0) { + $scope.hostsSelected = null; + } + } + + $scope.systemTrackingDisabled = ($scope.hostsSelected && $scope.hostsSelected.length > 2) ? true : false; + }); + + } + + function setJobStatus(){ + _.forEach($scope.hosts, function(value) { + SetStatus({ + scope: $scope, + host: value + }); + }); + } + + $scope.createHost = function(){ + $state.go('inventories.edit.groups.edit.nested_hosts.add'); + }; + $scope.editHost = function(id){ + $state.go('inventories.edit.groups.edit.nested_hosts.edit', {host_id: id}); + }; + $scope.deleteHost = function(id, name){ + var body = '" + + i18n._("Indicates if a host is available and should be included in running jobs.") + + "
" + + i18n._("For hosts that are part of an external" + + " inventory, this flag cannot be changed. It will be" + + " set by the inventory sync process.") + + "
", + dataTitle: i18n._('Host Enabled'), + ngDisabled: 'host.has_inventory_sources' + } + }, + fields: { + name: { + label: i18n._('Host Name'), + type: 'text', + required: true, + awPopOver: "" + + i18n._("Provide a host name, ip address, or ip address:port. Examples include:") + + "
" + + "myserver.domain.com", + dataTitle: i18n._('Host Name'), + dataPlacement: 'right', + dataContainer: 'body', + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + description: { + label: i18n._('Description'), + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)', + type: 'text' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + rows: 6, + class: 'Form-formGroup--fullWidth', + "default": "---", + awPopOver: "
" + + "127.0.0.1
" + + "10.1.0.140:25
" + + "server.example.com:25" + + "
" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
' + i18n.sprintf(i18n._('View JSON examples at %s'), 'www.json.org') + '
' + + '' + i18n.sprintf(i18n._('View YAML examples at %s'), 'docs.ansible.com') + '
', + dataTitle: i18n._('Host Variables'), + dataPlacement: 'right', + dataContainer: 'body' + }, + inventory: { + type: 'hidden', + includeOnEdit: true, + includeOnAdd: true + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + } + }, + + related: { + ansible_facts: { + name: 'ansible_facts', + title: i18n._('Facts'), + skipGenerator: true + }, + nested_groups: { + name: 'nested_groups', + ngClick: "$state.go('inventories.edit.groups.edit.nested_hosts.edit.nested_groups')", + include: "NestedGroupListDefinition", + includeForm: "NestedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'nested_group', + listState: nestedGroupListState + }, + insights: { + name: 'insights', + title: i18n._('Insights'), + skipGenerator: true + } + } + }; + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.list.js b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.list.js new file mode 100644 index 0000000000..f713e091a9 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.list.js @@ -0,0 +1,131 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'nested_hosts', + iterator: 'nested_host', + editTitle: '{{ nested_host.name }}', // i don't think this is correct + // showTitle: false, + well: true, + wellOverride: true, + index: false, + hover: true, + // hasChildren: true, + multiSelect: true, + trackBy: 'nested_host.id', + basePath: 'api/v2/groups/{{$stateParams.group_id}}/all_hosts/', + + fields: { + active_failures: { + label: '', + iconOnly: true, + nosort: true, + // do not remove this ng-click directive + // the list generator case to handle fields without ng-click + // cannot handle the aw-* directives + ngClick: 'noop()', + awPopOver: "{{ nested_host.job_status_html }}", + dataTitle: "{{ nested_host.job_status_title }}", + awToolTip: "{{ nested_host.badgeToolTip }}", + dataPlacement: 'top', + icon: "{{ 'fa icon-job-' + nested_host.active_failures }}", + id: 'active-failures-action', + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + key: true, + label: 'Hosts', + ngClick: "editHost(nested_host.id)", + ngClass: "{ 'host-disabled-label': !nested_host.enabled }", + columnClass: 'col-lg-6 col-md-8 col-sm-8 col-xs-7', + dataHostId: "{{ nested_host.id }}", + dataType: "nested_host", + class: 'InventoryManage-breakWord' + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-4 col-sm-4 col-xs-5 text-right', + copy: { + mode: 'all', + ngClick: "copyMoveHost(nested_host.id)", + awToolTip: 'Copy or move host to another group', + dataPlacement: "top", + ngShow: 'nested_host.summary_fields.user_capabilities.edit' + }, + edit: { + //label: 'Edit', + ngClick: "editHost(nested_host.id)", + icon: 'icon-edit', + awToolTip: 'Edit host', + dataPlacement: 'top', + ngShow: 'nested_host.summary_fields.user_capabilities.edit' + }, + view: { + //label: 'Edit', + ngClick: "editHost(nested_host.id)", + awToolTip: 'View host', + dataPlacement: 'top', + ngShow: '!nested_host.summary_fields.user_capabilities.edit' + }, + "delete": { + //label: 'Delete', + ngClick: "deleteHost(nested_host.id, nested_host.name)", + icon: 'icon-trash', + awToolTip: 'Delete host', + dataPlacement: 'top', + ngShow: 'nested_host.summary_fields.user_capabilities.delete' + } + }, + + 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.", + dataPlacement: 'top', + 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()', + awToolTip: "Select one or two hosts by clicking the checkbox beside the host. System tracking offers the ability to compare the results of two scan runs from different dates on one host or the same date on two hosts.", + dataTipWatch: "systemTrackingTooltip", + dataPlacement: 'top', + awFeature: 'system_tracking', + actionClass: 'btn List-buttonDefault system-tracking', + ngDisabled: 'systemTrackingDisabled || !hostsSelected', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + ngShow: true + }, + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + create: { + mode: 'all', + ngClick: "createHost()", + awToolTip: "Create a new host", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD HOST', + ngShow: 'canAdd', + dataPlacement: "top", + } + } + +}; diff --git a/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js new file mode 100644 index 0000000000..ab4ab233e4 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js @@ -0,0 +1,83 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default + ['$scope', '$state', '$stateParams', 'DashboardHostsForm', 'GenerateForm', 'ParseTypeChange', 'DashboardHostService', 'host', + function($scope, $state, $stateParams, DashboardHostsForm, GenerateForm, ParseTypeChange, DashboardHostService, host){ + $scope.parseType = 'yaml'; + $scope.formCancel = function(){ + $state.go('^', null, {reload: true}); + }; + $scope.toggleHostEnabled = function(){ + if ($scope.host.has_inventory_sources){ + return; + } + $scope.host.enabled = !$scope.host.enabled; + }; + $scope.toggleEnabled = function(){ + $scope.host.enabled = !$scope.host.enabled; + }; + $scope.groupsTab = function(){ + let id = $scope.host.summary_fields.inventory.id; + $state.go('hosts.edit.nested_groups', {inventory_id: id}); + }; + $scope.formSave = function(){ + var host = { + id: $scope.host.id, + variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + name: $scope.name, + description: $scope.description, + enabled: $scope.host.enabled + }; + DashboardHostService.putHost(host).then(function(){ + $state.go('^', null, {reload: true}); + }); + + }; + var init = function(){ + $scope.host = host.data; + $scope.name = host.data.name; + $scope.description = host.data.description; + $scope.variables = getVars(host.data.variables); + ParseTypeChange({ + scope: $scope, + field_id: 'host_variables', + variable: 'variables', + }); + }; + + // Adding this function b/c sometimes extra vars are returned to the + // UI as a string (ex: "foo: bar"), and other times as a + // json-object-string (ex: "{"foo": "bar"}"). CodeMirror wouldn't know + // how to prettify the latter. The latter occurs when host vars were + // system generated and not user-input (such as adding a cloud host); + function getVars(str){ + + // Quick function to test if the host vars are a json-object-string, + // by testing if they can be converted to a JSON object w/o error. + function IsJsonString(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + } + + if(str === ''){ + return '---'; + } + else if(IsJsonString(str)){ + str = JSON.parse(str); + return jsyaml.safeDump(str); + } + else if(!IsJsonString(str)){ + return str; + } + } + + init(); + }]; diff --git a/awx/ui/client/src/inventories/hosts/edit/main.js b/awx/ui/client/src/inventories/hosts/edit/main.js new file mode 100644 index 0000000000..2f0c5aee39 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/edit/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './host-edit.controller'; + +export default +angular.module('hostsEdit', []) + .controller('HostEditController', controller); diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts.form.js b/awx/ui/client/src/inventories/hosts/host.form.js similarity index 81% rename from awx/ui/client/src/inventories/manage/hosts/hosts.form.js rename to awx/ui/client/src/inventories/hosts/host.form.js index efb64a4785..fdc8df1d56 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts.form.js +++ b/awx/ui/client/src/inventories/hosts/host.form.js @@ -10,7 +10,8 @@ * @description This form is for adding/editing a host on the inventory page */ -export default ['i18n', function(i18n) { +export default ['i18n', 'nestedGroupListState', +function(i18n, nestedGroupListState) { return { addTitle: i18n._('CREATE HOST'), @@ -21,6 +22,8 @@ export default ['i18n', function(i18n) { formLabelSize: 'col-lg-3', formFieldSize: 'col-lg-9', iterator: 'host', + activeEditState: 'hosts.edit', + stateTree: 'hosts', headerFields:{ enabled: { class: 'Form-header-field', @@ -99,5 +102,28 @@ export default ['i18n', function(i18n) { ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' } }, + + related: { + ansible_facts: { + name: 'ansible_facts', + title: i18n._('Facts'), + skipGenerator: true + }, + nested_groups: { + name: 'nested_groups', + // ngClick: "$state.go('hosts.edit.nested_groups')", + ngClick: "groupsTab()", + include: "NestedGroupListDefinition", + includeForm: "NestedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'nested_group', + listState: nestedGroupListState + }, + insights: { + name: 'insights', + title: i18n._('Insights'), + skipGenerator: true + } + } }; }]; diff --git a/awx/ui/client/src/inventories/hosts/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js new file mode 100644 index 0000000000..8118431df5 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -0,0 +1,120 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['i18n', function(i18n) { + return { + name: 'hosts', + iterator: 'host', + editTitle: '{{ selected_group }}', + searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', + nonstandardSearchParam: { + root: 'ansible_facts', + param: 'host_filter' + }, + showTitle: false, + well: true, + index: false, + hover: true, + hasChildren: true, + 'class': 'table-no-border', + trackBy: 'host.id', + basePath: 'hosts', + title: false, + actionHolderClass: 'List-actionHolder List-actionHolder--leftAlign', + + fields: { + toggleHost: { + ngDisabled: 'host.has_inventory_sources', + label: '', + columnClass: 'List-staticColumn--toggle', + type: "toggle", + ngClick: "toggleHost($event, host)", + awToolTip: "" + + i18n._("Indicates if a host is available and should be included in running jobs.") + + "
" + + i18n._("For hosts that are part of an external" + + " inventory, this flag cannot be changed. It will be" + + " set by the inventory sync process.") + + "
", + dataPlacement: "right", + nosort: true, + }, + active_failures: { + label: '', + iconOnly: true, + nosort: true, + // do not remove this ng-click directive + // the list generator case to handle fields without ng-click + // cannot handle the aw-* directives + ngClick: 'noop()', + awPopOver: "{{ host.job_status_html }}", + dataTitle: "{{ host.job_status_title }}", + awToolTip: "{{ host.badgeToolTip }}", + dataPlacement: 'top', + icon: "{{ 'fa icon-job-' + host.active_failures }}", + id: 'active-failures-action', + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + key: true, + label: i18n._('Name'), + ngClick: "editHost(host.id)", + columnClass: 'col-lg-6 col-md-8 col-sm-8 col-xs-7', + dataHostId: "{{ host.id }}", + dataType: "host", + class: 'InventoryManage-breakWord' + }, + inventory_name: { + label: i18n._('Inventory'), + sourceModel: 'inventory', + sourceField: 'name', + columnClass: 'col-lg-5 col-md-4 col-sm-4 hidden-xs elllipsis', + linkTo: "{{ '/#/inventories/' + host.inventory_id }}" + }, + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-4 col-sm-4 col-xs-5 text-right', + edit: { + //label: 'Edit', + ngClick: "editHost(host.id)", + icon: 'icon-edit', + awToolTip: 'Edit host', + dataPlacement: 'top', + ngShow: 'host.summary_fields.user_capabilities.edit' + }, + view: { + //label: 'Edit', + ngClick: "editHost(host.id)", + awToolTip: 'View host', + dataPlacement: 'top', + ngShow: '!host.summary_fields.user_capabilities.edit' + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + smart_inventory: { + mode: 'all', + ngClick: "smartInventory()", + awToolTip: "Create a new Smart Inventory from search results.", + actionClass: 'btn List-buttonDefault', + buttonContent: 'SMART INVENTORY', + ngShow: 'canAdd', + dataPlacement: "top", + ngDisabled: '!enableSmartInventoryButton' + } + } + }; +}]; diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts.service.js b/awx/ui/client/src/inventories/hosts/hosts.service.js similarity index 100% rename from awx/ui/client/src/inventories/manage/hosts/hosts.service.js rename to awx/ui/client/src/inventories/hosts/hosts.service.js diff --git a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js new file mode 100644 index 0000000000..7f89b64e99 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -0,0 +1,139 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +function HostsList($scope, HostsList, $rootScope, GetBasePath, + rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, + HostManageService, SetStatus) { + + let list = HostsList; + + init(); + + function init(){ + $scope.canAdd = false; + $scope.enableSmartInventoryButton = false; + + rbacUiControlService.canAdd('hosts') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $rootScope.flashMessage = null; + + $scope.$watchCollection(list.name, function() { + $scope[list.name] = _.map($scope.hosts, function(value) { + value.inventory_name = value.summary_fields.inventory.name; + value.inventory_id = value.summary_fields.inventory.id; + return value; + }); + setJobStatus(); + }); + + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if(toState.name === 'hosts.addSmartInventory') { + $scope.enableSmartInventoryButton = false; + } + else { + if(toParams && toParams.host_search) { + let hasMoreThanDefaultKeys = false; + angular.forEach(toParams.host_search, function(value, key) { + if(key !== 'order_by' && key !== 'page_size') { + hasMoreThanDefaultKeys = true; + } + }); + $scope.enableSmartInventoryButton = hasMoreThanDefaultKeys ? true : false; + } + else { + $scope.enableSmartInventoryButton = false; + } + } + }); + + } + + function setJobStatus(){ + _.forEach($scope.hosts, function(value) { + SetStatus({ + scope: $scope, + host: value + }); + }); + } + + $scope.createHost = function(){ + $state.go('hosts.add'); + }; + $scope.editHost = function(id){ + $state.go('hosts.edit', {host_id: id}); + }; + $scope.deleteHost = function(id, name){ + var body = '" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
' + i18n.sprintf(i18n._('View JSON examples at %s'), 'www.json.org') + '
' + + '' + i18n.sprintf(i18n._('View YAML examples at %s'), 'docs.ansible.com') + '
', + dataTitle: i18n._('Inventory Variables'), + dataPlacement: 'right', + dataContainer: 'body', + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + } + }, + related: { + permissions: { + name: 'permissions', + awToolTip: i18n._('Please save before assigning permissions'), + dataPlacement: 'top', + basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/access_list/', + type: 'collection', + title: i18n._('Permissions'), + iterator: 'permission', + index: false, + open: false, + search: { + order_by: 'username' + }, + actions: { + add: { + label: i18n._('Add'), + ngClick: "$state.go('.add')", + awToolTip: i18n._('Add a permission'), + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD', + ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + + } + }, + fields: { + username: { + key: true, + label: i18n._('User'), + linkBase: 'users', + class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' + }, + role: { + label: i18n._('Role'), + type: 'role', + nosort: true, + class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4', + }, + team_roles: { + label: i18n._('Team Roles'), + type: 'team_roles', + nosort: true, + class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4', + } + } + }, + hosts: { + name: 'hosts', + include: "RelatedHostsListDefinition", + title: i18n._('Hosts'), + iterator: 'host', + listState: buildHostListState, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState + }, + //this is a placeholder for when we're ready for completed jobs + completed_jobs: { + name: 'completed_jobs', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/completed_jobs/', + type: 'collection', + title: i18n._('Completed Jobs'), + iterator: 'completed_job', + index: false, + open: false, + // search: { + // order_by: 'username' + // }, + actions: { + add: { + label: i18n._('Add'), + ngClick: "$state.go('.add')", + awToolTip: i18n._('Add a permission'), + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD', + // ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + + } + }, + fields: { + name: { + label: i18n._('Name'), + // linkBase: 'users', + class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' + } + } + } + } + + };}]; diff --git a/awx/ui/client/src/inventories/insights/insights.block.less b/awx/ui/client/src/inventories/insights/insights.block.less new file mode 100644 index 0000000000..38df774da8 --- /dev/null +++ b/awx/ui/client/src/inventories/insights/insights.block.less @@ -0,0 +1,57 @@ +@import "../../shared/branding/colors.default.less"; + +.InsightsNav{ + width: 100%; + display: flex; + border: 1px solid #B7B7B7; + border-radius:5px; + flex-wrap: wrap; + font-size: 14px; + font-weight: bold; + +} + +.InsightsNav-rightSide{ + align-items: center; + display: flex; + flex: 1 0 auto; + flex-wrap: wrap; + padding: 10px 0px 10px 0px +} + +.InsightsNav-leftSide{ + align-items: center; + display: flex; + flex: 1 0 auto; + justify-content: flex-end; + flex-wrap: wrap; + max-width: 100%; +} + +.InsightsNav-totalIssues{ + background-color: @default-link; + color: @default-bg; +} + +.InsightsNav-criticalIssues{ + background-color: @default-err; +} + +.InsightsNav-highIssues{ + background-color:@default-warning; +} + +.InsightsNav-mediumIssues{ + background-color: @default-succ; +} + +.InsightsNav-lowIssues{ + background-color: @default-succ; +} + +.InsightsNav-solvableBadge{ + background-color: @b7grey; +} +.InsightsNav-solvableBadge:last-of-type{ + margin-right: 20px; +} diff --git a/awx/ui/client/src/inventories/insights/insights.controller.js b/awx/ui/client/src/inventories/insights/insights.controller.js new file mode 100644 index 0000000000..e892b62387 --- /dev/null +++ b/awx/ui/client/src/inventories/insights/insights.controller.js @@ -0,0 +1,16 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ +function () { + + function init() { + // $scope.insights + } + + init(); + +}]; diff --git a/awx/ui/client/src/inventories/insights/insights.partial.html b/awx/ui/client/src/inventories/insights/insights.partial.html new file mode 100644 index 0000000000..30268578bc --- /dev/null +++ b/awx/ui/client/src/inventories/insights/insights.partial.html @@ -0,0 +1,20 @@ +Variables defined here apply to all child groups and hosts.
" + - "Enter variables using either JSON or YAML syntax. Use the " + - "radio button to toggle between the two.
" + - "JSON:{\n" + - "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + - '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + - 'View YAML examples at docs.ansible.com
', - dataContainer: 'body', - tab: 'properties' - }, - source: { - label: 'Source', - type: 'select', - ngOptions: 'source.label for source in source_type_options track by source.value', - ngChange: 'sourceChange(source)', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - ngModel: 'source' - }, - credential: { - // initializes a default value for this search param - // search params with default values set will not generate user-interactable search tags - search: { - kind: null - }, - label: 'Cloud Credential', - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - ngShow: "source && source.value !== '' && source.value !== 'custom'", - sourceModel: 'credential', - sourceField: 'name', - ngClick: 'lookupCredential()', - awRequiredWhen: { - reqExpression: "cloudCredentialRequired", - init: "false" - }, - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - watchBasePath: "credentialBasePath" - }, - source_regions: { - label: 'Regions', - type: 'select', - ngOptions: 'source.label for source in source_region_choices track by source.value', - multiSelect: true, - ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')", - - - dataTitle: 'Source Regions', - dataPlacement: 'right', - awPopOver: "Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + - "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + - "
", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - instance_filters: { - label: 'Instance Filters', - type: 'text', - ngShow: "source && source.value == 'ec2'", - dataTitle: 'Instance Filters', - dataPlacement: 'right', - awPopOver: "Provide a comma-separated list of filter expressions. " + - "Hosts are imported to Tower when ANY of the filters match.
" + - "Limit to hosts having a tag:tag-key=TowerManaged\n" + - "Limit to hosts using either key pair:
key-name=staging, key-name=production\n" + - "Limit to hosts where the Name tag begins with test:
tag:Name=test*\n" + - "
View the Describe Instances documentation " + - "for a complete list of supported filters.
", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - group_by: { - label: 'Only Group By', - type: 'select', - ngShow: "source && source.value == 'ec2'", - ngOptions: 'source.label for source in group_by_choices track by source.value', - multiSelect: true, - dataTitle: 'Only Group By', - dataPlacement: 'right', - awPopOver: "Select which groups to create automatically. " + - "Tower will create group names similar to the following examples based on the options selected:
If blank, all groups above are created except Instance ID.
", - dataContainer: 'body', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - inventory_script: { - label : "Custom Inventory Script", - type: 'lookup', - basePath: 'inventory_scripts', - list: 'InventoryScriptsList', - ngShow: "source && source.value === 'custom'", - sourceModel: 'inventory_script', - sourceField: 'name', - awRequiredWhen: { - reqExpression: "source && source.value === 'custom'", - init: "false" - }, - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - }, - custom_variables: { - id: 'custom_variables', - label: 'Environment Variables', //"{{vars_label}}" , - ngShow: "source && source.value=='custom' ", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Environment Variables", - dataPlacement: 'right', - awPopOver: "Provide environment variables to pass to the custom inventory script.
" + - "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + - "JSON:{\n" + - "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + - '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + - 'View YAML examples at docs.ansible.com
', - dataContainer: 'body' - }, - ec2_variables: { - id: 'ec2_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'ec2'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + - "" + - "view ec2.ini in the Ansible github repo.
" + - "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + - "JSON:{\n" + - "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + - '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + - 'View YAML examples at docs.ansible.com
', - dataContainer: 'body' - }, - vmware_variables: { - id: 'vmware_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'vmware'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + - "" + - "view vmware_inventory.ini in the Ansible github repo.
" + - "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + - "JSON:{\n" + - "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + - '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + - 'View YAML examples at docs.ansible.com
', - dataContainer: 'body' - }, - openstack_variables: { - id: 'openstack_variables', - label: 'Source Variables', //"{{vars_label}}" , - ngShow: "source && source.value == 'openstack'", - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - 'default': '---', - parseTypeName: 'envParseType', - dataTitle: "Source Variables", - dataPlacement: 'right', - awPopOver: "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + - "" + - "view openstack.yml in the Ansible github repo.
" + - "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + - "JSON:{\n" + - "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + - '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + - 'View YAML examples at docs.ansible.com
', - dataContainer: 'body' - }, - checkbox_group: { - label: 'Update Options', - type: 'checkbox_group', - ngShow: "source && (source.value !== '' && source.value !== null)", - class: 'Form-checkbox--stacked', - fields: [{ - name: 'overwrite', - label: 'Overwrite', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - - - awPopOver: 'If checked, all child groups and hosts not found on the external source will be deleted from ' + - 'the local inventory.
When not checked, local child hosts and groups not found on the external source will ' + - 'remain untouched by the inventory update process.
', - dataTitle: 'Overwrite', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, { - name: 'overwrite_vars', - label: 'Overwrite Variables', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - - - awPopOver: 'If checked, all variables for child groups and hosts will be removed and replaced by those ' + - 'found on the external source.
When not checked, a merge will be performed, combining local variables with ' + - 'those found on the external source.
', - dataTitle: 'Overwrite Variables', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, { - name: 'update_on_launch', - label: 'Update on Launch', - type: 'checkbox', - ngShow: "source.value !== '' && source.value !== null", - awPopOver: 'Each time a job runs using this inventory, refresh the inventory from the selected source before ' + - 'executing job tasks.
', - dataTitle: 'Update on Launch', - dataContainer: 'body', - dataPlacement: 'right', - labelClass: 'checkbox-options', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }] - }, - update_cache_timeout: { - label: "Cache Timeout (seconds)", - id: 'source-cache-timeout', - type: 'number', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - integer: true, - min: 0, - ngShow: "source && source.value !== '' && update_on_launch", - spinner: true, - "default": 0, - awPopOver: 'Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + - 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + - 'and a new inventory sync will be performed.
', - dataTitle: 'Cache Timeout', - dataPlacement: 'right', - dataContainer: "body" - } - }, - - buttons: { - cancel: { - ngClick: 'formCancel()', - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - close: { - ngClick: 'formCancel()', - ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - save: { - ngClick: 'formSave()', - ngDisabled: true, - ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' - } - }, - - related: { - "notifications": { - include: "NotificationsList" - } - } - - }; - var itm; - - for (itm in GroupFormObject.related) { - if (GroupFormObject.related[itm].include === "NotificationsList") { - GroupFormObject.related[itm] = angular.copy(NotificationsList); - GroupFormObject.related[itm].generateList = true; - GroupFormObject.related[itm].disabled = "source === undefined || source.value === ''"; - GroupFormObject.related[itm].ngClick = "$state.go('inventoryManage.editGroup.notifications')"; - } - } - return GroupFormObject; - }; - }]; diff --git a/awx/ui/client/src/inventories/manage/groups/main.js b/awx/ui/client/src/inventories/manage/groups/main.js deleted file mode 100644 index 9423ed0c81..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/main.js +++ /dev/null @@ -1,27 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import GroupAddController from './groups-add.controller'; -import GroupEditController from './groups-edit.controller'; -import GetHostsStatusMsg from './factories/get-hosts-status-msg.factory'; -import GetSourceTypeOptions from './factories/get-source-type-options.factory'; -import GetSyncStatusMsg from './factories/get-sync-status-msg.factory'; -import GroupsCancelUpdate from './factories/groups-cancel-update.factory'; -import ViewUpdateStatus from './factories/view-update-status.factory'; -import InventoryGroups from './inventory-groups.list'; -import GroupForm from './groups.form'; - -export default -angular.module('manageGroups', []) - .factory('GetHostsStatusMsg', GetHostsStatusMsg) - .factory('GetSourceTypeOptions', GetSourceTypeOptions) - .factory('GetSyncStatusMsg', GetSyncStatusMsg) - .factory('GroupsCancelUpdate', GroupsCancelUpdate) - .factory('ViewUpdateStatus', ViewUpdateStatus) - .factory('GroupForm', GroupForm) - .value('InventoryGroups', InventoryGroups) - .controller('GroupAddController', GroupAddController) - .controller('GroupEditController', GroupEditController); diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js deleted file mode 100644 index 47ac724664..0000000000 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js +++ /dev/null @@ -1,84 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'HostManageService', 'host', 'ToJSON', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, HostManageService, host, ToJSON){ - - init(); - - function init(){ - $scope.$watch('host.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - - $scope.parseType = 'yaml'; - $scope.host = host; - $scope.variables = getVars(host.variables); - $scope.name = host.name; - $scope.description = host.description; - - ParseTypeChange({ - scope: $scope, - field_id: 'host_variables', - }); - } - - // Adding this function b/c sometimes extra vars are returned to the - // UI as a string (ex: "foo: bar"), and other times as a - // json-object-string (ex: "{"foo": "bar"}"). CodeMirror wouldn't know - // how to prettify the latter. The latter occurs when host vars were - // system generated and not user-input (such as adding a cloud host); - function getVars(str){ - - // Quick function to test if the host vars are a json-object-string, - // by testing if they can be converted to a JSON object w/o error. - function IsJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; - } - - if(str === ''){ - return '---'; - } - else if(IsJsonString(str)){ - str = JSON.parse(str); - return jsyaml.safeDump(str); - } - else if(!IsJsonString(str)){ - return str; - } - } - - $scope.formCancel = function(){ - $state.go('^'); - }; - $scope.toggleHostEnabled = function(){ - if ($scope.host.has_inventory_sources){ - return; - } - $scope.host.enabled = !$scope.host.enabled; - }; - $scope.formSave = function(){ - var json_data = ToJSON($scope.parseType, $scope.variables, true), - host = { - id: $scope.host.id, - variables: json_data, - name: $scope.name, - description: $scope.description, - enabled: $scope.host.enabled - }; - HostManageService.put(host).then(function(){ - $state.go($state.current, null, {reload: true}); - }); - }; - }]; diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js deleted file mode 100644 index 278022e002..0000000000 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js +++ /dev/null @@ -1,120 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - export default - ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryHosts', 'HostManageService', - 'hostsUrl', 'SetStatus', 'Prompt', 'Wait', 'inventoryData', '$filter', 'hostsDataset', 'GetBasePath', 'rbacUiControlService', 'QuerySet', - function($scope, $rootScope, $state, $stateParams, InventoryHosts, HostManageService, - hostsUrl, SetStatus, Prompt, Wait, inventoryData, $filter, hostsDataset, GetBasePath, rbacUiControlService, qs){ - var list = InventoryHosts; - - init(); - function init(){ - $scope.inventory_id = $stateParams.inventory_id; - - $scope.canAdd = false; - - rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/hosts") - .then(function(params) { - $scope.canAdd = params.canAdd; - }); - - // Search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = hostsDataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $scope.$watch(`${list.iterator}_dataset`, () => { - $scope.hosts - .forEach((host) => SetStatus({scope: $scope, - host: host})); - }); - - $scope.$on(`ws-jobs`, function(e, data){ - if(data.status === 'failed' || data.status === 'successful'){ - let path = hostsUrl; - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(function(searchResponse) { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); - } - }); - - // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope. - // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See: - // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the - // problem that this solves. - $scope.ncyBreadcrumbIgnore = true; - if($state.current.name === "inventoryManage.editHost") { - $scope.rowBeingEdited = $state.params.host_id; - $scope.listBeingEdited = "hosts"; - } - } - - $scope.createHost = function(){ - $state.go('inventoryManage.addHost'); - }; - $scope.editHost = function(id){ - $state.go('inventoryManage.editHost', {host_id: id}); - }; - $scope.deleteHost = function(id, name){ - var body = '" + + i18n._("Indicates if a host is available and should be included in running jobs.") + + "
" + + i18n._("For hosts that are part of an external" + + " inventory, this flag cannot be changed. It will be" + + " set by the inventory sync process.") + + "
", + dataTitle: i18n._('Host Enabled'), + ngDisabled: 'host.has_inventory_sources' + } + }, + fields: { + name: { + label: i18n._('Host Name'), + type: 'text', + required: true, + awPopOver: "" + + i18n._("Provide a host name, ip address, or ip address:port. Examples include:") + + "
" + + "myserver.domain.com", + dataTitle: i18n._('Host Name'), + dataPlacement: 'right', + dataContainer: 'body', + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + description: { + label: i18n._('Description'), + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)', + type: 'text' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + rows: 6, + class: 'Form-formGroup--fullWidth', + "default": "---", + awPopOver: "
" + + "127.0.0.1
" + + "10.1.0.140:25
" + + "server.example.com:25" + + "
" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
' + i18n.sprintf(i18n._('View JSON examples at %s'), 'www.json.org') + '
' + + '' + i18n.sprintf(i18n._('View YAML examples at %s'), 'docs.ansible.com') + '
', + dataTitle: i18n._('Host Variables'), + dataPlacement: 'right', + dataContainer: 'body' + }, + inventory: { + type: 'hidden', + includeOnEdit: true, + includeOnAdd: true + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + } + }, + + related: { + ansible_facts: { + name: 'ansible_facts', + title: i18n._('Facts'), + skipGenerator: true + }, + nested_groups: { + name: 'nested_groups', + ngClick: "$state.go('inventories.edit.hosts.edit.nested_groups')", + include: "NestedGroupListDefinition", + includeForm: "NestedGroupFormDefinition", + title: i18n._('Groups'), + iterator: 'nested_group', + listState: nestedGroupListState + }, + insights: { + name: 'insights', + title: i18n._('Insights'), + skipGenerator: true + } + } + }; + }]; diff --git a/awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js similarity index 83% rename from awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js rename to awx/ui/client/src/inventories/related-hosts/related-host.list.js index a2793638a3..3a83afc195 100644 --- a/awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -1,5 +1,5 @@ /************************************************* - * Copyright (c) 2015 Ansible, Inc. + * Copyright (c) 2017 Ansible, Inc. * * All Rights Reserved *************************************************/ @@ -8,16 +8,15 @@ export default { name: 'hosts', iterator: 'host', editTitle: '{{ selected_group }}', - listTitle: 'HOSTS', - searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', showTitle: false, well: true, + wellOverride: true, index: false, hover: true, - hasChildren: true, - 'class': 'table-no-border', + // hasChildren: true, multiSelect: true, trackBy: 'host.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', fields: { active_failures: { @@ -84,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 host or a selection of multiple hosts.", + dataPlacement: 'top', + 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()', diff --git a/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js b/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js new file mode 100644 index 0000000000..234fa72bf7 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js @@ -0,0 +1,46 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import SourcesAddController from './sources-add.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.add`, + url: `/add`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'sourcesForm@inventories': { + templateProvider: function(GenerateForm, SourcesFormDefinition) { + let form = SourcesFormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: SourcesAddController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/sources/add/main.js b/awx/ui/client/src/inventories/sources/add/main.js new file mode 100644 index 0000000000..134eaf5214 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesAddState from './build-sources-add-state.factory'; +import controller from './sources-add.controller'; + +export default +angular.module('sourcesAdd', []) + .factory('buildSourcesAddState', buildSourcesAddState) + .controller('SourcesAddController', controller); diff --git a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js new file mode 100644 index 0000000000..ad98656eec --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -0,0 +1,192 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', + 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'rbacUiControlService', 'ToJSON', 'SourcesService', + function($state, $stateParams, $scope, SourcesFormDefinition, ParseTypeChange, + GenerateForm, inventoryData, GroupManageService, GetChoices, + GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, + ToJSON, SourcesService) { + + let form = SourcesFormDefinition; + init(); + + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/inventory_sources") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + field_id: 'inventory_source_variables', + variable: 'variables', + }); + initSources(); + } + + $scope.lookupCredential = function(){ + let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; + $state.go('.credential', { + credential_search: { + kind: kind, + page_size: '5', + page: '1' + } + }); + }; + + $scope.formCancel = function() { + $state.go('^'); + }; + + $scope.formSave = function() { + var params, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + params = { + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id, + instance_filters: $scope.instance_filters, + source_script: $scope.inventory_script, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + update_on_launch: $scope.update_on_launch, + update_cache_timeout: $scope.update_cache_timeout || 0, + variables: json_data, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(',') + }; + + if ($scope.source) { + params.source_vars = $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables']; + params.source = $scope.source.value; + } else { + params.source = null; + } + SourcesService.post(params).then(function(res){ + let inventory_source_id = res.data.id; + $state.go('^.edit', {inventory_source_id: inventory_source_id}, {reload: true}); + }); + }; + $scope.sourceChange = function(source) { + source = source.value; + if (source === 'custom'){ + $scope.credentialBasePath = GetBasePath('inventory_script'); + } + // equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' + else{ + $scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); + } + if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack') { + ParseTypeChange({ + scope: $scope, + field_id: source + '_variables', + variable: source + '_variables', + parse_variable: 'envParseType' + }); + } + + // reset fields + $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; + // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint + $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; + $scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false; + $scope.group_by = null; + $scope.source_regions = null; + $scope.credential = null; + $scope.credential_name = null; + initRegionSelect(); + }; + // region / source options callback + $scope.$on('choicesReadyGroup', function() { + initRegionSelect(); + }); + + $scope.$on('sourceTypeOptionsReady', function() { + initSourceSelect(); + }); + + function initRegionSelect(){ + CreateSelect2({ + element: '#inventory_source_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#inventory_source_group_by', + multiple: true + }); + } + function initSourceSelect(){ + CreateSelect2({ + element: '#inventory_source_source', + multiple: false + }); + } + + function initSources(){ + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'rax_regions', + choice_name: 'rax_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'ec2_regions', + choice_name: 'ec2_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'gce_regions', + choice_name: 'gce_region_choices', + callback: 'choicesReadyGroup' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'azure_regions', + choice_name: 'azure_region_choices', + callback: 'choicesReadyGroup' + }); + + // Load options for group_by + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'group_by', + variable: 'ec2_group_by', + choice_name: 'ec2_group_by_choices', + callback: 'choicesReadyGroup' + }); + GetSourceTypeOptions({ + scope: $scope, + variable: 'source_type_options', + //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref + }); + } + } +]; diff --git a/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js b/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js new file mode 100644 index 0000000000..5862650f27 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js @@ -0,0 +1,49 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import SourcesEditController from './sources-edit.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + function($stateExtender, templateUrl, $injector){ + var val = function(field, formStateDefinition, params) { + let state, + list = field.include ? $injector.get(field.include) : field, + breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), + stateConfig = { + name: `${formStateDefinition.name}.${list.iterator}s.edit`, + url: `/edit/:inventory_source_id`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'groupForm@inventories': { + templateProvider: function(GenerateForm, SourcesFormDefinition) { + let form = SourcesFormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'edit', + related: false + }); + }, + controller: SourcesEditController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }], + inventorySourceData: ['$stateParams', 'SourcesService', function($stateParams, SourcesService) { + return SourcesService.get({id: $stateParams.inventory_source_id }).then(res => res.data.results[0]); + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/sources/edit/main.js b/awx/ui/client/src/inventories/sources/edit/main.js new file mode 100644 index 0000000000..eb130001d5 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesEditState from './build-sources-edit-state.factory'; +import controller from './sources-edit.controller'; + +export default +angular.module('sourcesEdit', []) + .factory('buildSourcesEditState', buildSourcesEditState) + .controller('SourcesEditController', controller); diff --git a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js new file mode 100644 index 0000000000..220e89a0f9 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -0,0 +1,246 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'ParseVariableString', + 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'inventorySourceData', 'SourcesService', 'inventoryData', + function($state, $stateParams, $scope, ParseVariableString, + rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, + GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, + inventorySourceData, SourcesService, inventoryData) { + + init(); + + function init() { + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/inventory_sources") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + // instantiate expected $scope values from inventorySourceData + _.assign($scope, { credential: inventorySourceData.credential }, { overwrite: inventorySourceData.overwrite }, { overwrite_vars: inventorySourceData.overwrite_vars }, { update_on_launch: inventorySourceData.update_on_launch }, { update_cache_timeout: inventorySourceData.update_cache_timeout }, { instance_filters: inventorySourceData.instance_filters }, { inventory_script: inventorySourceData.source_script }); + if (inventorySourceData.credential) { + $scope.credential_name = inventorySourceData.summary_fields.credential.name; + } + + // display custom inventory_script name + if (inventorySourceData.source === 'custom') { + $scope.inventory_script_name = inventorySourceData.summary_fields.source_script.name; + } + $scope = angular.extend($scope, inventorySourceData); + + $scope.$watch('summary_fields.user_capabilities.edit', function(val) { + $scope.canAdd = val; + }); + + // init codemirror(s) + $scope.variables = $scope.variables === null || $scope.variables === '' ? '---' : ParseVariableString($scope.variables); + $scope.parseType = 'yaml'; + $scope.envParseType = 'yaml'; + + ParseTypeChange({ + scope: $scope, + field_id: 'inventory_source_variables', + variable: 'variables', + }); + + initSources(); + } + + var initRegionSelect = function() { + CreateSelect2({ + element: '#inventory_source_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#inventory_source_group_by', + multiple: true + }); + }; + + $scope.lookupCredential = function(){ + let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; + $state.go('.credential', { + credential_search: { + kind: kind, + page_size: '5', + page: '1' + } + }); + }; + + $scope.formCancel = function() { + $state.go('^'); + }; + $scope.formSave = function() { + var params, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + params = { + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id, + instance_filters: $scope.instance_filters, + source_script: $scope.inventory_script, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + update_on_launch: $scope.update_on_launch, + update_cache_timeout: $scope.update_cache_timeout || 0, + variables: json_data, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(',') + }; + + if ($scope.source) { + params.source_vars = $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables']; + params.source = $scope.source.value; + } else { + params.source = null; + } + // switch (source) { + // no inventory source set, just create a new group + // '' is the value supplied for Manual source type + // case null || '': + SourcesService.put(params).then(() => $state.go('.', null, { reload: true })); + // break; + // // create a new group and create/associate an inventory source + // // equal to case 'rax' || 'ec2' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' || 'custom' + // default: + // GroupManageService.put(group) + // .then(() => GroupManageService.putInventorySource(params, groupData.related.inventory_source)) + // .then(() => $state.go($state.current, null, { reload: true })); + // break; + // } + }; + + $scope.sourceChange = function(source) { + $scope.source = source; + if (source.value === 'ec2' || source.value === 'custom' || + source.value === 'vmware' || source.value === 'openstack') { + $scope[source.value + '_variables'] = $scope[source.value + '_variables'] === (null || undefined) ? '---' : $scope[source.value + '_variables']; + ParseTypeChange({ + scope: $scope, + field_id: source.value + '_variables', + variable: source.value + '_variables', + parse_variable: 'envParseType', + }); + } + // reset fields + // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint + $scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions']; + $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false; + $scope.group_by = null; + $scope.source_regions = null; + $scope.credential = null; + $scope.credential_name = null; + initRegionSelect(); + }; + + function initSourceSelect() { + $scope.source = _.find($scope.source_type_options, { value: inventorySourceData.source }); + CreateSelect2({ + element: '#inventory_source_source', + multiple: false + }); + // After the source is set, conditional fields will be visible + // CodeMirror is buggy if you instantiate it in a not-visible element + // So we initialize it here instead of the init() routine + if (inventorySourceData.source === 'ec2' || inventorySourceData.source === 'openstack' || + inventorySourceData.source === 'custom' || inventorySourceData.source === 'vmware') { + $scope[inventorySourceData.source + '_variables'] = inventorySourceData.source_vars === null || inventorySourceData.source_vars === '' ? '---' : ParseVariableString(inventorySourceData.source_vars); + ParseTypeChange({ + scope: $scope, + field_id: inventorySourceData.source + '_variables', + variable: inventorySourceData.source + '_variables', + parse_variable: 'envParseType', + }); + } + } + + function initRegionData() { + var source = $scope.source.value === 'azure_rm' ? 'azure' : $scope.source.value; + var regions = inventorySourceData.source_regions.split(','); + // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint + $scope.source_region_choices = $scope[source + '_regions']; + + // the API stores azure regions as all-lowercase strings - but the azure regions received from OPTIONS are Snake_Cased + if (source === 'azure') { + $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value.toLowerCase() === region)); + } + // all other regions are 1-1 + else { + $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value === region)); + } + $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; + if (source === 'ec2') { + var group_by = inventorySourceData.group_by.split(','); + $scope.group_by = _.map(group_by, (item) => _.find($scope.ec2_group_by, { value: item })); + } + initRegionSelect(); + } + + function initSources() { + GetSourceTypeOptions({ + scope: $scope, + variable: 'source_type_options', + //callback: 'sourceTypeOptionsReady' this callback is hard-coded into GetSourceTypeOptions(), included for ref + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'rax_regions', + choice_name: 'rax_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'ec2_regions', + choice_name: 'ec2_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'gce_regions', + choice_name: 'gce_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source_regions', + variable: 'azure_regions', + choice_name: 'azure_region_choices', + callback: 'choicesReadyGroup' + }); + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'group_by', + variable: 'ec2_group_by', + choice_name: 'ec2_group_by_choices', + callback: 'choicesReadyGroup' + }); + } + + // region / source options callback + $scope.$on('choicesReadyGroup', function() { + if (angular.isObject($scope.source)) { + initRegionData(); + } + }); + + $scope.$on('sourceTypeOptionsReady', function() { + initSourceSelect(); + }); + } +]; diff --git a/awx/ui/client/src/inventories/sources/factories/cancel-source-update.factory.js b/awx/ui/client/src/inventories/sources/factories/cancel-source-update.factory.js new file mode 100644 index 0000000000..a0fb73e7eb --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/cancel-source-update.factory.js @@ -0,0 +1,63 @@ +export default + function CancelSourceUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + inventory_source = params.inventory_source; + + // Cancel the update process + if (Empty(inventory_source)) { + inventory_source = Find({ list: scope.inventory_sources, key: 'id', val: id }); + scope.selected_inventory_source_id = inventory_source.id; + } + + if (inventory_source && (inventory_source.status === 'running' || inventory_source.status === 'pending')) { + // We found the inventory_source, and there is a running update + Wait('start'); + Rest.setUrl(inventory_source.url); + Rest.get() + .success(function (data) { + // Check that we have access to cancelling an update + var url = (data.related.current_update) ? data.related.current_update : data.related.last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + // Cancel the update process + Rest.setUrl(url); + Rest.post() + .success(function () { + Wait('stop'); + //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + + // 'Click the button to monitor the status.', 'alert-info'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST status: ' + status + }); + }); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + inventory_source.url + ' failed. GET status: ' + status + }); + }); + } + }; + } + +CancelSourceUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js similarity index 100% rename from awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js rename to awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js similarity index 71% rename from awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js rename to awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js index 2541abcc27..efb393caf8 100644 --- a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js +++ b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js @@ -1,9 +1,7 @@ export default - function GetSyncStatusMsg(Empty) { + function GetSyncStatusMsg() { return function(params) { var status = params.status, - source = params.source, - has_inventory_sources = params.has_inventory_sources, launch_class = '', launch_tip = 'Start sync process', schedule_tip = 'Schedule future inventory syncs', @@ -49,19 +47,6 @@ export default break; } - if (has_inventory_sources && Empty(source)) { - // parent has a source, therefore this group should not have a source - launch_class = "btn-disabled"; - status_tip = 'Managed by an external cloud source.'; - launch_tip = 'Can only be updated by running a sync on the parent group.'; - } - - if (has_inventory_sources === false && Empty(source)) { - launch_class = 'btn-disabled'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - } - return { "class": stat_class, "tooltip": status_tip, @@ -73,5 +58,4 @@ export default }; } -GetSyncStatusMsg.$inject = - [ 'Empty' ]; +GetSyncStatusMsg.$inject = []; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js similarity index 52% rename from awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js rename to awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js index 1f3280b51c..5a746aa075 100644 --- a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js +++ b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js @@ -2,37 +2,26 @@ export default function ViewUpdateStatus($state, Rest, ProcessErrors, Alert, Wait, Empty, Find) { return function(params) { var scope = params.scope, - group_id = params.group_id, - group = Find({ list: scope.groups, key: 'id', val: group_id }); + inventory_source_id = params.inventory_source_id, + inventory_source = Find({ list: scope.inventory_sources, key: 'id', val: inventory_source_id }); - if (scope.removeSourceReady) { - scope.removeSourceReady(); - } - scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { - - // Get the ID from the correct summary field - var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; - - $state.go('inventorySyncStdout', {id: update_id}); - - }); - - if (group) { - if (Empty(group.source)) { - // do nothing - } else if (Empty(group.status) || group.status === "never updated") { + if (inventory_source) { + if (Empty(inventory_source.status) || inventory_source.status === "never updated") { Alert('No Status Available', 'Variables defined here apply to all child groups and hosts.
" + + "Enter variables using either JSON or YAML syntax. Use the " + + "radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body', + tab: 'properties' + }, + source: { + label: 'Source', + type: 'select', + ngOptions: 'source.label for source in source_type_options track by source.value', + ngChange: 'sourceChange(source)', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + ngModel: 'source' + }, + credential: { + // initializes a default value for this search param + // search params with default values set will not generate user-interactable search tags + search: { + kind: null + }, + label: 'Cloud Credential', + type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + ngShow: "source && source.value !== '' && source.value !== 'custom'", + sourceModel: 'credential', + sourceField: 'name', + ngClick: 'lookupCredential()', + awRequiredWhen: { + reqExpression: "cloudCredentialRequired", + init: "false" + }, + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + watchBasePath: "credentialBasePath" + }, + source_regions: { + label: 'Regions', + type: 'select', + ngOptions: 'source.label for source in source_region_choices track by source.value', + multiSelect: true, + ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure' || source.value == 'azure_rm')", + + + dataTitle: 'Source Regions', + dataPlacement: 'right', + awPopOver: "Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, " + + "or choose All to include all regions. Tower will only be updated with Hosts associated with the selected regions." + + "
", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + instance_filters: { + label: 'Instance Filters', + type: 'text', + ngShow: "source && source.value == 'ec2'", + dataTitle: 'Instance Filters', + dataPlacement: 'right', + awPopOver: "Provide a comma-separated list of filter expressions. " + + "Hosts are imported to Tower when ANY of the filters match.
" + + "Limit to hosts having a tag:tag-key=TowerManaged\n" + + "Limit to hosts using either key pair:
key-name=staging, key-name=production\n" + + "Limit to hosts where the Name tag begins with test:
tag:Name=test*\n" + + "
View the Describe Instances documentation " + + "for a complete list of supported filters.
", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + group_by: { + label: 'Only Group By', + type: 'select', + ngShow: "source && source.value == 'ec2'", + ngOptions: 'source.label for source in group_by_choices track by source.value', + multiSelect: true, + dataTitle: 'Only Group By', + dataPlacement: 'right', + awPopOver: "Select which groups to create automatically. " + + "Tower will create group names similar to the following examples based on the options selected:
If blank, all groups above are created except Instance ID.
", + dataContainer: 'body', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + inventory_script: { + label : "Custom Inventory Script", + type: 'lookup', + basePath: 'inventory_scripts', + list: 'InventoryScriptsList', + ngShow: "source && source.value === 'custom'", + sourceModel: 'inventory_script', + sourceField: 'name', + awRequiredWhen: { + reqExpression: "source && source.value === 'custom'", + init: "false" + }, + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + }, + custom_variables: { + id: 'custom_variables', + label: 'Environment Variables', //"{{vars_label}}" , + ngShow: "source && source.value=='custom' ", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Environment Variables", + dataPlacement: 'right', + awPopOver: "Provide environment variables to pass to the custom inventory script.
" + + "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body' + }, + ec2_variables: { + id: 'ec2_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'ec2'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + + "" + + "view ec2.ini in the Ansible github repo.
" + + "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body' + }, + vmware_variables: { + id: 'vmware_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'vmware'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + + "" + + "view vmware_inventory.ini in the Ansible github repo.
" + + "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body' + }, + openstack_variables: { + id: 'openstack_variables', + label: 'Source Variables', //"{{vars_label}}" , + ngShow: "source && source.value == 'openstack'", + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + 'default': '---', + parseTypeName: 'envParseType', + dataTitle: "Source Variables", + dataPlacement: 'right', + awPopOver: "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + + "" + + "view openstack.yml in the Ansible github repo.
" + + "Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
View JSON examples at www.json.org
' + + 'View YAML examples at docs.ansible.com
', + dataContainer: 'body' + }, + checkbox_group: { + label: 'Update Options', + type: 'checkbox_group', + ngShow: "source && (source.value !== '' && source.value !== null)", + class: 'Form-checkbox--stacked', + fields: [{ + name: 'overwrite', + label: 'Overwrite', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + + + awPopOver: 'If checked, all child groups and hosts not found on the external source will be deleted from ' + + 'the local inventory.
When not checked, local child hosts and groups not found on the external source will ' + + 'remain untouched by the inventory update process.
', + dataTitle: 'Overwrite', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, { + name: 'overwrite_vars', + label: 'Overwrite Variables', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + + + awPopOver: 'If checked, all variables for child groups and hosts will be removed and replaced by those ' + + 'found on the external source.
When not checked, a merge will be performed, combining local variables with ' + + 'those found on the external source.
', + dataTitle: 'Overwrite Variables', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, { + name: 'update_on_launch', + label: 'Update on Launch', + type: 'checkbox', + ngShow: "source.value !== '' && source.value !== null", + awPopOver: 'Each time a job runs using this inventory, refresh the inventory from the selected source before ' + + 'executing job tasks.
', + dataTitle: 'Update on Launch', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }] + }, + update_cache_timeout: { + label: "Cache Timeout (seconds)", + id: 'source-cache-timeout', + type: 'number', + ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', + integer: true, + min: 0, + ngShow: "source && source.value !== '' && update_on_launch", + spinner: true, + "default": 0, + awPopOver: 'Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + + 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + + 'and a new inventory sync will be performed.
', + dataTitle: 'Cache Timeout', + dataPlacement: 'right', + dataContainer: "body" + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(group_obj.summary_fields.user_capabilities.edit || canAdd)' + } + } +}; diff --git a/awx/ui/client/src/inventories/sources/sources.list.js b/awx/ui/client/src/inventories/sources/sources.list.js new file mode 100644 index 0000000000..157477bdd1 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -0,0 +1,127 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'inventory_sources', + iterator: 'inventory_source', + editTitle: '{{ inventory_source.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + trackBy: 'inventory_source.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', + + fields: { + sync_status: { + label: '', + nosort: true, + mode: 'all', + iconOnly: true, + ngClick: 'viewUpdateStatus(inventory_source.id)', + awToolTip: "{{ inventory_source.status_tooltip }}", + dataTipWatch: "inventory_source.status_tooltip", + icon: "{{ 'fa icon-cloud-' + inventory_source.status_class }}", + ngClass: "inventory_source.status_class", + dataPlacement: "top", + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + label: 'Sources', + key: true, + ngClick: "groupSelect(inventory_source.id)", + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', + class: 'InventoryManage-breakWord', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + create: { + mode: 'all', + ngClick: "createSource()", + awToolTip: "Create a new source", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD SOURCE', + ngShow: 'canAdd', + dataPlacement: "top", + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', + + group_update: { + //label: 'Sync', + mode: 'all', + ngClick: 'updateSource(inventory_source)', + awToolTip: "{{ inventory_source.launch_tooltip }}", + dataTipWatch: "inventory_source.launch_tooltip", + ngShow: "(inventory_source.status !== 'running' && inventory_source.status " + + "!== 'pending' && inventory_source.status !== 'updating') && inventory_source.summary_fields.user_capabilities.start", + ngClass: "inventory_source.launch_class", + dataPlacement: "top", + }, + cancel: { + //label: 'Cancel', + mode: 'all', + ngClick: "cancelUpdate(inventory_source.id)", + awToolTip: "Cancel sync process", + 'class': 'red-txt', + ngShow: "(inventory_source.status == 'running' || inventory_source.status == 'pending' " + + "|| inventory_source.status == 'updating') && inventory_source.summary_fields.user_capabilities.start", + dataPlacement: "top", + iconClass: "fa fa-minus-circle" + }, + copy: { + mode: 'all', + ngClick: "copyMoveSource(inventory_source.id)", + awToolTip: 'Copy or move source', + ngShow: "inventory_source.id > 0 && inventory_source.summary_fields.user_capabilities.copy", + dataPlacement: "top" + }, + schedule: { + mode: 'all', + ngClick: "scheduleSource(inventory_source.id)", + awToolTip: "{{ inventory_source.group_schedule_tooltip }}", + ngClass: "inventory_source.scm_type_class", + dataPlacement: 'top', + ngShow: "!(inventory_source.summary_fields.inventory_source.source === '')" + }, + edit: { + //label: 'Edit', + mode: 'all', + ngClick: "editSource(inventory_source.id)", + awToolTip: 'Edit source', + dataPlacement: "top", + ngShow: "inventory_source.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editSource(inventory_source.id)", + awToolTip: 'View source', + dataPlacement: "top", + ngShow: "!inventory_source.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteSource(inventory_source)", + awToolTip: 'Delete source', + dataPlacement: "top", + ngShow: "inventory_source.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/sources/sources.service.js b/awx/ui/client/src/inventories/sources/sources.service.js new file mode 100644 index 0000000000..a913951806 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.service.js @@ -0,0 +1,113 @@ +export default + ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){ + return { + stringifyParams: function(params){ + return _.reduce(params, (result, value, key) => { + return result + key + '=' + value + '&'; + }, ''); + }, + // cute abstractions via fn.bind() + url: function(){ + return ''; + }, + error: function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + this.url + '. GET returned: ' + status }); + }, + success: function(data){ + return data; + }, + // HTTP methods + get: function(params){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + '?' + this.stringifyParams(params); + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + post: function(inventory_source){ + Wait('start'); + this.url = GetBasePath('inventory_sources'); + Rest.setUrl(this.url); + return Rest.post(inventory_source) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + put: function(inventory_source){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + inventory_source.id; + Rest.setUrl(this.url); + return Rest.put(inventory_source) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + delete: function(id){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + id; + Rest.setUrl(this.url); + return Rest.destroy() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + getCredential: function(id){ + Wait('start'); + this.url = GetBasePath('credentials') + id; + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + getInventorySource: function(params){ + Wait('start'); + this.url = GetBasePath('inventory_sources') + '?' + this.stringifyParams(params); + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + putInventorySource: function(params, url){ + Wait('start'); + this.url = url; + Rest.setUrl(this.url); + return Rest.put(params) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + // these relationship setters could be consolidated, but verbosity makes the operation feel more clear @ controller level + associateGroup: function(group, target){ + Wait('start'); + this.url = GetBasePath('groups') + target + '/children/'; + Rest.setUrl(this.url); + return Rest.post(group) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + disassociateGroup: function(group, parent){ + Wait('start'); + this.url = GetBasePath('groups') + parent + '/children/'; + Rest.setUrl(this.url); + return Rest.post({id: group, disassociate: 1}) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + promote: function(group, inventory){ + Wait('start'); + this.url = GetBasePath('inventory') + inventory + '/groups/'; + Rest.setUrl(this.url); + return Rest.post({id: group, disassociate: 1}) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + } + }; + }]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index 88e90f6007..c08c8f2838 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -157,7 +157,7 @@ export default // If we are on the inventory manage page or any child state of that // page then we want to stay on that page. Otherwise go to the stdout // view. - if(!$state.includes('inventoryManage')) { + if(!$state.includes('inventories.edit')) { goTojobResults('inventorySyncStdout'); } } diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 63d813ff12..8691596e52 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1493,7 +1493,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if(this.mode === "edit"){ html += `