From f9b46b7c3d17a7554d947cbeee6015ef3445d7a5 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 5 Apr 2017 11:45:02 -0400 Subject: [PATCH 01/49] Set up new inventory directory and states --- awx/ui/client/legacy-styles/forms.less | 4 + awx/ui/client/src/app.js | 2 + .../list/inventory-list.controller.js | 2 +- .../add/inventory-add.controller.js | 104 ++++++ awx/ui/client/src/inventoriesnew/add/main.js | 11 + .../edit/inventory-edit.controller.js | 136 ++++++++ awx/ui/client/src/inventoriesnew/edit/main.js | 11 + .../hosts/add/host-add.controller.js | 14 + .../src/inventoriesnew/hosts/add/main.js | 11 + .../hosts/edit/host-edit.controller.js | 14 + .../src/inventoriesnew/hosts/edit/main.js | 11 + .../src/inventoriesnew/hosts/host.form.js | 103 ++++++ .../src/inventoriesnew/hosts/host.list.js | 121 +++++++ .../hosts/list/host-list.controller.js | 80 +++++ .../src/inventoriesnew/hosts/list/main.js | 11 + .../client/src/inventoriesnew/hosts/main.js | 20 ++ .../inventoriesnew/inventories.partial.html | 14 + .../src/inventoriesnew/inventory.form.js | 135 ++++++++ .../src/inventoriesnew/inventory.list.js | 98 ++++++ .../list/inventory-list.controller.js | 308 ++++++++++++++++++ awx/ui/client/src/inventoriesnew/list/main.js | 11 + awx/ui/client/src/inventoriesnew/main.js | 236 ++++++++++++++ .../src/main-menu/main-menu.partial.html | 17 + .../src/shared/stateDefinitions.factory.js | 21 +- 24 files changed, 1485 insertions(+), 10 deletions(-) create mode 100644 awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/add/main.js create mode 100644 awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/edit/main.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/add/host-add.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/add/main.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/edit/main.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/host.form.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/host.list.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/list/main.js create mode 100644 awx/ui/client/src/inventoriesnew/hosts/main.js create mode 100644 awx/ui/client/src/inventoriesnew/inventories.partial.html create mode 100644 awx/ui/client/src/inventoriesnew/inventory.form.js create mode 100644 awx/ui/client/src/inventoriesnew/inventory.list.js create mode 100644 awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js create mode 100644 awx/ui/client/src/inventoriesnew/list/main.js create mode 100644 awx/ui/client/src/inventoriesnew/main.js 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/src/app.js b/awx/ui/client/src/app.js index 0649023b54..f5eaf2080b 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -40,6 +40,7 @@ if ($basePath) { import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventories from './inventories/main'; +import inventorynew from './inventoriesnew/main'; import inventoryScripts from './inventory-scripts/main'; import credentialTypes from './credential-types/main'; import organizations from './organizations/main'; @@ -99,6 +100,7 @@ var tower = angular.module('Tower', [ configuration.name, systemTracking.name, inventories.name, + inventorynew.name, inventoryScripts.name, credentialTypes.name, organizations.name, diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 2f43f48100..d900048060 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -250,7 +250,7 @@ function InventoriesList($scope, $rootScope, $location, }; $scope.addInventory = function () { - $state.go('inventories.add'); + $state.go('inventoriesnew.add'); }; $scope.editInventory = function (id) { diff --git a/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js b/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js new file mode 100644 index 0000000000..17f42276d0 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js @@ -0,0 +1,104 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +function InventoriesAdd($scope, $location, + GenerateForm, InventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, + $state) { + + $scope.canAdd = false; + rbacUiControlService.canAdd(GetBasePath('inventory')) + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + Rest.setUrl(GetBasePath('inventory')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add an inventory.', 'alert-info'); + } + }); + + ClearScope(); + + // Inject dynamic view + var defaultUrl = GetBasePath('inventory'), + form = InventoryForm; + + init(); + + function init() { + $scope.canEditOrg = true; + form.formLabelSize = null; + form.formFieldSize = null; + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'variables', + parse_variable: 'parseType', + field_id: 'inventory_variables' + }); + } + + // Save + $scope.formSave = function() { + Wait('start'); + try { + var fld, json_data, data; + + json_data = ToJSON($scope.parseType, $scope.variables, true); + + data = {}; + for (fld in form.fields) { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + + Rest.setUrl(defaultUrl); + Rest.post(data) + .success(function(data) { + var inventory_id = data.id; + Wait('stop'); + $location.path('/inventoriesnew/' + inventory_id); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new inventory. Post returned status: ' + status + }); + }); + } catch (err) { + Wait('stop'); + Alert("Error", "Error parsing inventory variables. Parser returned: " + err); + } + + }; + + $scope.formCancel = function() { + $state.go('inventoriesnew'); + }; +} + +export default ['$scope', '$location', + 'GenerateForm', 'InventoryForm', 'rbacUiControlService', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', + 'Wait', 'ToJSON', '$state', InventoriesAdd +]; diff --git a/awx/ui/client/src/inventoriesnew/add/main.js b/awx/ui/client/src/inventoriesnew/add/main.js new file mode 100644 index 0000000000..2d3c391495 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/add/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './inventory-add.controller'; + +export default +angular.module('newInventoryAdd', []) + .controller('NewInventoryAddController', controller); diff --git a/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js b/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js new file mode 100644 index 0000000000..98c1549499 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js @@ -0,0 +1,136 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +function InventoriesEdit($scope, $location, + $stateParams, InventoriesNewForm, Rest, ProcessErrors, + ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, + ParseVariableString, $state, OrgAdminLookup) { + + // Inject dynamic view + var defaultUrl = GetBasePath('inventory'), + form = InventoriesNewForm, + inventory_id = $stateParams.inventory_id, + master = {}, + fld, json_data, data; + + ClearScope(); + init(); + + function init() { + ClearScope(); + form.formLabelSize = null; + form.formFieldSize = null; + $scope.inventory_id = inventory_id; + + $scope.$watch('inventory_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + } + + + Wait('start'); + Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); + Rest.get() + .success(function(data) { + var fld; + for (fld in form.fields) { + if (fld === 'variables') { + $scope.variables = ParseVariableString(data.variables); + master.variables = $scope.variables; + } else if (fld === 'inventory_name') { + $scope[fld] = data.name; + master[fld] = $scope[fld]; + } else if (fld === 'inventory_description') { + $scope[fld] = data.description; + master[fld] = $scope[fld]; + } else if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = $scope[fld]; + } + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + + Wait('stop'); + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'variables', + parse_variable: 'parseType', + field_id: 'inventory_variables' + }); + + OrgAdminLookup.checkForAdminAccess({organization: data.organization}) + .then(function(canEditOrg){ + $scope.canEditOrg = canEditOrg; + }); + + $scope.inventory_obj = data; + $scope.name = data.name; + + $scope.$emit('inventoryLoaded'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status + }); + }); + // Save + $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) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + + Rest.setUrl(defaultUrl + inventory_id + '/'); + Rest.put(data) + .success(function() { + Wait('stop'); + $state.go($state.current, {}, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to update inventory. PUT returned status: ' + status + }); + }); + }; + + $scope.formCancel = function() { + $state.go('inventoriesnew'); + }; + +} + +export default ['$scope', '$location', + '$stateParams', 'InventoriesNewForm', 'Rest', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', + 'ToJSON', 'ParseVariableString', + '$state', 'OrgAdminLookup', InventoriesEdit, +]; diff --git a/awx/ui/client/src/inventoriesnew/edit/main.js b/awx/ui/client/src/inventoriesnew/edit/main.js new file mode 100644 index 0000000000..a6a404a869 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/edit/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './inventory-edit.controller'; + +export default + angular.module('newInventoryEdit', []) + .controller('NewInventoryEditController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/add/host-add.controller.js b/awx/ui/client/src/inventoriesnew/hosts/add/host-add.controller.js new file mode 100644 index 0000000000..151d75401d --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/add/host-add.controller.js @@ -0,0 +1,14 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +function HostsAdd($scope) { + +console.log('inside host add'); + +} + +export default ['$scope', HostsAdd +]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/add/main.js b/awx/ui/client/src/inventoriesnew/hosts/add/main.js new file mode 100644 index 0000000000..8205e986c6 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/add/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './host-add.controller'; + +export default +angular.module('hostsAdd', []) + .controller('NewHostAddController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js new file mode 100644 index 0000000000..d1d1092085 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js @@ -0,0 +1,14 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +function HostsEdit($scope) { + + console.log('inside host edit'); + +} + +export default ['$scope', HostsEdit +]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/edit/main.js b/awx/ui/client/src/inventoriesnew/hosts/edit/main.js new file mode 100644 index 0000000000..420e870e99 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/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('NewHostEditController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/host.form.js b/awx/ui/client/src/inventoriesnew/hosts/host.form.js new file mode 100644 index 0000000000..efb64a4785 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/host.form.js @@ -0,0 +1,103 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Hosts + * @description This form is for adding/editing a host on the inventory page +*/ + +export default ['i18n', function(i18n) { + return { + + addTitle: i18n._('CREATE HOST'), + editTitle: '{{ host.name }}', + name: 'host', + basePath: 'hosts', + well: false, + formLabelSize: 'col-lg-3', + formFieldSize: 'col-lg-9', + iterator: 'host', + headerFields:{ + enabled: { + class: 'Form-header-field', + ngClick: 'toggleHostEnabled(host)', + type: 'toggle', + 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.") + + "

", + 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
" + + "127.0.0.1
" + + "10.1.0.140:25
" + + "server.example.com:25" + + "
", + 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: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

' + 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)' + } + }, + }; + }]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/host.list.js b/awx/ui/client/src/inventoriesnew/hosts/host.list.js new file mode 100644 index 0000000000..632a38129b --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/host.list.js @@ -0,0 +1,121 @@ +/************************************************* + * 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', + showTitle: false, + well: true, + index: false, + hover: true, + hasChildren: true, + 'class': 'table-no-border', + trackBy: 'host.id', + title: false, + + fields: { + toggleHost: { + ngDisabled: "!host.summary_fields.user_capabilities.edit", + 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: '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' + } + }, + + fieldActions: { + + columnClass: 'col-lg-6 col-md-4 col-sm-4 col-xs-5 text-right', + copy: { + mode: 'all', + ngClick: "copyMoveHost(host.id)", + awToolTip: 'Copy or move host to another group', + dataPlacement: "top", + ngShow: 'host.summary_fields.user_capabilities.edit' + }, + 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' + }, + "delete": { + //label: 'Delete', + ngClick: "deleteHost(host.id, host.name)", + icon: 'icon-trash', + awToolTip: 'Delete host', + dataPlacement: 'top', + ngShow: 'host.summary_fields.user_capabilities.delete' + } + }, + + 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 results.", + actionClass: 'btn List-buttonDefault', + buttonContent: 'SMART INVENTORY', + ngShow: 'canAdd', + dataPlacement: "top", + } + } + }; +}]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js b/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js new file mode 100644 index 0000000000..3f5ac46872 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js @@ -0,0 +1,80 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +function HostsList($scope, HostsNewList, $rootScope, GetBasePath, + rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, HostManageService) { + + let list = HostsNewList; + + init(); + + function init(){ + $scope.canAdd = 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.createHost = function(){ + $state.go('hostsnew.add'); + }; + $scope.editHost = function(id){ + $state.go('hostsnew.edit', {host_id: id}); + }; + $scope.deleteHost = function(id, name){ + var body = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; + var action = function(){ + delete $rootScope.promptActionBtnClass; + Wait('start'); + HostManageService.delete(id).then(() => { + $('#prompt-modal').modal('hide'); + if (parseInt($state.params.host_id) === id) { + $state.go("hostsnew", null, {reload: true}); + } else { + $state.go($state.current.name, null, {reload: true}); + } + Wait('stop'); + }); + }; + // Prompt depends on having $rootScope.promptActionBtnClass available... + Prompt({ + hdr: 'Delete Host', + body: body, + action: action, + actionText: 'DELETE', + }); + $rootScope.promptActionBtnClass = 'Modal-errorButton'; + }; + + $scope.toggleHost = function(event, host) { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + + host.enabled = !host.enabled; + + HostManageService.put(host).then(function(){ + $state.go($state.current, null, {reload: true}); + }); + }; + +} + +export default ['$scope', 'HostsNewList', '$rootScope', 'GetBasePath', + 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', 'HostManageService', HostsList +]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/list/main.js b/awx/ui/client/src/inventoriesnew/hosts/list/main.js new file mode 100644 index 0000000000..4e603ef45d --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/list/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './host-list.controller'; + +export default +angular.module('hostsList', []) + .controller('NewHostListController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/main.js b/awx/ui/client/src/inventoriesnew/hosts/main.js new file mode 100644 index 0000000000..7363b267f9 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/hosts/main.js @@ -0,0 +1,20 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import hostAdd from './add/main'; + import hostEdit from './edit/main'; + import hostList from './list/main'; + import HostsNewList from './host.list'; + import HostsNewForm from './host.form'; + +export default +angular.module('hostnew', [ + hostAdd.name, + hostEdit.name, + hostList.name + ]) + .factory('HostsNewForm', HostsNewForm) + .factory('HostsNewList', HostsNewList); diff --git a/awx/ui/client/src/inventoriesnew/inventories.partial.html b/awx/ui/client/src/inventoriesnew/inventories.partial.html new file mode 100644 index 0000000000..524ead4a7f --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/inventories.partial.html @@ -0,0 +1,14 @@ +
+
+
+
+
+
+
INVENTORIES
+
HOSTS
+
+
+
+
+
+
diff --git a/awx/ui/client/src/inventoriesnew/inventory.form.js b/awx/ui/client/src/inventoriesnew/inventory.form.js new file mode 100644 index 0000000000..a138461f49 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/inventory.form.js @@ -0,0 +1,135 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name forms.function:Inventories + * @description This form is for adding/editing an inventory + */ + +export default ['i18n', function(i18n) { + return { + + addTitle: i18n._('NEW INVENTORY'), + editTitle: '{{ inventory_name }}', + name: 'inventory', + basePath: 'inventory', + // the top-most node of this generated state tree + stateTree: 'inventoriesnew', + tabs: true, + + fields: { + inventory_name: { + realName: 'name', + label: i18n._('Name'), + type: 'text', + required: true, + capitalize: false, + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + inventory_description: { + realName: 'description', + label: i18n._('Description'), + type: 'text', + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + organization: { + label: i18n._('Organization'), + type: 'lookup', + basePath: 'organizations', + list: 'OrganizationList', + sourceModel: 'organization', + sourceField: 'name', + awRequiredWhen: { + reqExpression: "organizationrequired", + init: "true" + }, + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', + awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + class: 'Form-formGroup--fullWidth', + rows: 6, + "default": "---", + awPopOver: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

' + 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: { + 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', + } + } + } + } + + };}]; diff --git a/awx/ui/client/src/inventoriesnew/inventory.list.js b/awx/ui/client/src/inventoriesnew/inventory.list.js new file mode 100644 index 0000000000..c06f3703ec --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/inventory.list.js @@ -0,0 +1,98 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +export default ['i18n', function(i18n) { + return { + + name: 'inventoriesnew', + iterator: 'inventory', + selectTitle: i18n._('Add Inventories'), + editTitle: i18n._('INVENTORIES'), + listTitle: i18n._('INVENTORIES'), + selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), " "), + index: false, + hover: true, + basePath: 'inventory', + title: false, + + fields: { + status: { + label: '', + columnClass: 'List-staticColumn--mediumStatus', + nosort: true, + ngClick: "null", + iconOnly: true, + excludeModal: true, + icons: [{ + icon: "{{ 'icon-cloud-' + inventory.syncStatus }}", + awToolTip: "{{ inventory.syncTip }}", + awTipPlacement: "right", + ngClick: "showGroupSummary($event, inventory.id)", + ngClass: "inventory.launch_class" + },{ + icon: "{{ 'icon-job-' + inventory.hostsStatus }}", + awToolTip: false, + ngClick: "showHostSummary($event, inventory.id)", + ngClass: "" + }] + }, + name: { + key: true, + label: i18n._('Name'), + columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent', + modalColumnClass: 'col-md-11', + linkTo: '/#/inventoriesnew/{{inventory.id}}' + }, + organization: { + label: i18n._('Organization'), + ngBind: 'inventory.summary_fields.organization.name', + linkTo: '/#/organizations/{{ inventory.organization }}', + sourceModel: 'organization', + sourceField: 'name', + excludeModal: true, + columnClass: 'col-md-5 col-sm-3 hidden-xs' + } + }, + + actions: { + add: { + mode: 'all', // One of: edit, select, all + ngClick: 'addInventory()', + awToolTip: i18n._('Create a new inventory'), + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ' + i18n._('ADD'), + ngShow: 'canAdd' + } + }, + + fieldActions: { + + columnClass: 'col-md-2 col-sm-4 col-xs-4', + + edit: { + label: i18n._('Edit'), + ngClick: 'editInventory(inventory.id)', + awToolTip: i18n._('Edit inventory'), + dataPlacement: 'top', + ngShow: 'inventory.summary_fields.user_capabilities.edit' + }, + view: { + label: i18n._('View'), + ngClick: 'editInventory(inventory.id)', + awToolTip: i18n._('View inventory'), + dataPlacement: 'top', + ngShow: '!inventory.summary_fields.user_capabilities.edit' + }, + "delete": { + label: i18n._('Delete'), + ngClick: "deleteInventory(inventory.id, inventory.name)", + awToolTip: i18n._('Delete inventory'), + dataPlacement: 'top', + ngShow: 'inventory.summary_fields.user_capabilities.delete' + } + } + };}]; diff --git a/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js b/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js new file mode 100644 index 0000000000..e37f9ab6e8 --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js @@ -0,0 +1,308 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +function InventoriesList($scope, $rootScope, $location, + $compile, $filter, Rest, InventoriesNewList, Prompt, + ProcessErrors, GetBasePath, Wait, Find, Empty, $state, rbacUiControlService, Dataset) { + + let list = InventoriesNewList, + defaultUrl = GetBasePath('inventory'); + + init(); + + function init(){ + $scope.canAdd = false; + + rbacUiControlService.canAdd('inventory') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + $scope.$watchCollection(list.name, function(){ + _.forEach($scope[list.name], buildStatusIndicators); + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $rootScope.flashMessage = null; + + } + + function buildStatusIndicators(inventory){ + inventory.launch_class = ""; + if (inventory.has_inventory_sources) { + if (inventory.inventory_sources_with_failures > 0) { + inventory.syncStatus = 'error'; + inventory.syncTip = inventory.inventory_sources_with_failures + ' groups with sync failures. Click for details'; + } + else { + inventory.syncStatus = 'successful'; + inventory.syncTip = 'No inventory sync failures. Click for details.'; + } + } + else { + inventory.syncStatus = 'na'; + inventory.syncTip = 'Not configured for inventory sync.'; + inventory.launch_class = "btn-disabled"; + } + if (inventory.has_active_failures) { + inventory.hostsStatus = 'error'; + inventory.hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.'; + } + else if (inventory.total_hosts) { + inventory.hostsStatus = 'successful'; + inventory.hostsTip = 'No hosts with failures. Click for details.'; + } + else { + inventory.hostsStatus = 'none'; + inventory.hostsTip = 'Inventory contains 0 hosts.'; + } + } + + function ellipsis(a) { + if (a.length > 20) { + return a.substr(0,20) + '...'; + } + return a; + } + + function attachElem(event, html, title) { + var elem = $(event.target).parent(); + try { + elem.tooltip('hide'); + elem.popover('destroy'); + } + catch(err) { + //ignore + } + $('.popover').each(function() { + // remove lingering popover
. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + elem.attr({ + "aw-pop-over": html, + "data-popover-title": title, + "data-placement": "right" }); + elem.removeAttr('ng-click'); + $compile(elem)($scope); + $scope.triggerPopover(event); + } + if ($scope.removeHostSummaryReady) { + $scope.removeHostSummaryReady(); + } + $scope.removeHostSummaryReady = $scope.$on('HostSummaryReady', function(e, event, data) { + + var html, title = "Recent Jobs"; + Wait('stop'); + if (data.count > 0) { + html = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + html += "\n"; + + data.results.forEach(function(row) { + html += "\n"; + html += "\n"; + html += ""; + html += ""; + html += "\n"; + }); + html += "\n"; + html += "
StatusFinishedName
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + $filter('sanitize')(ellipsis(row.name)) + "
\n"; + } + else { + html = "

No recent job data available for this inventory.

\n"; + } + attachElem(event, html, title); + }); + + if ($scope.removeGroupSummaryReady) { + $scope.removeGroupSummaryReady(); + } + $scope.removeGroupSummaryReady = $scope.$on('GroupSummaryReady', function(e, event, inventory, data) { + var html, title; + + Wait('stop'); + + // Build the html for our popover + html = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + data.results.forEach( function(row) { + if (row.related.last_update) { + html += ""; + html += ``; + html += ""; + html += ""; + html += "\n"; + } + else { + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + } + }); + html += "\n"; + html += "
StatusLast SyncGroup
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "
NA" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "
\n"; + title = "Sync Status"; + attachElem(event, html, title); + }); + + $scope.showGroupSummary = function(event, id) { + try{ + var elem = $(event.target).parent(); + // if the popover is visible already, then exit the function here + if(elem.data()['bs.popover'].tip().hasClass('in')){ + return; + } + } + catch(err){ + var inventory; + if (!Empty(id)) { + inventory = Find({ list: $scope.inventories, key: 'id', val: id }); + if (inventory.syncStatus !== 'na') { + Wait('start'); + Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5'); + Rest.get() + .success(function(data) { + $scope.$emit('GroupSummaryReady', event, inventory, data); + }) + .error(function(data, status) { + ProcessErrors( $scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status + }); + }); + } + } + } + }; + + $scope.showHostSummary = function(event, id) { + try{ + var elem = $(event.target).parent(); + // if the popover is visible already, then exit the function here + if(elem.data()['bs.popover'].tip().hasClass('in')){ + return; + } + } + catch(err){ + var url, inventory; + if (!Empty(id)) { + inventory = Find({ list: $scope.inventories, key: 'id', val: id }); + if (inventory.total_hosts > 0) { + Wait('start'); + url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed="; + url += (inventory.has_active_failures) ? 'true' : "false"; + url += "&order_by=-finished&page_size=5"; + Rest.setUrl(url); + Rest.get() + .success( function(data) { + $scope.$emit('HostSummaryReady', event, data); + }) + .error( function(data, status) { + ProcessErrors( $scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET returned: ' + status + }); + }); + } + } + } + }; + + $scope.viewJob = function(url) { + + // Pull the id out of the URL + var id = url.replace(/^\//, '').split('/')[3]; + + $state.go('inventorySyncStdout', {id: id}); + + }; + + $scope.addInventory = function () { + $state.go('inventoriesnew.add'); + }; + + $scope.editInventory = function (id) { + $state.go('inventoriesnew.edit', {inventory_id: id}); + }; + + $scope.manageInventory = function(id){ + $location.path($location.path() + '/' + id + '/manage'); + }; + + $scope.deleteInventory = function (id, name) { + + var action = function () { + var url = defaultUrl + id + '/'; + Wait('start'); + $('#prompt-modal').modal('hide'); + Rest.setUrl(url); + Rest.destroy() + .success(function () { + if (parseInt($state.params.inventory_id) === id) { + $state.go("^", null, {reload: true}); + } else { + $state.go('.', null, {reload: true}); + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors( $scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to delete the inventory below?
' + $filter('sanitize')(name) + '
', + action: action, + actionText: 'DELETE' + }); + }; + + // Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status + $scope.viewJobs = function (id) { + $location.url('/jobs/?inventory__int=' + id); + }; + + $scope.viewFailedJobs = function (id) { + $location.url('/jobs/?inventory__int=' + id + '&status=failed'); + }; +} + +export default ['$scope', '$rootScope', '$location', + '$compile', '$filter', 'Rest', 'InventoriesNewList', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', 'Dataset', InventoriesList +]; diff --git a/awx/ui/client/src/inventoriesnew/list/main.js b/awx/ui/client/src/inventoriesnew/list/main.js new file mode 100644 index 0000000000..09550f45ca --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/list/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './inventory-list.controller'; + +export default +angular.module('newInventoryList', []) + .controller('NewInventoryListController', controller); diff --git a/awx/ui/client/src/inventoriesnew/main.js b/awx/ui/client/src/inventoriesnew/main.js new file mode 100644 index 0000000000..4e736caf9a --- /dev/null +++ b/awx/ui/client/src/inventoriesnew/main.js @@ -0,0 +1,236 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import hostnew from './hosts/main'; +import inventoryAdd from './add/main'; +import inventoryEdit from './edit/main'; +import inventoryList from './list/main'; +import { templateUrl } from '../shared/template-url/template-url.factory'; +import { N_ } from '../i18n'; +// import inventoriesnewRoute from './inventories.route'; +import InventoriesNewList from './inventory.list'; +import InventoriesNewForm from './inventory.form'; +export default +angular.module('inventorynew', [ + hostnew.name, + inventoryAdd.name, + inventoryEdit.name, + inventoryList.name + ]) + .factory('InventoriesNewForm', InventoriesNewForm) + .factory('InventoriesNewList', InventoriesNewList) + .config(['$stateProvider', '$stateExtenderProvider', 'stateDefinitionsProvider', + function($stateProvider, $stateExtenderProvider, stateDefinitionsProvider) { + // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states + // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree + let stateDefinitions = stateDefinitionsProvider.$get(); + + $stateProvider.state({ + name: 'inventoriesnew', + url: '/inventoriesnew', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'inventoriesnew', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'InventoriesNewList', + form: 'InventoriesNewForm', + controllers: { + list: 'NewInventoryListController', + add: 'NewInventoryAddController', + edit: 'NewInventoryEditController' + }, + ncyBreadcrumb: { + label: N_('INVENTORIESNEW') + }, + views: { + '@': { + templateUrl: templateUrl('inventoriesnew/inventories') + }, + 'list@inventoriesnew': { + templateProvider: function(InventoriesNewList, generateList) { + let html = generateList.build({ + list: InventoriesNewList, + mode: 'edit' + }); + return html; + }, + controller: 'NewInventoryListController' + } + } + }) + }); + + $stateProvider.state({ + name: 'hostsnew', + url: '/hostsnew', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'hostsnew', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'HostsNewList', + form: 'HostsNewForm', + controllers: { + list: 'NewHostListController', + add: 'NewHostAddController', + edit: 'NewHostEditController' + }, + ncyBreadcrumb: { + label: N_('HOSTSNEW') + }, + views: { + '@': { + templateUrl: templateUrl('inventoriesnew/inventories') + }, + 'list@hostsnew': { + templateProvider: function(HostsNewList, generateList) { + let html = generateList.build({ + list: HostsNewList, + mode: 'edit' + }); + return html; + }, + controller: 'NewHostListController' + } + } + }) + }); + + + + // function generateInvAddStateTree() { + // + // let addInventory = stateDefinitions.generateTree({ + // url: '/add', + // name: 'inventoriesnew.add', + // modes: ['add'], + // form: 'InventoriesNewForm', + // controllers: { + // add: 'NewInventoryAddController' + // } + // }); + // + // return Promise.all([ + // addInventory, + // ]).then((generated) => { + // return { + // states: _.reduce(generated, (result, definition) => { + // return result.concat(definition.states); + // }, []) + // }; + // }); + // } + // + // function generateInvEditStateTree() { + // + // let editInventory = stateDefinitions.generateTree({ + // url: '/:inventory_id', + // name: 'inventoriesnew.edit', + // modes: ['edit'], + // form: 'InventoriesNewForm', + // controllers: { + // edit: 'NewInventoryEditController' + // } + // }); + // + // return Promise.all([ + // editInventory, + // ]).then((generated) => { + // return { + // states: _.reduce(generated, (result, definition) => { + // return result.concat(definition.states); + // }, []) + // }; + // }); + // } + // + // let inventoriesnew = { + // name: 'inventoriesnew', + // route: '/inventoriesnew', + // ncyBreadcrumb: { + // label: N_("INVENTORIESNEW") + // }, + // params: { + // inventory_search: { + // value: {order_by: 'name', page_size: '20', role_level: 'admin_role'}, + // dynamic: true + // } + // }, + // resolve: { + // Dataset: ['InventoriesNewList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { + // let path = GetBasePath(list.basePath) || GetBasePath(list.name); + // return qs.search(path, $stateParams[`${list.iterator}_search`]); + // }], + // ListDefinition: ['InventoriesNewList', (list) => { + // return list; + // }] + // }, + // views: { + // '@': { + // templateUrl: templateUrl('inventoriesnew/inventories') + // }, + // 'list@inventoriesnew': { + // templateProvider: function(InventoriesNewList, generateList) { + // let html = generateList.build({ + // list: InventoriesNewList, + // mode: 'edit' + // }); + // return html; + // }, + // controller: 'NewInventoryListController' + // } + // } + // }; + // stateExtender.addState(inventoriesnew); + // + // let hostsnew = { + // name: 'inventoriesnew.hosts', + // route: '/hosts', + // ncyBreadcrumb: { + // label: N_("HOSTS") + // }, + // params: { + // host_search: { + // value: {order_by: 'name', page_size: '20'}, + // dynamic: true + // } + // }, + // resolve: { + // Dataset: ['HostsNewList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { + // let path = GetBasePath(list.basePath) || GetBasePath(list.name); + // return qs.search(path, $stateParams[`${list.iterator}_search`]); + // }], + // ListDefinition: ['HostsNewList', (list) => { + // return list; + // }] + // }, + // views: { + // 'list@inventoriesnew': { + // templateProvider: function(HostsNewList, generateList) { + // let html = generateList.build({ + // list: HostsNewList, + // mode: 'edit' + // }); + // return html; + // }, + // controller: 'NewHostListController' + // } + // } + // }; + // stateExtender.addState(hostsnew); + // + // let addInventoryTree = { + // name: 'inventoriesnew.add', + // url: '/add', + // lazyLoad: () => generateInvAddStateTree() + // }; + // $stateProvider.state(addInventoryTree); + // + // let editInventoryTree = { + // name: 'inventoriesnew.edit', + // url: '/:inventory_id', + // lazyLoad: () => generateInvEditStateTree() + // }; + // $stateProvider.state(editInventoryTree); + } + ]); diff --git a/awx/ui/client/src/main-menu/main-menu.partial.html b/awx/ui/client/src/main-menu/main-menu.partial.html index 398488add7..4d28ea22e8 100644 --- a/awx/ui/client/src/main-menu/main-menu.partial.html +++ b/awx/ui/client/src/main-menu/main-menu.partial.html @@ -27,6 +27,14 @@ INVENTORIES + + + INVENTORIES NEW + + INVENTORIES + + + INVENTORIES NEW + + list }, - views: { - '@': { - // resolves to a variable property name: - // 'templateUrl' OR 'templateProvider' - [params.templates && params.templates.list ? 'templateUrl' : 'templateProvider']: generateTemplateBlock(), - controller: params.controllers.list, - } - } + views: views }); // allow passed-in params to override default resolve block if (params.resolve && params.resolve.list) { @@ -125,7 +128,7 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto } if (list.search) { state.params[`${list.iterator}_search`].value = _.merge(state.params[`${list.iterator}_search`].value, list.search); - } + }if(state.name === 'inventoriesnew'){console.log(state);} return state; }, /** From 3bcb4b0496738e1ac0a9994fc50d19588fe40cc9 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 5 Apr 2017 16:46:12 -0700 Subject: [PATCH 02/49] adjustments to host list adding Inventory column and disabling host toggle for external sources --- .../src/inventoriesnew/hosts/host.list.js | 13 +++++++--- .../hosts/list/host-list.controller.js | 26 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/inventoriesnew/hosts/host.list.js b/awx/ui/client/src/inventoriesnew/hosts/host.list.js index 632a38129b..283ce28779 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/host.list.js +++ b/awx/ui/client/src/inventoriesnew/hosts/host.list.js @@ -21,7 +21,7 @@ export default ['i18n', function(i18n) { fields: { toggleHost: { - ngDisabled: "!host.summary_fields.user_capabilities.edit", + ngDisabled: 'host.has_inventory_sources', label: '', columnClass: 'List-staticColumn--toggle', type: "toggle", @@ -54,13 +54,20 @@ export default ['i18n', function(i18n) { }, name: { key: true, - label: 'Name', + 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: { diff --git a/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js b/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js index 3f5ac46872..92c0f2befe 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js @@ -5,7 +5,8 @@ *************************************************/ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, - rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, HostManageService) { + rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, + HostManageService, SetStatus) { let list = HostsNewList; @@ -26,6 +27,24 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, $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(); + }); + + } + + function setJobStatus(){ + _.forEach($scope.hosts, function(value) { + SetStatus({ + scope: $scope, + host: value + }); + }); } $scope.createHost = function(){ @@ -58,7 +77,7 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, }); $rootScope.promptActionBtnClass = 'Modal-errorButton'; }; - + $scope.toggleHost = function(event, host) { try { $(event.target).tooltip('hide'); @@ -76,5 +95,6 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, } export default ['$scope', 'HostsNewList', '$rootScope', 'GetBasePath', - 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', 'HostManageService', HostsList + 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', + 'HostManageService', 'SetStatus', HostsList ]; From b741354ad01a9dc34ddc8c934241c7d2f2b9a3ff Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 6 Apr 2017 15:20:55 -0400 Subject: [PATCH 03/49] Removed old inventories code. Cleaned up new inventories code --- awx/ui/client/src/app.js | 2 - .../add/inventory-add.controller.js | 2 +- awx/ui/client/src/inventories/add/main.js | 2 +- .../edit/inventory-edit.controller.js | 10 +- awx/ui/client/src/inventories/edit/main.js | 2 +- .../edit/build-groups-edit-state.factory.js | 93 +++++ .../nested-groups-list-state.factory.js | 85 ++++ .../nested-hosts-list.controller.js | 162 ++++++++ .../nested-hosts/nested-hosts.form.js} | 28 +- .../hosts/add/host-add.controller.js | 0 .../hosts/add/main.js | 2 +- .../hosts/edit/host-edit.controller.js | 0 .../hosts/edit/main.js | 2 +- .../hosts/host.form.js | 0 .../hosts/host.list.js | 1 + .../{manage => }/hosts/hosts.service.js | 0 .../hosts/list/host-list.controller.js | 13 +- .../hosts/list/main.js | 2 +- awx/ui/client/src/inventories/hosts/main.js | 26 ++ .../set-enabled-msg.factory.js | 0 .../factories => hosts}/set-status.factory.js | 0 .../inventories.partial.html | 6 +- .../client/src/inventories/inventory.list.js | 3 +- .../list/inventory-list.controller.js | 8 +- awx/ui/client/src/inventories/list/main.js | 2 +- awx/ui/client/src/inventories/main.js | 365 ++++-------------- .../manage/adhoc/adhoc.controller.js | 308 --------------- .../inventories/manage/adhoc/adhoc.form.js | 159 -------- .../manage/adhoc/adhoc.partial.html | 4 - .../inventories/manage/adhoc/adhoc.route.js | 28 -- .../src/inventories/manage/adhoc/main.js | 7 - .../manage/breadcrumbs/breadcrumbs.block.less | 25 -- .../breadcrumbs/breadcrumbs.controller.js | 38 -- .../breadcrumbs/breadcrumbs.partial.html | 22 -- .../manage/copy-move/copy-move-groups.list.js | 24 -- .../manage/copy-move/copy-move.block.less | 33 -- .../manage/copy-move/copy-move.partial.html | 21 - .../src/inventories/manage/copy-move/main.js | 15 - .../factories/get-hosts-status-msg.factory.js | 33 -- .../get-source-type-options.factory.js | 37 -- .../factories/get-sync-status-msg.factory.js | 77 ---- .../factories/groups-cancel-update.factory.js | 81 ---- .../factories/view-update-status.factory.js | 46 --- .../manage/groups/groups-list.partial.html | 79 ---- .../manage/groups/groups.block.less | 16 - .../inventories/manage/groups/groups.form.js | 360 ----------------- .../manage/groups/groups.service.js | 113 ------ .../manage/groups/inventory-groups.list.js | 167 -------- .../src/inventories/manage/groups/main.js | 27 -- .../manage/hosts/hosts-edit.controller.js | 84 ---- .../manage/hosts/hosts-list.controller.js | 120 ------ .../manage/hosts/inventory-hosts.list.js | 119 ------ .../src/inventories/manage/hosts/main.js | 21 - .../manage/inventory-manage.block.less | 3 - .../manage/inventory-manage.controller.js | 28 -- .../manage/inventory-manage.partial.html | 8 - awx/ui/client/src/inventories/manage/main.js | 24 -- .../edit/build-host-edit-state.factory.js | 73 ++++ .../add/inventory-add.controller.js | 104 ----- awx/ui/client/src/inventoriesnew/add/main.js | 11 - .../edit/inventory-edit.controller.js | 136 ------- awx/ui/client/src/inventoriesnew/edit/main.js | 11 - .../client/src/inventoriesnew/hosts/main.js | 20 - .../src/inventoriesnew/inventory.form.js | 135 ------- .../src/inventoriesnew/inventory.list.js | 98 ----- .../list/inventory-list.controller.js | 308 --------------- awx/ui/client/src/inventoriesnew/list/main.js | 11 - awx/ui/client/src/inventoriesnew/main.js | 236 ----------- .../src/main-menu/main-menu.partial.html | 17 - .../src/shared/stateDefinitions.factory.js | 2 +- awx/ui/templates/ui/index.html | 3 +- 71 files changed, 572 insertions(+), 3536 deletions(-) create mode 100644 awx/ui/client/src/inventories/groups/edit/build-groups-edit-state.factory.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list-state.factory.js create mode 100644 awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list.controller.js rename awx/ui/client/src/inventories/{manage/hosts/hosts.form.js => groups/nested-hosts/nested-hosts.form.js} (79%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/add/host-add.controller.js (100%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/add/main.js (83%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/edit/host-edit.controller.js (100%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/edit/main.js (82%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/host.form.js (100%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/host.list.js (99%) rename awx/ui/client/src/inventories/{manage => }/hosts/hosts.service.js (100%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/list/host-list.controller.js (89%) rename awx/ui/client/src/{inventoriesnew => inventories}/hosts/list/main.js (82%) create mode 100644 awx/ui/client/src/inventories/hosts/main.js rename awx/ui/client/src/inventories/{manage/hosts/factories => hosts}/set-enabled-msg.factory.js (100%) rename awx/ui/client/src/inventories/{manage/hosts/factories => hosts}/set-status.factory.js (100%) rename awx/ui/client/src/{inventoriesnew => inventories}/inventories.partial.html (60%) delete mode 100644 awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js delete mode 100644 awx/ui/client/src/inventories/manage/adhoc/adhoc.form.js delete mode 100644 awx/ui/client/src/inventories/manage/adhoc/adhoc.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js delete mode 100644 awx/ui/client/src/inventories/manage/adhoc/main.js delete mode 100644 awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.block.less delete mode 100644 awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.controller.js delete mode 100644 awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js delete mode 100644 awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less delete mode 100644 awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/copy-move/main.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups-list.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups.block.less delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups.form.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/groups.service.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js delete mode 100644 awx/ui/client/src/inventories/manage/groups/main.js delete mode 100644 awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js delete mode 100644 awx/ui/client/src/inventories/manage/hosts/hosts-list.controller.js delete mode 100644 awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js delete mode 100644 awx/ui/client/src/inventories/manage/hosts/main.js delete mode 100644 awx/ui/client/src/inventories/manage/inventory-manage.block.less delete mode 100644 awx/ui/client/src/inventories/manage/inventory-manage.controller.js delete mode 100644 awx/ui/client/src/inventories/manage/inventory-manage.partial.html delete mode 100644 awx/ui/client/src/inventories/manage/main.js create mode 100644 awx/ui/client/src/inventories/related-hosts/edit/build-host-edit-state.factory.js delete mode 100644 awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js delete mode 100644 awx/ui/client/src/inventoriesnew/add/main.js delete mode 100644 awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js delete mode 100644 awx/ui/client/src/inventoriesnew/edit/main.js delete mode 100644 awx/ui/client/src/inventoriesnew/hosts/main.js delete mode 100644 awx/ui/client/src/inventoriesnew/inventory.form.js delete mode 100644 awx/ui/client/src/inventoriesnew/inventory.list.js delete mode 100644 awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js delete mode 100644 awx/ui/client/src/inventoriesnew/list/main.js delete mode 100644 awx/ui/client/src/inventoriesnew/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index f5eaf2080b..0649023b54 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -40,7 +40,6 @@ if ($basePath) { import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventories from './inventories/main'; -import inventorynew from './inventoriesnew/main'; import inventoryScripts from './inventory-scripts/main'; import credentialTypes from './credential-types/main'; import organizations from './organizations/main'; @@ -100,7 +99,6 @@ var tower = angular.module('Tower', [ configuration.name, systemTracking.name, inventories.name, - inventorynew.name, inventoryScripts.name, credentialTypes.name, organizations.name, 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..02e608af6c 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -77,7 +77,7 @@ function InventoriesAdd($scope, $location, .success(function(data) { var inventory_id = data.id; Wait('stop'); - $location.path('/inventories/' + inventory_id + '/manage'); + $location.path('/inventories/' + inventory_id); }) .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/edit/inventory-edit.controller.js b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js index 78d35d311f..c08f101ce0 100644 --- a/awx/ui/client/src/inventories/edit/inventory-edit.controller.js +++ b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js @@ -11,13 +11,13 @@ */ function InventoriesEdit($scope, $location, - $stateParams, InventoryForm, Rest, ProcessErrors, + $stateParams, InventoriesForm, Rest, ProcessErrors, ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, ParseVariableString, $state, OrgAdminLookup) { // Inject dynamic view var defaultUrl = GetBasePath('inventory'), - form = InventoryForm, + form = InventoriesForm, inventory_id = $stateParams.inventory_id, master = {}, fld, json_data, data; @@ -122,10 +122,6 @@ function InventoriesEdit($scope, $location, }); }; - $scope.manageInventory = function() { - $location.path($location.path() + '/manage'); - }; - $scope.formCancel = function() { $state.go('inventories'); }; @@ -133,7 +129,7 @@ function InventoriesEdit($scope, $location, } export default ['$scope', '$location', - '$stateParams', 'InventoryForm', 'Rest', + '$stateParams', 'InventoriesForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', '$state', 'OrgAdminLookup', InventoriesEdit, 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/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/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..7efd37c391 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list-state.factory.js @@ -0,0 +1,85 @@ +/************************************************* +* 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`]); + } + ], + inventoryData: ['InventoryManageService', '$stateParams', 'host', function(InventoryManageService, $stateParams, host) { + var id = ($stateParams.inventory_id) ? $stateParams.inventory_id : host.data.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-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 = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; + var action = function(){ + delete $rootScope.promptActionBtnClass; + Wait('start'); + HostManageService.delete(id).then(() => { + $('#prompt-modal').modal('hide'); + if (parseInt($state.params.host_id) === id) { + $state.go("hosts", null, {reload: true}); + } else { + $state.go($state.current.name, null, {reload: true}); + } + Wait('stop'); + }); + }; + // Prompt depends on having $rootScope.promptActionBtnClass available... + Prompt({ + hdr: 'Delete Host', + body: body, + action: action, + actionText: 'DELETE', + }); + $rootScope.promptActionBtnClass = 'Modal-errorButton'; + }; + + $scope.toggleHost = function(event, host) { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + + host.enabled = !host.enabled; + + HostManageService.put(host).then(function(){ + $state.go($state.current, null, {reload: true}); + }); + }; + + $scope.smartInventory = function() { + $state.go('inventories.addSmartInventory'); + }; + + $scope.systemTracking = function(){ + var hostIds = _.map($scope.hostsSelected, (host) => host.id); + $state.go('systemTracking', { + inventoryId: $state.params.inventory_id, + hosts: $scope.hostsSelected, + hostIds: hostIds + }); + }; + + $scope.setAdhocPattern = function(){ + var pattern = _($scope.hostsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; + + $scope.copyMoveHost = function(id) { + $state.go('inventories.edit.hosts.copyMoveHost', {host_id: id}); + }; +}]; diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts.form.js b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.form.js similarity index 79% rename from awx/ui/client/src/inventories/manage/hosts/hosts.form.js rename to awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.form.js index efb64a4785..d2ffe33e05 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts.form.js +++ b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.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,9 @@ export default ['i18n', function(i18n) { formLabelSize: 'col-lg-3', formFieldSize: 'col-lg-9', iterator: 'host', + // activeEditState: 'inventories.edit.hosts.edit', + activeEditState: 'inventories.edit.groups.edit.nested_hosts.edit', + stateTree: 'inventories.edit.groups.edit.nested_hosts', headerFields:{ enabled: { class: 'Form-header-field', @@ -99,5 +103,27 @@ 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('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/inventoriesnew/hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js similarity index 100% rename from awx/ui/client/src/inventoriesnew/hosts/add/host-add.controller.js rename to awx/ui/client/src/inventories/hosts/add/host-add.controller.js diff --git a/awx/ui/client/src/inventoriesnew/hosts/add/main.js b/awx/ui/client/src/inventories/hosts/add/main.js similarity index 83% rename from awx/ui/client/src/inventoriesnew/hosts/add/main.js rename to awx/ui/client/src/inventories/hosts/add/main.js index 8205e986c6..9f1f083b96 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/add/main.js +++ b/awx/ui/client/src/inventories/hosts/add/main.js @@ -8,4 +8,4 @@ import controller from './host-add.controller'; export default angular.module('hostsAdd', []) - .controller('NewHostAddController', controller); + .controller('HostAddController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js similarity index 100% rename from awx/ui/client/src/inventoriesnew/hosts/edit/host-edit.controller.js rename to awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js diff --git a/awx/ui/client/src/inventoriesnew/hosts/edit/main.js b/awx/ui/client/src/inventories/hosts/edit/main.js similarity index 82% rename from awx/ui/client/src/inventoriesnew/hosts/edit/main.js rename to awx/ui/client/src/inventories/hosts/edit/main.js index 420e870e99..2f0c5aee39 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/edit/main.js +++ b/awx/ui/client/src/inventories/hosts/edit/main.js @@ -8,4 +8,4 @@ import controller from './host-edit.controller'; export default angular.module('hostsEdit', []) - .controller('NewHostEditController', controller); + .controller('HostEditController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/host.form.js b/awx/ui/client/src/inventories/hosts/host.form.js similarity index 100% rename from awx/ui/client/src/inventoriesnew/hosts/host.form.js rename to awx/ui/client/src/inventories/hosts/host.form.js diff --git a/awx/ui/client/src/inventoriesnew/hosts/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js similarity index 99% rename from awx/ui/client/src/inventoriesnew/hosts/host.list.js rename to awx/ui/client/src/inventories/hosts/host.list.js index 283ce28779..ec70bb2716 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/host.list.js +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -17,6 +17,7 @@ export default ['i18n', function(i18n) { hasChildren: true, 'class': 'table-no-border', trackBy: 'host.id', + basePath: 'hosts', title: false, fields: { 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/inventoriesnew/hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js similarity index 89% rename from awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js rename to awx/ui/client/src/inventories/hosts/list/host-list.controller.js index 92c0f2befe..86d186ed74 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -4,11 +4,12 @@ * All Rights Reserved *************************************************/ -function HostsList($scope, HostsNewList, $rootScope, GetBasePath, + +function HostsList($scope, HostsList, $rootScope, GetBasePath, rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, HostManageService, SetStatus) { - let list = HostsNewList; + let list = HostsList; init(); @@ -48,10 +49,10 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, } $scope.createHost = function(){ - $state.go('hostsnew.add'); + $state.go('hosts.add'); }; $scope.editHost = function(id){ - $state.go('hostsnew.edit', {host_id: id}); + $state.go('hosts.edit', {host_id: id}); }; $scope.deleteHost = function(id, name){ var body = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; @@ -61,7 +62,7 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, HostManageService.delete(id).then(() => { $('#prompt-modal').modal('hide'); if (parseInt($state.params.host_id) === id) { - $state.go("hostsnew", null, {reload: true}); + $state.go("hosts", null, {reload: true}); } else { $state.go($state.current.name, null, {reload: true}); } @@ -94,7 +95,7 @@ function HostsList($scope, HostsNewList, $rootScope, GetBasePath, } -export default ['$scope', 'HostsNewList', '$rootScope', 'GetBasePath', +export default ['$scope', 'HostsList', '$rootScope', 'GetBasePath', 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', 'HostManageService', 'SetStatus', HostsList ]; diff --git a/awx/ui/client/src/inventoriesnew/hosts/list/main.js b/awx/ui/client/src/inventories/hosts/list/main.js similarity index 82% rename from awx/ui/client/src/inventoriesnew/hosts/list/main.js rename to awx/ui/client/src/inventories/hosts/list/main.js index 4e603ef45d..0682898494 100644 --- a/awx/ui/client/src/inventoriesnew/hosts/list/main.js +++ b/awx/ui/client/src/inventories/hosts/list/main.js @@ -8,4 +8,4 @@ import controller from './host-list.controller'; export default angular.module('hostsList', []) - .controller('NewHostListController', controller); + .controller('HostListController', controller); diff --git a/awx/ui/client/src/inventories/hosts/main.js b/awx/ui/client/src/inventories/hosts/main.js new file mode 100644 index 0000000000..d6fca96d15 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/main.js @@ -0,0 +1,26 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import hostAdd from './add/main'; + import hostEdit from './edit/main'; + import hostList from './list/main'; + import HostsList from './host.list'; + import HostsForm from './host.form'; + import HostManageService from './hosts.service'; + import SetStatus from './set-status.factory'; + import SetEnabledMsg from './set-enabled-msg.factory'; + +export default +angular.module('host', [ + hostAdd.name, + hostEdit.name, + hostList.name + ]) + .factory('HostsForm', HostsForm) + .factory('HostsList', HostsList) + .factory('SetStatus', SetStatus) + .factory('SetEnabledMsg', SetEnabledMsg) + .service('HostManageService', HostManageService); diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js b/awx/ui/client/src/inventories/hosts/set-enabled-msg.factory.js similarity index 100% rename from awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js rename to awx/ui/client/src/inventories/hosts/set-enabled-msg.factory.js diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js b/awx/ui/client/src/inventories/hosts/set-status.factory.js similarity index 100% rename from awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js rename to awx/ui/client/src/inventories/hosts/set-status.factory.js diff --git a/awx/ui/client/src/inventoriesnew/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html similarity index 60% rename from awx/ui/client/src/inventoriesnew/inventories.partial.html rename to awx/ui/client/src/inventories/inventories.partial.html index 524ead4a7f..871367dcb9 100644 --- a/awx/ui/client/src/inventoriesnew/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,11 +1,11 @@ -
+
-
INVENTORIES
-
HOSTS
+
INVENTORIES
+
HOSTS
diff --git a/awx/ui/client/src/inventories/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index e5f0930a8c..38c6f91874 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -17,6 +17,7 @@ export default ['i18n', function(i18n) { index: false, hover: true, basePath: 'inventory', + title: false, fields: { status: { @@ -44,9 +45,9 @@ export default ['i18n', function(i18n) { label: i18n._('Name'), columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent', modalColumnClass: 'col-md-11', - linkTo: '/#/inventories/{{inventory.id}}/manage', awToolTip: "{{ inventory.description }}", awTipPlacement: "top" + linkTo: '/#/inventories/{{inventory.id}}' }, organization: { label: i18n._('Organization'), diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index d900048060..6a2f0e261b 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -11,10 +11,10 @@ */ function InventoriesList($scope, $rootScope, $location, - $compile, $filter, Rest, InventoryList, Prompt, + $compile, $filter, Rest, InventoriesList, Prompt, ProcessErrors, GetBasePath, Wait, Find, Empty, $state, rbacUiControlService, Dataset) { - let list = InventoryList, + let list = InventoriesList, defaultUrl = GetBasePath('inventory'); init(); @@ -250,7 +250,7 @@ function InventoriesList($scope, $rootScope, $location, }; $scope.addInventory = function () { - $state.go('inventoriesnew.add'); + $state.go('inventories.add'); }; $scope.editInventory = function (id) { @@ -303,6 +303,6 @@ function InventoriesList($scope, $rootScope, $location, } export default ['$scope', '$rootScope', '$location', - '$compile', '$filter', 'Rest', 'InventoryList', + '$compile', '$filter', 'Rest', 'InventoriesList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', 'Dataset', InventoriesList ]; diff --git a/awx/ui/client/src/inventories/list/main.js b/awx/ui/client/src/inventories/list/main.js index e4514b6caf..11c6e4e694 100644 --- a/awx/ui/client/src/inventories/list/main.js +++ b/awx/ui/client/src/inventories/list/main.js @@ -7,5 +7,5 @@ import controller from './inventory-list.controller'; export default -angular.module('inventoryList', []) +angular.module('InventoryList', []) .controller('InventoryListController', controller); diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 45207bcf7e..1cc57ff9a1 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -4,312 +4,101 @@ * All Rights Reserved *************************************************/ +import host from './hosts/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; -import inventoryManage from './manage/main'; -import inventoryManageListRoute from './manage/inventory-manage.route'; -import { copyMoveGroupRoute, copyMoveHostRoute } from './manage/copy-move/copy-move.route'; -import adHocRoute from './manage/adhoc/adhoc.route'; import { templateUrl } from '../shared/template-url/template-url.factory'; import { N_ } from '../i18n'; - -// actual inventory list config object -import InventoryList from './inventory.list'; -import InventoryForm from './inventory.form'; - +import InventoriesList from './inventory.list'; +import InventoriesForm from './inventory.form'; export default angular.module('inventory', [ + host.name, inventoryAdd.name, inventoryEdit.name, - inventoryList.name, - inventoryManage.name, + inventoryList.name ]) - .factory('InventoryList', InventoryList) - .factory('InventoryForm', InventoryForm) - .config(['$stateProvider', '$stateExtenderProvider', 'stateDefinitionsProvider', - function($stateProvider, $stateExtenderProvider, stateDefinitionsProvider) { + .factory('InventoriesForm', InventoriesForm) + .factory('InventoriesList', InventoriesList) + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree - let stateTree, inventories, - addGroup, editGroup, addHost, editHost, - listSchedules, addSchedule, editSchedule, adhocCredentialLookup, - stateDefinitions = stateDefinitionsProvider.$get(), - stateExtender = $stateExtenderProvider.$get(); + let stateDefinitions = stateDefinitionsProvider.$get(); - function generateStateTree() { - - // inventories state node - inventories = stateDefinitions.generateTree({ - parent: 'inventories', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'InventoryList', - form: 'InventoryForm', - controllers: { - list: 'InventoryListController', - add: 'InventoryAddController', - edit: 'InventoryEditController' - }, - data: { - activityStream: true, - activityStreamTarget: 'inventory' - }, - ncyBreadcrumb: { - label: N_('INVENTORIES') - } - }); - - // scheduler state nodes - listSchedules = { - name: 'inventoryManage.editGroup.schedules', - url: '/schedules', - searchPrefix: 'schedule', - ncyBreadcrumb: { - parent: 'inventoryManage({group_id: parentObject.id})', - label: N_('SCHEDULES') - }, - resolve: { - Dataset: ['ScheduleList', 'QuerySet', '$stateParams', 'GetBasePath', 'groupData', - function(list, qs, $stateParams, GetBasePath, groupData) { - let path = `${groupData.related.inventory_source}schedules`; - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ], - ParentObject: ['groupData', function(groupData) { - return groupData; - }], - UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', - function(Rest, GetBasePath, $stateParams, $q) { - Rest.setUrl(GetBasePath('unified_jobs')); - var val = $q.defer(); - Rest.options() - .then(function(data) { - val.resolve(data.data); - }, function(data) { - val.reject(data); - }); - return val.promise; - }], - ScheduleList: ['SchedulesList', 'groupData', - (SchedulesList, groupData) => { - let list = _.cloneDeep(SchedulesList); - list.basePath = `${groupData.related.inventory_source}schedules`; - return list; - } - ] - }, - views: { - // clear form template when views render in this substate - 'form': { - templateProvider: () => '' + $stateProvider.state({ + name: 'inventories', + url: '/inventories', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'inventories', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'InventoriesList', + form: 'InventoriesForm', + controllers: { + list: 'InventoryListController', + add: 'InventoryAddController', + edit: 'InventoryEditController' }, - // target the un-named ui-view @ root level - '@': { - templateProvider: function(ScheduleList, generateList, ParentObject) { - // include name of parent resource in listTitle - ScheduleList.listTitle = `${ParentObject.name}
` + N_('SCHEDULES'); - let html = generateList.build({ - list: ScheduleList, - mode: 'edit' - }); - html = generateList.wrapPanel(html); - return "
" + generateList.insertFormView() + html + "
"; + urls: { + list: '/inventories' + }, + ncyBreadcrumb: { + label: N_('INVENTORIES') + }, + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') }, - controller: 'schedulerListController' - } - } - }; - - addSchedule = { - name: 'inventoryManage.editGroup.schedules.add', - url: '/add', - ncyBreadcrumb: { - label: N_("CREATE SCHEDULE") - }, - views: { - 'form': { - controller: 'schedulerAddController', - templateUrl: templateUrl("scheduler/schedulerForm") - } - } - }; - - editSchedule = { - name: 'inventoryManage.editGroup.schedules.edit', - url: '/:schedule_id', - ncyBreadcrumb: { - label: "{{schedule_obj.name}}" - }, - views: { - 'form': { - templateUrl: templateUrl("scheduler/schedulerForm"), - controller: 'schedulerEditController', - } - } - }; - - // group state nodes - addGroup = stateDefinitions.generateTree({ - url: '/add-group', - name: 'inventoryManage.addGroup', - modes: ['add'], - form: 'GroupForm', - controllers: { - add: 'GroupAddController' - } - }); - - editGroup = stateDefinitions.generateTree({ - url: '/edit-group/:group_id', - name: 'inventoryManage.editGroup', - modes: ['edit'], - form: 'GroupForm', - controllers: { - edit: 'GroupEditController' - }, - resolve: { - edit: { - groupData: ['$stateParams', 'GroupManageService', function($stateParams, GroupManageService) { - return GroupManageService.get({ id: $stateParams.group_id }).then(res => res.data.results[0]); - }], - inventorySourceData: ['$stateParams', 'GroupManageService', function($stateParams, GroupManageService) { - return GroupManageService.getInventorySource({ group: $stateParams.group_id }).then(res => res.data.results[0]); - }] - } - }, - // concat boilerplate schedule state definitions with generated editGroup state definitions - }).then((generated) => { - let schedulerDefinitions = _.map([ - stateExtender.buildDefinition(listSchedules), - stateExtender.buildDefinition(addSchedule), - stateExtender.buildDefinition(editSchedule) - ], - (state) => stateExtender.buildDefinition(state)); - return { - states: _(generated.states) - .concat(schedulerDefinitions) - .value() - }; - }); - - // host state nodes - addHost = stateDefinitions.generateTree({ - url: '/add-host', - name: 'inventoryManage.addHost', - modes: ['add'], - form: 'HostForm', - controllers: { - add: 'HostsAddController' - } - }); - - editHost = stateDefinitions.generateTree({ - url: '/edit-host/:host_id', - name: 'inventoryManage.editHost', - modes: ['edit'], - form: 'HostForm', - controllers: { - edit: 'HostEditController' - }, - resolve: { - edit: { - host: ['$stateParams', 'HostManageService', function($stateParams, HostManageService) { - return HostManageService.get({ id: $stateParams.host_id }).then(function(res) { - return res.data.results[0]; - }); - }] - } - }, - ncyBreadcrumb: { - label: "{{host.name}}", - }, - }); - - adhocCredentialLookup = { - searchPrefix: 'credential', - name: 'inventoryManage.adhoc.credential', - url: '/credential', - data: { - formChildState: true - }, - params: { - credential_search: { - value: { - page_size: '5' - }, - squash: true, - dynamic: true - } - }, - ncyBreadcrumb: { - skip: true - }, - views: { - 'related': { - templateProvider: function(ListDefinition, generateList) { - let list_html = generateList.build({ - mode: 'lookup', - list: ListDefinition, - input_type: 'radio' - }); - return `${list_html}`; - + 'list@inventories': { + templateProvider: function(InventoriesList, generateList) { + let html = generateList.build({ + list: InventoriesList, + mode: 'edit' + }); + return html; + }, + controller: 'InventoryListController' } } - }, - resolve: { - ListDefinition: ['CredentialList', function(CredentialList) { - let list = _.cloneDeep(CredentialList); - list.lookupConfirmText = 'SELECT'; - return list; - }], - Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.name) || GetBasePath(list.basePath); - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ] - }, - onExit: function($state) { - if ($state.transition) { - $('#form-modal').modal('hide'); - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open'); - } - }, - }; - - return Promise.all([ - inventories, - addGroup, - editGroup, - addHost, - editHost, - ]).then((generated) => { - return { - states: _.reduce(generated, (result, definition) => { - return result.concat(definition.states); - }, [ - stateExtender.buildDefinition(inventoryManageListRoute), - stateExtender.buildDefinition(copyMoveGroupRoute), - stateExtender.buildDefinition(copyMoveHostRoute), - stateExtender.buildDefinition(adHocRoute), - stateExtender.buildDefinition(adhocCredentialLookup) - - ]) - }; + }) }); - } - stateTree = { - name: 'inventories', - url: '/inventories', - ncyBreadcrumb: { - label: N_("INVENTORIES") - }, - lazyLoad: () => generateStateTree() - }; - - $stateProvider.state(stateTree); + $stateProvider.state({ + name: 'hosts', + url: '/hosts', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'hosts', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'HostsList', + form: 'HostsForm', + controllers: { + list: 'HostListController', + add: 'HostAddController', + edit: 'HostEditController' + }, + urls: { + list: '/hosts' + }, + ncyBreadcrumb: { + label: N_('HOSTS') + }, + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') + }, + 'list@hosts': { + templateProvider: function(HostsList, generateList) { + let html = generateList.build({ + list: HostsList, + mode: 'edit' + }); + return html; + }, + controller: 'HostListController' + } + } + }) + }); } ]); diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js deleted file mode 100644 index 4b8e25c051..0000000000 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.controller.js +++ /dev/null @@ -1,308 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Adhoc - * @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran. -*/ -function adhocController($q, $scope, $stateParams, - $state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm, - GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, - KindChange, Wait, ParseTypeChange) { - - ClearScope(); - - // this is done so that we can access private functions for testing, but - // we don't want to populate the "public" scope with these internal - // functions - var privateFn = {}; - this.privateFn = privateFn; - - var id = $stateParams.inventory_id, - hostPattern = $stateParams.pattern; - - // note: put any urls that the controller will use in here!!!! - privateFn.setAvailableUrls = function() { - return { - adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/', - inventoryUrl: GetBasePath('inventory') + id + '/', - machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh' - }; - }; - - var urls = privateFn.setAvailableUrls(); - - // set the default options for the selects of the adhoc form - privateFn.setFieldDefaults = function(verbosity_options, forks_default) { - var verbosity; - for (verbosity in verbosity_options) { - if (verbosity_options[verbosity].isDefault) { - $scope.verbosity = verbosity_options[verbosity]; - } - } - $("#forks-number").spinner("value", forks_default); - $scope.forks = forks_default; - }; - - // set when "working" starts and stops - privateFn.setLoadingStartStop = function() { - var asyncHelper = {}, - formReadyPromise = 0; - - Wait('start'); - - if (asyncHelper.removeChoicesReady) { - asyncHelper.removeChoicesReady(); - } - asyncHelper.removeChoicesReady = $scope.$on('adhocFormReady', - isFormDone); - - // check to see if all requests have completed - function isFormDone() { - formReadyPromise++; - - if (formReadyPromise === 2) { - privateFn.setFieldDefaults($scope.adhoc_verbosity_options, - $scope.forks_field.default); - - CreateSelect2({ - element: '#adhoc_module_name', - multiple: false - }); - - CreateSelect2({ - element: '#adhoc_verbosity', - multiple: false - }); - - Wait('stop'); - } - } - }; - - // set the arguments help to watch on change of the module - privateFn.instantiateArgumentHelp = function() { - $scope.$watch('module_name', function(val) { - if (val) { - // give the docs for the selected module in the popover - $scope.argsPopOver = '

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

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

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

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

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

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

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

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

', - dataTitle: 'Limit', - dataPlacement: 'right', - dataContainer: 'body' - }, - credential: { - label: 'Machine Credential', - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - sourceModel: 'credential', - sourceField: 'name', - class: 'squeeze', - awPopOver: '

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

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

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

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

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

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

', - dataTitle: 'Forks', - dataPlacement: 'right', - dataContainer: "body" - }, - extra_vars: { - label: i18n._('Extra Variables'), - type: 'textarea', - class: 'Form-textAreaLabel Form-formGroup--fullWidth', - rows: 6, - "default": "---", - column: 2, - awPopOver: "

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

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n", - dataTitle: i18n._('Extra Variables'), - dataPlacement: 'right', - dataContainer: "body" - } - }, - buttons: { - reset: { - ngClick: 'formReset()', - ngDisabled: true, - label: 'Reset', - 'class': 'btn btn-sm Form-cancelButton' - }, - launch: { - label: 'Save', - ngClick: 'launchJob()', - ngDisabled: true, - 'class': 'btn btn-sm List-buttonSubmit launchButton' - } - }, - - related: {} - }; -}]; diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.partial.html b/awx/ui/client/src/inventories/manage/adhoc/adhoc.partial.html deleted file mode 100644 index eb25ec5b56..0000000000 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.partial.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
-
-
diff --git a/awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js b/awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js deleted file mode 100644 index 05a8d6dcad..0000000000 --- a/awx/ui/client/src/inventories/manage/adhoc/adhoc.route.js +++ /dev/null @@ -1,28 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - import {templateUrl} from '../../../shared/template-url/template-url.factory'; - import { N_ } from '../../../i18n'; - -export default { - url: '/adhoc', - params:{ - pattern: { - value: 'all', - squash: true - } - }, - name: 'inventoryManage.adhoc', - views: { - 'form@inventoryManage': { - templateUrl: templateUrl('inventories/manage/adhoc/adhoc'), - controller: 'adhocController' - } - }, - ncyBreadcrumb: { - label: N_("RUN COMMAND") - } -}; diff --git a/awx/ui/client/src/inventories/manage/adhoc/main.js b/awx/ui/client/src/inventories/manage/adhoc/main.js deleted file mode 100644 index 1931e8f1d1..0000000000 --- a/awx/ui/client/src/inventories/manage/adhoc/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import adhocController from './adhoc.controller'; -import form from './adhoc.form'; - -export default - angular.module('adhoc', []) - .controller('adhocController', adhocController) - .factory('adhocForm', form); diff --git a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.block.less b/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.block.less deleted file mode 100644 index c87b1b3f52..0000000000 --- a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.block.less +++ /dev/null @@ -1,25 +0,0 @@ -.InventoryManageBreadCrumbs .BreadCrumb-list{ - padding-right: 0px; -} -.InventoryManageBreadCrumb-ncy.BreadCrumb-list{ - padding-left: 0px; -} -.InventoryManageBreadCrumbs-separator{ - content: "/"; - padding: 0 5px; - color: #B7B7B7; -} -.InventoryManageBreadCrumbs{ - position: relative; - height: auto; - top: -36px; - .BreadCrumb-list{ - margin-bottom: 0px; - } -} -.InventoryManage-breakWord{ - word-break: break-all; -} -ol.BreadCrumb-list{ - display: inline-block; -} diff --git a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.controller.js b/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.controller.js deleted file mode 100644 index 0ace8e9dec..0000000000 --- a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.controller.js +++ /dev/null @@ -1,38 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default - ['$state', '$stateParams', '$scope', '$rootScope', 'inventoryData', 'breadCrumbData', function($state, $stateParams, $scope, $rootScope, inventoryData, breadCrumbData){ - // process result data into the same order specified in the traversal path - $scope.groups = _.sortBy(breadCrumbData, function(item){ - var index = _.indexOf($stateParams.group, item.id); - return (index === -1) ? $stateParams.group.length : index; - }); - $scope.inventory = inventoryData; - $scope.currentState = $state.current.name; - // 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; - // slices the group stack at $index to supply new group params to $state.go() - $scope.goToGroup = function(index){ - var group = $stateParams.group.slice(0, index); - $state.go('inventoryManage', {group: group}, {reload: true}); - }; - $scope.goToInventory = function(){ - $state.go('inventoryManage', {group: undefined}, {reload: true}); - }; - - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState){ - $scope.currentState = toState.name; - }); - - // Remove the listener when the scope is destroyed to avoid a memory leak - $scope.$on('$destroy', function() { - cleanUpStateChangeListener(); - }); - }]; diff --git a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.partial.html b/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.partial.html deleted file mode 100644 index 04777cc0f8..0000000000 --- a/awx/ui/client/src/inventories/manage/breadcrumbs/breadcrumbs.partial.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js b/awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js deleted file mode 100644 index 5a98b24591..0000000000 --- a/awx/ui/client/src/inventories/manage/copy-move/copy-move-groups.list.js +++ /dev/null @@ -1,24 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - - -export default { - name: 'groups', - iterator: 'copy', - selectTitle: 'Copy Groups', - index: false, - well: false, - emptyListText: 'PLEASE CREATE ADDITIONAL GROUPS / HOSTS TO PERFORM THIS ACTION', - fields: { - name: { - key: true, - label: 'Target Group Name' - } - }, - basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/groups' -}; diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less b/awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less deleted file mode 100644 index 608f59654a..0000000000 --- a/awx/ui/client/src/inventories/manage/copy-move/copy-move.block.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "./client/src/shared/branding/colors.default.less"; - -#Inventory-copyMovePanel { - .List-searchRow { - width: 50%; - } - .Form-header { - width: 50%; - margin-top: -20px; - } - - .Form-saveButton { - &:disabled { - border-color: @default-icon-hov; - } - } -} -.copyMove-choices { - float: right; - width: 25%; - text-align: right; -} -.copyMove-buttons{ - height: 30px; - margin-top: 20px; - - button { - margin-left: 20px; - } -} -.copyMove-root{ - margin-top: 10px; -} diff --git a/awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html b/awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html deleted file mode 100644 index 030f0c7e3a..0000000000 --- a/awx/ui/client/src/inventories/manage/copy-move/copy-move.partial.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
-
{{item.name}}
-
-
- - -
-
-
- Use the inventory root -
-
- - -
-
diff --git a/awx/ui/client/src/inventories/manage/copy-move/main.js b/awx/ui/client/src/inventories/manage/copy-move/main.js deleted file mode 100644 index a9fbd47660..0000000000 --- a/awx/ui/client/src/inventories/manage/copy-move/main.js +++ /dev/null @@ -1,15 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import CopyMoveGroupsController from './copy-move-groups.controller'; -import CopyMoveHostsController from './copy-move-hosts.controller'; -import CopyMoveGroupList from './copy-move-groups.list'; - -export default -angular.module('manageCopyMove', []) - .controller('CopyMoveGroupsController', CopyMoveGroupsController) - .controller('CopyMoveHostsController', CopyMoveHostsController) - .value('CopyMoveGroupList', CopyMoveGroupList); diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js deleted file mode 100644 index 19a846c414..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js +++ /dev/null @@ -1,33 +0,0 @@ -export default - function GetHostsStatusMsg() { - return function(params) { - var active_failures = params.active_failures, - total_hosts = params.total_hosts, - tip, failures, html_class; - - // Return values for use on host status indicator - - if (active_failures > 0) { - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; - html_class = 'error'; - failures = true; - } else { - failures = false; - if (total_hosts === 0) { - // no hosts - tip = "Contains 0 hosts."; - html_class = 'none'; - } else { - // many hosts with 0 failures - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; - html_class = 'success'; - } - } - - return { - tooltip: tip, - failures: failures, - 'class': html_class - }; - }; - } diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js deleted file mode 100644 index befef8a499..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js +++ /dev/null @@ -1,37 +0,0 @@ -export default - function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { - return function(params) { - var scope = params.scope, - variable = params.variable; - - if (scope[variable] === undefined) { - scope[variable] = []; - Rest.setUrl(GetBasePath('inventory_sources')); - Rest.options() - .success(function (data) { - var i, choices = data.actions.GET.source.choices; - for (i = 0; i < choices.length; i++) { - if (choices[i][0] !== 'file') { - scope[variable].push({ - label: choices[i][1], - value: choices[i][0] - }); - } - } - scope.cloudCredentialRequired = false; - scope.$emit('sourceTypeOptionsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status - }); - }); - } - }; - } - -GetSourceTypeOptions.$inject = - [ 'Rest', - 'ProcessErrors', - 'GetBasePath' - ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js deleted file mode 100644 index 2541abcc27..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js +++ /dev/null @@ -1,77 +0,0 @@ -export default - function GetSyncStatusMsg(Empty) { - 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', - stat, stat_class, status_tip; - - stat = status; - stat_class = stat; - - switch (status) { - case 'never updated': - stat = 'never'; - stat_class = 'na'; - status_tip = 'Sync not performed. Click to start it now.'; - break; - case 'none': - case 'ok': - case '': - launch_class = 'btn-disabled'; - stat = 'n/a'; - stat_class = 'na'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - break; - case 'canceled': - status_tip = 'Sync canceled. Click to view log.'; - break; - case 'failed': - status_tip = 'Sync failed. Click to view log.'; - break; - case 'successful': - status_tip = 'Sync completed. Click to view log.'; - break; - case 'pending': - status_tip = 'Sync pending.'; - launch_class = "btn-disabled"; - launch_tip = "Sync pending"; - break; - case 'updating': - case 'running': - launch_class = "btn-disabled"; - launch_tip = "Sync running"; - status_tip = "Sync running. Click to view log."; - 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, - "status": stat, - "launch_class": launch_class, - "launch_tip": launch_tip, - "schedule_tip": schedule_tip - }; - }; - } - -GetSyncStatusMsg.$inject = - [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js deleted file mode 100644 index 1447d0aa1c..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js +++ /dev/null @@ -1,81 +0,0 @@ -export default - function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { - return function(params) { - var scope = params.scope, - id = params.id, - group = params.group; - - if (scope.removeCancelUpdate) { - scope.removeCancelUpdate(); - } - scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { - // 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 - }); - }); - }); - - if (scope.removeCheckCancel) { - scope.removeCheckCancel(); - } - scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { - // Check that we have access to cancelling an update - var url = (current_update) ? current_update : last_update; - url += 'cancel/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - if (data.can_cancel) { - scope.$emit('CancelUpdate', url); - //} else { - // Wait('stop'); - // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + - // 'the latest status.', 'alert-info'); - } - else { - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET status: ' + status - }); - }); - }); - - // Cancel the update process - if (Empty(group)) { - group = Find({ list: scope.groups, key: 'id', val: id }); - scope.selected_group_id = group.id; - } - - if (group && (group.status === 'running' || group.status === 'pending')) { - // We found the group, and there is a running update - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status - }); - }); - } - }; - } - -GroupsCancelUpdate.$inject = - [ 'Empty', 'Rest', 'ProcessErrors', - 'Alert', 'Wait', 'Find' - ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js deleted file mode 100644 index 1f3280b51c..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js +++ /dev/null @@ -1,46 +0,0 @@ -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 }); - - 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") { - Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + - 'clicking the button.
', 'alert-info', null, null, null, null, true); - } else { - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('SourceReady', data); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' GET returned status: ' + status }); - }); - } - } - }; - } - -ViewUpdateStatus.$inject = - [ '$state', 'Rest', 'ProcessErrors', - 'Alert', 'Wait', 'Empty', 'Find' - ]; diff --git a/awx/ui/client/src/inventories/manage/groups/groups-list.partial.html b/awx/ui/client/src/inventories/manage/groups/groups-list.partial.html deleted file mode 100644 index 1a02f3a515..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/groups-list.partial.html +++ /dev/null @@ -1,79 +0,0 @@ - diff --git a/awx/ui/client/src/inventories/manage/groups/groups.block.less b/awx/ui/client/src/inventories/manage/groups/groups.block.less deleted file mode 100644 index e8387dcd10..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/groups.block.less +++ /dev/null @@ -1,16 +0,0 @@ -.select2-selection.select2-selection--multiple.Form-dropDown{ - height: auto !important; -} -.GroupDelete .Modal-header{ - margin-bottom: 20px; -} -.GroupDelete .modal-body{ - padding-top: 20px; -} -.Inventory-groupManage{ - // ugly hack to avoid the surface area of changing form generator's default classes - .checkbox-inline{ - display: block; - padding-bottom: 5px; - } -} diff --git a/awx/ui/client/src/inventories/manage/groups/groups.form.js b/awx/ui/client/src/inventories/manage/groups/groups.form.js deleted file mode 100644 index 16fee6017d..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/groups.form.js +++ /dev/null @@ -1,360 +0,0 @@ -/************************************************* - * 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 ['NotificationsList', - function(NotificationsList) { - return function() { - var GroupFormObject = { - - addTitle: 'CREATE GROUP', - editTitle: '{{ name }}', - showTitle: true, - name: 'group', - basePath: 'groups', - // the parent node this generated state definition tree expects to attach to - stateTree: 'inventoryManage', - // 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: 'inventoryManage.editGroup', - detailsClick: "$state.go('inventoryManage.editGroup')", - well: false, - 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" + - "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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:
\n" + - "
tag-key=TowerManaged
\n" + - "Limit to hosts using either key pair:
\n" + - "
key-name=staging, key-name=production
\n" + - "Limit to hosts where the Name tag begins with test:
\n" + - "
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:

    " + - "
  • Availability Zone: zones » us-east-1b
  • " + - "
  • Image ID: images » ami-b007ab1e
  • " + - "
  • Instance ID: instances » i-ca11ab1e
  • " + - "
  • Instance Type: types » type_m1_medium
  • " + - "
  • Key Name: keys » key_testing
  • " + - "
  • Region: regions » us-east-1
  • " + - "
  • Security Group: security_groups » security_group_default
  • " + - "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + - "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + - "
  • Tag None: tags » tag_none
  • " + - "

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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/groups.service.js b/awx/ui/client/src/inventories/manage/groups/groups.service.js deleted file mode 100644 index 54ed90dfc7..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/groups.service.js +++ /dev/null @@ -1,113 +0,0 @@ -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('groups') + '?' + 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(group){ - Wait('start'); - this.url = GetBasePath('groups'); - Rest.setUrl(this.url); - return Rest.post(group) - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - put: function(group){ - Wait('start'); - this.url = GetBasePath('groups') + group.id; - Rest.setUrl(this.url); - return Rest.put(group) - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - delete: function(id){ - Wait('start'); - this.url = GetBasePath('groups') + 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/inventories/manage/groups/inventory-groups.list.js b/awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js deleted file mode 100644 index 3c4c12c9c1..0000000000 --- a/awx/ui/client/src/inventories/manage/groups/inventory-groups.list.js +++ /dev/null @@ -1,167 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -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, - index: false, - hover: true, - 'class': 'table-no-border', - multiSelect: true, - trackBy: 'group.id', - - 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, - mode: 'all', - 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)", - 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'}}" - } - }, - - actions: { - refresh: { - mode: 'all', - awToolTip: "Refresh the page", - ngClick: "refreshGroups()", - ngShow: "socketStatus == 'error'", - actionClass: 'btn List-buttonDefault', - buttonContent: 'REFRESH' - }, - 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", - 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(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(group.id)", - awToolTip: 'Edit group', - dataPlacement: "top", - ngShow: "group.summary_fields.user_capabilities.edit" - }, - view: { - //label: 'Edit', - mode: 'all', - ngClick: "editGroup(group.id)", - awToolTip: 'View group', - dataPlacement: "top", - ngShow: "!group.summary_fields.user_capabilities.edit" - }, - "delete": { - //label: 'Delete', - mode: 'all', - ngClick: "deleteGroup(group)", - awToolTip: 'Delete group', - dataPlacement: "top", - ngShow: "group.summary_fields.user_capabilities.delete" - } - } -}; 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 = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; - var action = function(){ - delete $rootScope.promptActionBtnClass; - Wait('start'); - HostManageService.delete(id).then(() => { - $('#prompt-modal').modal('hide'); - if (parseInt($state.params.host_id) === id) { - $state.go("inventoryManage", null, {reload: true}); - } else { - $state.go($state.current.name, null, {reload: true}); - } - Wait('stop'); - }); - }; - // Prompt depends on having $rootScope.promptActionBtnClass available... - Prompt({ - hdr: 'Delete Host', - body: body, - action: action, - actionText: 'DELETE', - }); - $rootScope.promptActionBtnClass = 'Modal-errorButton'; - }; - $scope.copyMoveHost = function(id){ - $state.go('inventoryManage.copyMoveHost', {host_id: id}); - }; - $scope.systemTracking = function(){ - var hostIds = _.map($scope.$parent.hostsSelectedItems, (host) => host.id); - $state.go('systemTracking', { - inventory: inventoryData, - inventoryId: $stateParams.inventory_id, - hosts: $scope.$parent.hostsSelectedItems, - hostIds: hostIds - }); - }; - // $scope.$parent governed by InventoryManageController, for unified multiSelect options - $scope.$on('multiSelectList.selectionChanged', (event, selection) => { - $scope.$parent.hostsSelected = selection.length > 0 ? true : false; - $scope.$parent.hostsSelectedItems = selection.selectedItems; - $scope.$parent.systemTrackingDisabled = selection.length > 0 && selection.length < 3 ? false : true; - $scope.$parent.systemTrackingTooltip = selection.length > 0 && selection.length < 3 ? "Compare host facts over time" : "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."; - }); - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "inventoryManage.editHost") { - $scope.rowBeingEdited = toParams.host_id; - $scope.listBeingEdited = "hosts"; - } - 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(); - }); - }]; diff --git a/awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js b/awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js deleted file mode 100644 index a2793638a3..0000000000 --- a/awx/ui/client/src/inventories/manage/hosts/inventory-hosts.list.js +++ /dev/null @@ -1,119 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -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, - index: false, - hover: true, - hasChildren: true, - 'class': 'table-no-border', - multiSelect: true, - trackBy: 'host.id', - - 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: "{{ 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: 'Hosts', - ngClick: "editHost(host.id)", - ngClass: "{ 'host-disabled-label': !host.enabled }", - columnClass: 'col-lg-6 col-md-8 col-sm-8 col-xs-7', - dataHostId: "{{ host.id }}", - dataType: "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(host.id)", - awToolTip: 'Copy or move host to another group', - dataPlacement: "top", - ngShow: 'host.summary_fields.user_capabilities.edit' - }, - 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' - }, - "delete": { - //label: 'Delete', - ngClick: "deleteHost(host.id, host.name)", - icon: 'icon-trash', - awToolTip: 'Delete host', - dataPlacement: 'top', - ngShow: 'host.summary_fields.user_capabilities.delete' - } - }, - - actions: { - 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/manage/hosts/main.js b/awx/ui/client/src/inventories/manage/hosts/main.js deleted file mode 100644 index b3d514a4f4..0000000000 --- a/awx/ui/client/src/inventories/manage/hosts/main.js +++ /dev/null @@ -1,21 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import HostsAddController from './hosts-add.controller'; -import HostsEditController from './hosts-edit.controller'; -import SetStatus from './factories/set-status.factory'; -import SetEnabledMsg from './factories/set-enabled-msg.factory'; -import InventoryHosts from './inventory-hosts.list'; -import HostForm from './hosts.form'; - -export default -angular.module('manageHosts', []) - .factory('SetStatus', SetStatus) - .factory('SetEnabledMsg', SetEnabledMsg) - .factory('HostForm', HostForm) - .value('InventoryHosts', InventoryHosts) - .controller('HostsAddController', HostsAddController) - .controller('HostEditController', HostsEditController); diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.block.less b/awx/ui/client/src/inventories/manage/inventory-manage.block.less deleted file mode 100644 index caaf039058..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.block.less +++ /dev/null @@ -1,3 +0,0 @@ -.InventoryManage-container{ - margin-top: -36px; -} diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.controller.js b/awx/ui/client/src/inventories/manage/inventory-manage.controller.js deleted file mode 100644 index f3201d475a..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.controller.js +++ /dev/null @@ -1,28 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ -export default - ['$scope', '$state', 'inventoryData', function($scope, $state, inventoryData){ - $scope.groupsSelected = false; - $scope.hostsSelected = false; - $scope.hostsSelectedItems = []; - $scope.groupsSelectedItems = []; - - $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; - - $scope.setAdhocPattern = function(){ - var pattern = _($scope.groupsSelectedItems) - .concat($scope.hostsSelectedItems) - .map(function(item){ - return item.name; - }).value().join(':'); - - $state.go('inventoryManage.adhoc', {pattern: pattern}); - }; - - $scope.$watchGroup(['groupsSelected', 'hostsSelected'], function(newVals) { - $scope.adhocCommandTooltip = (newVals[0] || newVals[1]) ? "Run a command on the selected inventory" : "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."; - }); - }]; diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.partial.html b/awx/ui/client/src/inventories/manage/inventory-manage.partial.html deleted file mode 100644 index 80a4bc2707..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.partial.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/inventories/manage/main.js b/awx/ui/client/src/inventories/manage/main.js deleted file mode 100644 index 79925117ec..0000000000 --- a/awx/ui/client/src/inventories/manage/main.js +++ /dev/null @@ -1,24 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import InventoryManageService from './inventory-manage.service'; -import HostManageService from './hosts/hosts.service'; -import GroupManageService from './groups/groups.service'; -import hosts from './hosts/main'; -import groups from './groups/main'; -import adhoc from './adhoc/main'; -import copyMove from './copy-move/main'; - -export default -angular.module('inventoryManage', [ - hosts.name, - groups.name, - copyMove.name, - adhoc.name - ]) - .service('InventoryManageService', InventoryManageService) - .service('HostManageService', HostManageService) - .service('GroupManageService', GroupManageService); diff --git a/awx/ui/client/src/inventories/related-hosts/edit/build-host-edit-state.factory.js b/awx/ui/client/src/inventories/related-hosts/edit/build-host-edit-state.factory.js new file mode 100644 index 0000000000..7dfbd5d819 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/edit/build-host-edit-state.factory.js @@ -0,0 +1,73 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import RelatedHostEditController from './host-edit.controller'; + +export default ['$stateExtender', 'templateUrl', '$injector', + 'RelatedHostsFormDefinition', 'NestedHostsFormDefinition', + 'nestedGroupListState', + function($stateExtender, templateUrl, $injector, + RelatedHostsFormDefinition, NestedHostsFormDefinition, + nestedGroupListState){ + 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/:host_id`, + ncyBreadcrumb: { + parent: `${formStateDefinition.name}`, + label: `${breadcrumbLabel}` + }, + views: { + 'hostForm@inventories': { + templateProvider: function(GenerateForm, RelatedHostsFormDefinition, NestedHostsFormDefinition, $stateParams) { + let form = RelatedHostsFormDefinition; + if($stateParams.group_id){ + form = NestedHostsFormDefinition; + } + return GenerateForm.buildHTML(form, { + mode: 'edit', + related: false + }); + }, + controller: RelatedHostEditController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }], + host: ['$stateParams', 'HostManageService', function($stateParams, HostManageService) { + return HostManageService.get({ id: $stateParams.host_id }).then(function(res) { + return res.data.results[0]; + }); + }] + } + }; + var relatedGroupListState; + state = $stateExtender.buildDefinition(stateConfig); + if(stateConfig.name === "inventories.edit.groups.edit.nested_hosts.edit"){ + relatedGroupListState = nestedGroupListState(NestedHostsFormDefinition.related.nested_groups, state, params); + relatedGroupListState = $stateExtender.buildDefinition(relatedGroupListState); + states.push(state, relatedGroupListState); + return states; + } + if(stateConfig.name === "inventories.edit.hosts.edit"){ + relatedGroupListState = nestedGroupListState(RelatedHostsFormDefinition.related.nested_groups, state, params); + relatedGroupListState = $stateExtender.buildDefinition(relatedGroupListState); + states.push(state, relatedGroupListState); + return states; + } + else { + return state; + } + + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js b/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js deleted file mode 100644 index 17f42276d0..0000000000 --- a/awx/ui/client/src/inventoriesnew/add/inventory-add.controller.js +++ /dev/null @@ -1,104 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Inventories - * @description This controller's for the Inventory page - */ - -function InventoriesAdd($scope, $location, - GenerateForm, InventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors, - ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, - $state) { - - $scope.canAdd = false; - rbacUiControlService.canAdd(GetBasePath('inventory')) - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - Rest.setUrl(GetBasePath('inventory')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert('Permission Error', 'You do not have permission to add an inventory.', 'alert-info'); - } - }); - - ClearScope(); - - // Inject dynamic view - var defaultUrl = GetBasePath('inventory'), - form = InventoryForm; - - init(); - - function init() { - $scope.canEditOrg = true; - form.formLabelSize = null; - form.formFieldSize = null; - - // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); - - $scope.parseType = 'yaml'; - ParseTypeChange({ - scope: $scope, - variable: 'variables', - parse_variable: 'parseType', - field_id: 'inventory_variables' - }); - } - - // Save - $scope.formSave = function() { - Wait('start'); - try { - var fld, json_data, data; - - json_data = ToJSON($scope.parseType, $scope.variables, true); - - data = {}; - for (fld in form.fields) { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = $scope[fld]; - } else { - data[fld] = $scope[fld]; - } - } - - Rest.setUrl(defaultUrl); - Rest.post(data) - .success(function(data) { - var inventory_id = data.id; - Wait('stop'); - $location.path('/inventoriesnew/' + inventory_id); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to add new inventory. Post returned status: ' + status - }); - }); - } catch (err) { - Wait('stop'); - Alert("Error", "Error parsing inventory variables. Parser returned: " + err); - } - - }; - - $scope.formCancel = function() { - $state.go('inventoriesnew'); - }; -} - -export default ['$scope', '$location', - 'GenerateForm', 'InventoryForm', 'rbacUiControlService', 'Rest', 'Alert', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', - 'Wait', 'ToJSON', '$state', InventoriesAdd -]; diff --git a/awx/ui/client/src/inventoriesnew/add/main.js b/awx/ui/client/src/inventoriesnew/add/main.js deleted file mode 100644 index 2d3c391495..0000000000 --- a/awx/ui/client/src/inventoriesnew/add/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import controller from './inventory-add.controller'; - -export default -angular.module('newInventoryAdd', []) - .controller('NewInventoryAddController', controller); diff --git a/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js b/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js deleted file mode 100644 index 98c1549499..0000000000 --- a/awx/ui/client/src/inventoriesnew/edit/inventory-edit.controller.js +++ /dev/null @@ -1,136 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Inventories - * @description This controller's for the Inventory page - */ - -function InventoriesEdit($scope, $location, - $stateParams, InventoriesNewForm, Rest, ProcessErrors, - ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, - ParseVariableString, $state, OrgAdminLookup) { - - // Inject dynamic view - var defaultUrl = GetBasePath('inventory'), - form = InventoriesNewForm, - inventory_id = $stateParams.inventory_id, - master = {}, - fld, json_data, data; - - ClearScope(); - init(); - - function init() { - ClearScope(); - form.formLabelSize = null; - form.formFieldSize = null; - $scope.inventory_id = inventory_id; - - $scope.$watch('inventory_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - } - - - Wait('start'); - Rest.setUrl(GetBasePath('inventory') + inventory_id + '/'); - Rest.get() - .success(function(data) { - var fld; - for (fld in form.fields) { - if (fld === 'variables') { - $scope.variables = ParseVariableString(data.variables); - master.variables = $scope.variables; - } else if (fld === 'inventory_name') { - $scope[fld] = data.name; - master[fld] = $scope[fld]; - } else if (fld === 'inventory_description') { - $scope[fld] = data.description; - master[fld] = $scope[fld]; - } else if (data[fld]) { - $scope[fld] = data[fld]; - master[fld] = $scope[fld]; - } - if (form.fields[fld].sourceModel && data.summary_fields && - data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - } - } - - Wait('stop'); - $scope.parseType = 'yaml'; - ParseTypeChange({ - scope: $scope, - variable: 'variables', - parse_variable: 'parseType', - field_id: 'inventory_variables' - }); - - OrgAdminLookup.checkForAdminAccess({organization: data.organization}) - .then(function(canEditOrg){ - $scope.canEditOrg = canEditOrg; - }); - - $scope.inventory_obj = data; - $scope.name = data.name; - - $scope.$emit('inventoryLoaded'); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status - }); - }); - // Save - $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) { - data[form.fields[fld].realName] = $scope[fld]; - } else { - data[fld] = $scope[fld]; - } - } - - Rest.setUrl(defaultUrl + inventory_id + '/'); - Rest.put(data) - .success(function() { - Wait('stop'); - $state.go($state.current, {}, { reload: true }); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to update inventory. PUT returned status: ' + status - }); - }); - }; - - $scope.formCancel = function() { - $state.go('inventoriesnew'); - }; - -} - -export default ['$scope', '$location', - '$stateParams', 'InventoriesNewForm', 'Rest', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', - 'ToJSON', 'ParseVariableString', - '$state', 'OrgAdminLookup', InventoriesEdit, -]; diff --git a/awx/ui/client/src/inventoriesnew/edit/main.js b/awx/ui/client/src/inventoriesnew/edit/main.js deleted file mode 100644 index a6a404a869..0000000000 --- a/awx/ui/client/src/inventoriesnew/edit/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import controller from './inventory-edit.controller'; - -export default - angular.module('newInventoryEdit', []) - .controller('NewInventoryEditController', controller); diff --git a/awx/ui/client/src/inventoriesnew/hosts/main.js b/awx/ui/client/src/inventoriesnew/hosts/main.js deleted file mode 100644 index 7363b267f9..0000000000 --- a/awx/ui/client/src/inventoriesnew/hosts/main.js +++ /dev/null @@ -1,20 +0,0 @@ -/************************************************* - * Copyright (c) 2017 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - import hostAdd from './add/main'; - import hostEdit from './edit/main'; - import hostList from './list/main'; - import HostsNewList from './host.list'; - import HostsNewForm from './host.form'; - -export default -angular.module('hostnew', [ - hostAdd.name, - hostEdit.name, - hostList.name - ]) - .factory('HostsNewForm', HostsNewForm) - .factory('HostsNewList', HostsNewList); diff --git a/awx/ui/client/src/inventoriesnew/inventory.form.js b/awx/ui/client/src/inventoriesnew/inventory.form.js deleted file mode 100644 index a138461f49..0000000000 --- a/awx/ui/client/src/inventoriesnew/inventory.form.js +++ /dev/null @@ -1,135 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name forms.function:Inventories - * @description This form is for adding/editing an inventory - */ - -export default ['i18n', function(i18n) { - return { - - addTitle: i18n._('NEW INVENTORY'), - editTitle: '{{ inventory_name }}', - name: 'inventory', - basePath: 'inventory', - // the top-most node of this generated state tree - stateTree: 'inventoriesnew', - tabs: true, - - fields: { - inventory_name: { - realName: 'name', - label: i18n._('Name'), - type: 'text', - required: true, - capitalize: false, - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - inventory_description: { - realName: 'description', - label: i18n._('Description'), - type: 'text', - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' - }, - organization: { - label: i18n._('Organization'), - type: 'lookup', - basePath: 'organizations', - list: 'OrganizationList', - sourceModel: 'organization', - sourceField: 'name', - awRequiredWhen: { - reqExpression: "organizationrequired", - init: "true" - }, - ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', - awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' - }, - variables: { - label: i18n._('Variables'), - type: 'textarea', - class: 'Form-formGroup--fullWidth', - rows: 6, - "default": "---", - awPopOver: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

' + 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: { - 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', - } - } - } - } - - };}]; diff --git a/awx/ui/client/src/inventoriesnew/inventory.list.js b/awx/ui/client/src/inventoriesnew/inventory.list.js deleted file mode 100644 index c06f3703ec..0000000000 --- a/awx/ui/client/src/inventoriesnew/inventory.list.js +++ /dev/null @@ -1,98 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - -export default ['i18n', function(i18n) { - return { - - name: 'inventoriesnew', - iterator: 'inventory', - selectTitle: i18n._('Add Inventories'), - editTitle: i18n._('INVENTORIES'), - listTitle: i18n._('INVENTORIES'), - selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), " "), - index: false, - hover: true, - basePath: 'inventory', - title: false, - - fields: { - status: { - label: '', - columnClass: 'List-staticColumn--mediumStatus', - nosort: true, - ngClick: "null", - iconOnly: true, - excludeModal: true, - icons: [{ - icon: "{{ 'icon-cloud-' + inventory.syncStatus }}", - awToolTip: "{{ inventory.syncTip }}", - awTipPlacement: "right", - ngClick: "showGroupSummary($event, inventory.id)", - ngClass: "inventory.launch_class" - },{ - icon: "{{ 'icon-job-' + inventory.hostsStatus }}", - awToolTip: false, - ngClick: "showHostSummary($event, inventory.id)", - ngClass: "" - }] - }, - name: { - key: true, - label: i18n._('Name'), - columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent', - modalColumnClass: 'col-md-11', - linkTo: '/#/inventoriesnew/{{inventory.id}}' - }, - organization: { - label: i18n._('Organization'), - ngBind: 'inventory.summary_fields.organization.name', - linkTo: '/#/organizations/{{ inventory.organization }}', - sourceModel: 'organization', - sourceField: 'name', - excludeModal: true, - columnClass: 'col-md-5 col-sm-3 hidden-xs' - } - }, - - actions: { - add: { - mode: 'all', // One of: edit, select, all - ngClick: 'addInventory()', - awToolTip: i18n._('Create a new inventory'), - actionClass: 'btn List-buttonSubmit', - buttonContent: '+ ' + i18n._('ADD'), - ngShow: 'canAdd' - } - }, - - fieldActions: { - - columnClass: 'col-md-2 col-sm-4 col-xs-4', - - edit: { - label: i18n._('Edit'), - ngClick: 'editInventory(inventory.id)', - awToolTip: i18n._('Edit inventory'), - dataPlacement: 'top', - ngShow: 'inventory.summary_fields.user_capabilities.edit' - }, - view: { - label: i18n._('View'), - ngClick: 'editInventory(inventory.id)', - awToolTip: i18n._('View inventory'), - dataPlacement: 'top', - ngShow: '!inventory.summary_fields.user_capabilities.edit' - }, - "delete": { - label: i18n._('Delete'), - ngClick: "deleteInventory(inventory.id, inventory.name)", - awToolTip: i18n._('Delete inventory'), - dataPlacement: 'top', - ngShow: 'inventory.summary_fields.user_capabilities.delete' - } - } - };}]; diff --git a/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js b/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js deleted file mode 100644 index e37f9ab6e8..0000000000 --- a/awx/ui/client/src/inventoriesnew/list/inventory-list.controller.js +++ /dev/null @@ -1,308 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Inventories - * @description This controller's for the Inventory page - */ - -function InventoriesList($scope, $rootScope, $location, - $compile, $filter, Rest, InventoriesNewList, Prompt, - ProcessErrors, GetBasePath, Wait, Find, Empty, $state, rbacUiControlService, Dataset) { - - let list = InventoriesNewList, - defaultUrl = GetBasePath('inventory'); - - init(); - - function init(){ - $scope.canAdd = false; - - rbacUiControlService.canAdd('inventory') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - $scope.$watchCollection(list.name, function(){ - _.forEach($scope[list.name], buildStatusIndicators); - }); - - // Search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $rootScope.flashMessage = null; - - } - - function buildStatusIndicators(inventory){ - inventory.launch_class = ""; - if (inventory.has_inventory_sources) { - if (inventory.inventory_sources_with_failures > 0) { - inventory.syncStatus = 'error'; - inventory.syncTip = inventory.inventory_sources_with_failures + ' groups with sync failures. Click for details'; - } - else { - inventory.syncStatus = 'successful'; - inventory.syncTip = 'No inventory sync failures. Click for details.'; - } - } - else { - inventory.syncStatus = 'na'; - inventory.syncTip = 'Not configured for inventory sync.'; - inventory.launch_class = "btn-disabled"; - } - if (inventory.has_active_failures) { - inventory.hostsStatus = 'error'; - inventory.hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.'; - } - else if (inventory.total_hosts) { - inventory.hostsStatus = 'successful'; - inventory.hostsTip = 'No hosts with failures. Click for details.'; - } - else { - inventory.hostsStatus = 'none'; - inventory.hostsTip = 'Inventory contains 0 hosts.'; - } - } - - function ellipsis(a) { - if (a.length > 20) { - return a.substr(0,20) + '...'; - } - return a; - } - - function attachElem(event, html, title) { - var elem = $(event.target).parent(); - try { - elem.tooltip('hide'); - elem.popover('destroy'); - } - catch(err) { - //ignore - } - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - $('.tooltip').each( function() { - // close any lingering tool tipss - $(this).hide(); - }); - elem.attr({ - "aw-pop-over": html, - "data-popover-title": title, - "data-placement": "right" }); - elem.removeAttr('ng-click'); - $compile(elem)($scope); - $scope.triggerPopover(event); - } - if ($scope.removeHostSummaryReady) { - $scope.removeHostSummaryReady(); - } - $scope.removeHostSummaryReady = $scope.$on('HostSummaryReady', function(e, event, data) { - - var html, title = "Recent Jobs"; - Wait('stop'); - if (data.count > 0) { - html = "\n"; - html += "\n"; - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - html += "\n"; - html += "\n"; - - data.results.forEach(function(row) { - html += "\n"; - html += "\n"; - html += ""; - html += ""; - html += "\n"; - }); - html += "\n"; - html += "
StatusFinishedName
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + $filter('sanitize')(ellipsis(row.name)) + "
\n"; - } - else { - html = "

No recent job data available for this inventory.

\n"; - } - attachElem(event, html, title); - }); - - if ($scope.removeGroupSummaryReady) { - $scope.removeGroupSummaryReady(); - } - $scope.removeGroupSummaryReady = $scope.$on('GroupSummaryReady', function(e, event, inventory, data) { - var html, title; - - Wait('stop'); - - // Build the html for our popover - html = "\n"; - html += "\n"; - html += ""; - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - html += "\n"; - data.results.forEach( function(row) { - if (row.related.last_update) { - html += ""; - html += ``; - html += ""; - html += ""; - html += "\n"; - } - else { - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - } - }); - html += "\n"; - html += "
StatusLast SyncGroup
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "
NA" + $filter('sanitize')(ellipsis(row.summary_fields.group.name)) + "
\n"; - title = "Sync Status"; - attachElem(event, html, title); - }); - - $scope.showGroupSummary = function(event, id) { - try{ - var elem = $(event.target).parent(); - // if the popover is visible already, then exit the function here - if(elem.data()['bs.popover'].tip().hasClass('in')){ - return; - } - } - catch(err){ - var inventory; - if (!Empty(id)) { - inventory = Find({ list: $scope.inventories, key: 'id', val: id }); - if (inventory.syncStatus !== 'na') { - Wait('start'); - Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5'); - Rest.get() - .success(function(data) { - $scope.$emit('GroupSummaryReady', event, inventory, data); - }) - .error(function(data, status) { - ProcessErrors( $scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status - }); - }); - } - } - } - }; - - $scope.showHostSummary = function(event, id) { - try{ - var elem = $(event.target).parent(); - // if the popover is visible already, then exit the function here - if(elem.data()['bs.popover'].tip().hasClass('in')){ - return; - } - } - catch(err){ - var url, inventory; - if (!Empty(id)) { - inventory = Find({ list: $scope.inventories, key: 'id', val: id }); - if (inventory.total_hosts > 0) { - Wait('start'); - url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed="; - url += (inventory.has_active_failures) ? 'true' : "false"; - url += "&order_by=-finished&page_size=5"; - Rest.setUrl(url); - Rest.get() - .success( function(data) { - $scope.$emit('HostSummaryReady', event, data); - }) - .error( function(data, status) { - ProcessErrors( $scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET returned: ' + status - }); - }); - } - } - } - }; - - $scope.viewJob = function(url) { - - // Pull the id out of the URL - var id = url.replace(/^\//, '').split('/')[3]; - - $state.go('inventorySyncStdout', {id: id}); - - }; - - $scope.addInventory = function () { - $state.go('inventoriesnew.add'); - }; - - $scope.editInventory = function (id) { - $state.go('inventoriesnew.edit', {inventory_id: id}); - }; - - $scope.manageInventory = function(id){ - $location.path($location.path() + '/' + id + '/manage'); - }; - - $scope.deleteInventory = function (id, name) { - - var action = function () { - var url = defaultUrl + id + '/'; - Wait('start'); - $('#prompt-modal').modal('hide'); - Rest.setUrl(url); - Rest.destroy() - .success(function () { - if (parseInt($state.params.inventory_id) === id) { - $state.go("^", null, {reload: true}); - } else { - $state.go('.', null, {reload: true}); - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors( $scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status - }); - }); - }; - - Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to delete the inventory below?
' + $filter('sanitize')(name) + '
', - action: action, - actionText: 'DELETE' - }); - }; - - // Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status - $scope.viewJobs = function (id) { - $location.url('/jobs/?inventory__int=' + id); - }; - - $scope.viewFailedJobs = function (id) { - $location.url('/jobs/?inventory__int=' + id + '&status=failed'); - }; -} - -export default ['$scope', '$rootScope', '$location', - '$compile', '$filter', 'Rest', 'InventoriesNewList', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', 'Dataset', InventoriesList -]; diff --git a/awx/ui/client/src/inventoriesnew/list/main.js b/awx/ui/client/src/inventoriesnew/list/main.js deleted file mode 100644 index 09550f45ca..0000000000 --- a/awx/ui/client/src/inventoriesnew/list/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import controller from './inventory-list.controller'; - -export default -angular.module('newInventoryList', []) - .controller('NewInventoryListController', controller); diff --git a/awx/ui/client/src/inventoriesnew/main.js b/awx/ui/client/src/inventoriesnew/main.js deleted file mode 100644 index 4e736caf9a..0000000000 --- a/awx/ui/client/src/inventoriesnew/main.js +++ /dev/null @@ -1,236 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import hostnew from './hosts/main'; -import inventoryAdd from './add/main'; -import inventoryEdit from './edit/main'; -import inventoryList from './list/main'; -import { templateUrl } from '../shared/template-url/template-url.factory'; -import { N_ } from '../i18n'; -// import inventoriesnewRoute from './inventories.route'; -import InventoriesNewList from './inventory.list'; -import InventoriesNewForm from './inventory.form'; -export default -angular.module('inventorynew', [ - hostnew.name, - inventoryAdd.name, - inventoryEdit.name, - inventoryList.name - ]) - .factory('InventoriesNewForm', InventoriesNewForm) - .factory('InventoriesNewList', InventoriesNewList) - .config(['$stateProvider', '$stateExtenderProvider', 'stateDefinitionsProvider', - function($stateProvider, $stateExtenderProvider, stateDefinitionsProvider) { - // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states - // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree - let stateDefinitions = stateDefinitionsProvider.$get(); - - $stateProvider.state({ - name: 'inventoriesnew', - url: '/inventoriesnew', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'inventoriesnew', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'InventoriesNewList', - form: 'InventoriesNewForm', - controllers: { - list: 'NewInventoryListController', - add: 'NewInventoryAddController', - edit: 'NewInventoryEditController' - }, - ncyBreadcrumb: { - label: N_('INVENTORIESNEW') - }, - views: { - '@': { - templateUrl: templateUrl('inventoriesnew/inventories') - }, - 'list@inventoriesnew': { - templateProvider: function(InventoriesNewList, generateList) { - let html = generateList.build({ - list: InventoriesNewList, - mode: 'edit' - }); - return html; - }, - controller: 'NewInventoryListController' - } - } - }) - }); - - $stateProvider.state({ - name: 'hostsnew', - url: '/hostsnew', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'hostsnew', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'HostsNewList', - form: 'HostsNewForm', - controllers: { - list: 'NewHostListController', - add: 'NewHostAddController', - edit: 'NewHostEditController' - }, - ncyBreadcrumb: { - label: N_('HOSTSNEW') - }, - views: { - '@': { - templateUrl: templateUrl('inventoriesnew/inventories') - }, - 'list@hostsnew': { - templateProvider: function(HostsNewList, generateList) { - let html = generateList.build({ - list: HostsNewList, - mode: 'edit' - }); - return html; - }, - controller: 'NewHostListController' - } - } - }) - }); - - - - // function generateInvAddStateTree() { - // - // let addInventory = stateDefinitions.generateTree({ - // url: '/add', - // name: 'inventoriesnew.add', - // modes: ['add'], - // form: 'InventoriesNewForm', - // controllers: { - // add: 'NewInventoryAddController' - // } - // }); - // - // return Promise.all([ - // addInventory, - // ]).then((generated) => { - // return { - // states: _.reduce(generated, (result, definition) => { - // return result.concat(definition.states); - // }, []) - // }; - // }); - // } - // - // function generateInvEditStateTree() { - // - // let editInventory = stateDefinitions.generateTree({ - // url: '/:inventory_id', - // name: 'inventoriesnew.edit', - // modes: ['edit'], - // form: 'InventoriesNewForm', - // controllers: { - // edit: 'NewInventoryEditController' - // } - // }); - // - // return Promise.all([ - // editInventory, - // ]).then((generated) => { - // return { - // states: _.reduce(generated, (result, definition) => { - // return result.concat(definition.states); - // }, []) - // }; - // }); - // } - // - // let inventoriesnew = { - // name: 'inventoriesnew', - // route: '/inventoriesnew', - // ncyBreadcrumb: { - // label: N_("INVENTORIESNEW") - // }, - // params: { - // inventory_search: { - // value: {order_by: 'name', page_size: '20', role_level: 'admin_role'}, - // dynamic: true - // } - // }, - // resolve: { - // Dataset: ['InventoriesNewList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { - // let path = GetBasePath(list.basePath) || GetBasePath(list.name); - // return qs.search(path, $stateParams[`${list.iterator}_search`]); - // }], - // ListDefinition: ['InventoriesNewList', (list) => { - // return list; - // }] - // }, - // views: { - // '@': { - // templateUrl: templateUrl('inventoriesnew/inventories') - // }, - // 'list@inventoriesnew': { - // templateProvider: function(InventoriesNewList, generateList) { - // let html = generateList.build({ - // list: InventoriesNewList, - // mode: 'edit' - // }); - // return html; - // }, - // controller: 'NewInventoryListController' - // } - // } - // }; - // stateExtender.addState(inventoriesnew); - // - // let hostsnew = { - // name: 'inventoriesnew.hosts', - // route: '/hosts', - // ncyBreadcrumb: { - // label: N_("HOSTS") - // }, - // params: { - // host_search: { - // value: {order_by: 'name', page_size: '20'}, - // dynamic: true - // } - // }, - // resolve: { - // Dataset: ['HostsNewList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => { - // let path = GetBasePath(list.basePath) || GetBasePath(list.name); - // return qs.search(path, $stateParams[`${list.iterator}_search`]); - // }], - // ListDefinition: ['HostsNewList', (list) => { - // return list; - // }] - // }, - // views: { - // 'list@inventoriesnew': { - // templateProvider: function(HostsNewList, generateList) { - // let html = generateList.build({ - // list: HostsNewList, - // mode: 'edit' - // }); - // return html; - // }, - // controller: 'NewHostListController' - // } - // } - // }; - // stateExtender.addState(hostsnew); - // - // let addInventoryTree = { - // name: 'inventoriesnew.add', - // url: '/add', - // lazyLoad: () => generateInvAddStateTree() - // }; - // $stateProvider.state(addInventoryTree); - // - // let editInventoryTree = { - // name: 'inventoriesnew.edit', - // url: '/:inventory_id', - // lazyLoad: () => generateInvEditStateTree() - // }; - // $stateProvider.state(editInventoryTree); - } - ]); diff --git a/awx/ui/client/src/main-menu/main-menu.partial.html b/awx/ui/client/src/main-menu/main-menu.partial.html index 4d28ea22e8..398488add7 100644 --- a/awx/ui/client/src/main-menu/main-menu.partial.html +++ b/awx/ui/client/src/main-menu/main-menu.partial.html @@ -27,14 +27,6 @@ INVENTORIES - - - INVENTORIES NEW - - INVENTORIES - - - INVENTORIES NEW - - - -
+
From 6608819c959d39f23023c53edf248e92e04febca Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 6 Apr 2017 16:28:39 -0400 Subject: [PATCH 04/49] removed css rule targeting host/group search bar --- awx/ui/client/src/shared/smart-search/smart-search.block.less | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/smart-search.block.less b/awx/ui/client/src/shared/smart-search/smart-search.block.less index c87bc70364..23070e8759 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.block.less +++ b/awx/ui/client/src/shared/smart-search/smart-search.block.less @@ -16,10 +16,6 @@ line-height: 20px; width: 50%; } -// `.${list.name}List` class can be used to set add custom class overrides -.groupsList .SmartSearch-bar, .hostsList .SmartSearch-bar, .PortalMode .SmartSearch-bar{ - width: 100%; -} .SmartSearch-tags{ padding-left: 0px; From 2e177598c2494e60bfde834eb7d5a604b87cef11 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 6 Apr 2017 16:33:12 -0400 Subject: [PATCH 05/49] Fixed add inventory form --- .../client/src/inventories/add/inventory-add.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 02e608af6c..19da6885dd 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -11,7 +11,7 @@ */ function InventoriesAdd($scope, $location, - GenerateForm, InventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors, + GenerateForm, InventoriesForm, rbacUiControlService, Rest, Alert, ProcessErrors, ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, $state) { @@ -34,7 +34,7 @@ function InventoriesAdd($scope, $location, // Inject dynamic view var defaultUrl = GetBasePath('inventory'), - form = InventoryForm; + form = InventoriesForm; init(); @@ -98,7 +98,7 @@ function InventoriesAdd($scope, $location, } export default ['$scope', '$location', - 'GenerateForm', 'InventoryForm', 'rbacUiControlService', 'Rest', 'Alert', + 'GenerateForm', 'InventoriesForm', 'rbacUiControlService', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state', InventoriesAdd ]; From 54a92b5b5ef23b477432deeb1018d370d5d24dff Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 6 Apr 2017 16:49:09 -0400 Subject: [PATCH 06/49] Normalized inventory list/form. This was the way it used to be --- .../inventories/add/inventory-add.controller.js | 6 +++--- .../edit/inventory-edit.controller.js | 6 +++--- .../list/inventory-list.controller.js | 6 +++--- awx/ui/client/src/inventories/main.js | 16 ++++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) 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 19da6885dd..02e608af6c 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -11,7 +11,7 @@ */ function InventoriesAdd($scope, $location, - GenerateForm, InventoriesForm, rbacUiControlService, Rest, Alert, ProcessErrors, + GenerateForm, InventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors, ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, $state) { @@ -34,7 +34,7 @@ function InventoriesAdd($scope, $location, // Inject dynamic view var defaultUrl = GetBasePath('inventory'), - form = InventoriesForm; + form = InventoryForm; init(); @@ -98,7 +98,7 @@ function InventoriesAdd($scope, $location, } export default ['$scope', '$location', - 'GenerateForm', 'InventoriesForm', 'rbacUiControlService', 'Rest', 'Alert', + 'GenerateForm', 'InventoryForm', 'rbacUiControlService', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state', InventoriesAdd ]; 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 c08f101ce0..69ac5ccec3 100644 --- a/awx/ui/client/src/inventories/edit/inventory-edit.controller.js +++ b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js @@ -11,13 +11,13 @@ */ function InventoriesEdit($scope, $location, - $stateParams, InventoriesForm, Rest, ProcessErrors, + $stateParams, InventoryForm, Rest, ProcessErrors, ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, ParseVariableString, $state, OrgAdminLookup) { // Inject dynamic view var defaultUrl = GetBasePath('inventory'), - form = InventoriesForm, + form = InventoryForm, inventory_id = $stateParams.inventory_id, master = {}, fld, json_data, data; @@ -129,7 +129,7 @@ function InventoriesEdit($scope, $location, } export default ['$scope', '$location', - '$stateParams', 'InventoriesForm', 'Rest', + '$stateParams', 'InventoryForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString', '$state', 'OrgAdminLookup', InventoriesEdit, diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 6a2f0e261b..2f43f48100 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -11,10 +11,10 @@ */ function InventoriesList($scope, $rootScope, $location, - $compile, $filter, Rest, InventoriesList, Prompt, + $compile, $filter, Rest, InventoryList, Prompt, ProcessErrors, GetBasePath, Wait, Find, Empty, $state, rbacUiControlService, Dataset) { - let list = InventoriesList, + let list = InventoryList, defaultUrl = GetBasePath('inventory'); init(); @@ -303,6 +303,6 @@ function InventoriesList($scope, $rootScope, $location, } export default ['$scope', '$rootScope', '$location', - '$compile', '$filter', 'Rest', 'InventoriesList', + '$compile', '$filter', 'Rest', 'InventoryList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', 'Dataset', InventoriesList ]; diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 1cc57ff9a1..794cf76a5e 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -10,8 +10,8 @@ import inventoryEdit from './edit/main'; import inventoryList from './list/main'; import { templateUrl } from '../shared/template-url/template-url.factory'; import { N_ } from '../i18n'; -import InventoriesList from './inventory.list'; -import InventoriesForm from './inventory.form'; +import InventoryList from './inventory.list'; +import InventoryForm from './inventory.form'; export default angular.module('inventory', [ host.name, @@ -19,8 +19,8 @@ angular.module('inventory', [ inventoryEdit.name, inventoryList.name ]) - .factory('InventoriesForm', InventoriesForm) - .factory('InventoriesList', InventoriesList) + .factory('InventoryForm', InventoryForm) + .factory('InventoryList', InventoryList) .config(['$stateProvider', 'stateDefinitionsProvider', function($stateProvider, stateDefinitionsProvider) { // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states @@ -33,8 +33,8 @@ angular.module('inventory', [ lazyLoad: () => stateDefinitions.generateTree({ parent: 'inventories', // top-most node in the generated tree (will replace this state definition) modes: ['add', 'edit'], - list: 'InventoriesList', - form: 'InventoriesForm', + list: 'InventoryList', + form: 'InventoryForm', controllers: { list: 'InventoryListController', add: 'InventoryAddController', @@ -51,9 +51,9 @@ angular.module('inventory', [ templateUrl: templateUrl('inventories/inventories') }, 'list@inventories': { - templateProvider: function(InventoriesList, generateList) { + templateProvider: function(InventoryList, generateList) { let html = generateList.build({ - list: InventoriesList, + list: InventoryList, mode: 'edit' }); return html; From 14ec5183d207e8fc7cc4b7ee7ad26929fffc57c3 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 6 Apr 2017 14:02:58 -0700 Subject: [PATCH 07/49] fixes to edit->host and host list removing the copy and delete icons from the top level host list, as well as fixing the edit host view which was slightly bjorked. --- .../hosts/edit/host-edit.controller.js | 77 +++++++++++++++++-- .../client/src/inventories/hosts/host.list.js | 15 ---- awx/ui/client/src/inventories/main.js | 11 +++ 3 files changed, 82 insertions(+), 21 deletions(-) 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 index d1d1092085..250c65e4d7 100644 --- a/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js +++ b/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js @@ -1,14 +1,79 @@ /************************************************* - * Copyright (c) 2017 Ansible, Inc. + * Copyright (c) 2016 Ansible, Inc. * * All Rights Reserved *************************************************/ -function HostsEdit($scope) { + 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.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}); + }); - console.log('inside host edit'); + }; + 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){ -export default ['$scope', HostsEdit -]; + // 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/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js index ec70bb2716..cc1a1aa613 100644 --- a/awx/ui/client/src/inventories/hosts/host.list.js +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -74,13 +74,6 @@ export default ['i18n', function(i18n) { fieldActions: { columnClass: 'col-lg-6 col-md-4 col-sm-4 col-xs-5 text-right', - copy: { - mode: 'all', - ngClick: "copyMoveHost(host.id)", - awToolTip: 'Copy or move host to another group', - dataPlacement: "top", - ngShow: 'host.summary_fields.user_capabilities.edit' - }, edit: { //label: 'Edit', ngClick: "editHost(host.id)", @@ -95,14 +88,6 @@ export default ['i18n', function(i18n) { awToolTip: 'View host', dataPlacement: 'top', ngShow: '!host.summary_fields.user_capabilities.edit' - }, - "delete": { - //label: 'Delete', - ngClick: "deleteHost(host.id, host.name)", - icon: 'icon-trash', - awToolTip: 'Delete host', - dataPlacement: 'top', - ngShow: 'host.summary_fields.user_capabilities.delete' } }, diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 794cf76a5e..5ff397b7fd 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -80,6 +80,17 @@ angular.module('inventory', [ urls: { list: '/hosts' }, + resolve: { + edit: { + host: ['Rest', '$stateParams', 'GetBasePath', + function(Rest, $stateParams, GetBasePath) { + let path = GetBasePath('hosts') + $stateParams.host_id; + Rest.setUrl(path); + return Rest.get(); + } + ] + } + }, ncyBreadcrumb: { label: N_('HOSTS') }, From 96c25fe464670a313af0b0ad7c1c87a5e136c603 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 6 Apr 2017 17:14:19 -0700 Subject: [PATCH 08/49] adding related tabs for inventories for groups, hosts, sources, and completed jobs. this is for the tabs themselves. --- .../client/src/inventories/inventory.form.js | 129 ++++++++++++++++++ .../src/shared/stateDefinitions.factory.js | 5 +- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 3164d47b5a..7e87e56dcf 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -129,6 +129,135 @@ export default ['i18n', function(i18n) { class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4', } } + }, + groups: { + name: 'groups', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', + type: 'collection', + title: i18n._('Groups'), + iterator: 'group', + 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' + } + } + }, + hosts: { + name: 'hosts', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', + type: 'collection', + title: i18n._('Hosts'), + iterator: 'host', + 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' + } + } + }, + inventory_sources: { + name: 'inventory_sources', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', + type: 'collection', + title: i18n._('Sources'), + iterator: 'inventory_source', + 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' + } + } + }, + //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/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 8560bb1fd0..b42aaebd91 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -483,9 +483,10 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto ListDefinition: () => { return list; }, - inventorySourceData: ['$stateParams', 'GroupManageService', function($stateParams, GroupManageService) { + inventorySourceData: ['$stateParams', //'GroupManageService', + function($stateParams){ //, GroupManageService) { if($stateParams.hasOwnProperty('group_id')){ - return GroupManageService.getInventorySource({ group: $stateParams.group_id }).then(res => res.data.results[0]); + return; //GroupManageService.getInventorySource({ group: $stateParams.group_id }).then(res => res.data.results[0]); } else{ return null; From 570b7fde8837ea3cc83438219f54a3723aab553a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 7 Apr 2017 11:22:39 -0400 Subject: [PATCH 09/49] Added smart inventory add scaffolding --- .../hosts/list/host-list.controller.js | 6 +- awx/ui/client/src/inventories/hosts/main.js | 4 +- .../hosts/smart-inventory/add/main.js | 11 + .../add/smart-inventory-add.controller.js | 104 ++++++++++ .../hosts/smart-inventory/edit/main.js | 11 + .../edit/smart-inventory-edit.controller.js | 14 ++ .../inventories/hosts/smart-inventory/main.js | 16 ++ .../smart-inventory/smart-inventory.form.js | 194 ++++++++++++++++++ awx/ui/client/src/inventories/main.js | 111 ++++++---- 9 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/add/main.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/edit/main.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/main.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.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 index 86d186ed74..eb3df91631 100644 --- a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -78,7 +78,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, }); $rootScope.promptActionBtnClass = 'Modal-errorButton'; }; - + $scope.toggleHost = function(event, host) { try { $(event.target).tooltip('hide'); @@ -93,6 +93,10 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, }); }; + $scope.smartInventory = function() { + $state.go('hosts.addSmartInventory'); + }; + } export default ['$scope', 'HostsList', '$rootScope', 'GetBasePath', diff --git a/awx/ui/client/src/inventories/hosts/main.js b/awx/ui/client/src/inventories/hosts/main.js index d6fca96d15..fe21c6a43e 100644 --- a/awx/ui/client/src/inventories/hosts/main.js +++ b/awx/ui/client/src/inventories/hosts/main.js @@ -12,12 +12,14 @@ import HostManageService from './hosts.service'; import SetStatus from './set-status.factory'; import SetEnabledMsg from './set-enabled-msg.factory'; + import SmartInventory from './smart-inventory/main'; export default angular.module('host', [ hostAdd.name, hostEdit.name, - hostList.name + hostList.name, + SmartInventory.name, ]) .factory('HostsForm', HostsForm) .factory('HostsList', HostsList) diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/add/main.js b/awx/ui/client/src/inventories/hosts/smart-inventory/add/main.js new file mode 100644 index 0000000000..911492557f --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/add/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './smart-inventory-add.controller'; + +export default +angular.module('smartInventoryAdd', []) + .controller('SmartInventoryAddController', controller); diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js new file mode 100644 index 0000000000..a97ea886d0 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js @@ -0,0 +1,104 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Inventories + * @description This controller's for the Inventory page + */ + +function SmartInventoryAdd($scope, $location, + GenerateForm, SmartInventoryForm, rbacUiControlService, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON, + $state) { + + $scope.canAdd = false; + rbacUiControlService.canAdd(GetBasePath('inventory')) + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + Rest.setUrl(GetBasePath('inventory')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add an inventory.', 'alert-info'); + } + }); + + ClearScope(); + + // Inject dynamic view + var defaultUrl = GetBasePath('inventory'), + form = SmartInventoryForm; + + init(); + + function init() { + $scope.canEditOrg = true; + form.formLabelSize = null; + form.formFieldSize = null; + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $scope.parseType = 'yaml'; + ParseTypeChange({ + scope: $scope, + variable: 'variables', + parse_variable: 'parseType', + field_id: 'inventory_variables' + }); + } + + // Save + $scope.formSave = function() { + Wait('start'); + try { + var fld, json_data, data; + + json_data = ToJSON($scope.parseType, $scope.variables, true); + + data = {}; + for (fld in form.fields) { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + + Rest.setUrl(defaultUrl); + Rest.post(data) + .success(function(data) { + var inventory_id = data.id; + Wait('stop'); + $location.path('/inventories/' + inventory_id); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new inventory. Post returned status: ' + status + }); + }); + } catch (err) { + Wait('stop'); + Alert("Error", "Error parsing inventory variables. Parser returned: " + err); + } + + }; + + $scope.formCancel = function() { + $state.go('hosts'); + }; +} + +export default ['$scope', '$location', + 'GenerateForm', 'SmartInventoryForm', 'rbacUiControlService', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', + 'Wait', 'ToJSON', '$state', SmartInventoryAdd +]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/main.js b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/main.js new file mode 100644 index 0000000000..b5c59c9e6d --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './smart-inventory-edit.controller'; + +export default +angular.module('smartInventoryEdit', []) + .controller('SmartInventoryEditController', controller); diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js new file mode 100644 index 0000000000..5cf8bf1612 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js @@ -0,0 +1,14 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +function SmartInventoryEdit($scope) { + +console.log('inside smart inventory add'); + +} + +export default ['$scope', SmartInventoryEdit +]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/main.js b/awx/ui/client/src/inventories/hosts/smart-inventory/main.js new file mode 100644 index 0000000000..7fa368df70 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/main.js @@ -0,0 +1,16 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import smartInventoryAdd from './add/main'; + import smartInventoryEdit from './edit/main'; + import SmartInventoryForm from './smart-inventory.form'; + +export default +angular.module('smartInventory', [ + smartInventoryAdd.name, + smartInventoryEdit.name + ]) + .factory('SmartInventoryForm', SmartInventoryForm); diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js new file mode 100644 index 0000000000..d467217078 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js @@ -0,0 +1,194 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['i18n', function(i18n) { + return { + + addTitle: i18n._('NEW SMART INVENTORY'), + editTitle: '{{ inventory_name }}', + name: 'inventory', + basePath: 'inventory', + // the top-most node of this generated state tree + stateTree: 'hosts', + tabs: true, + + fields: { + inventory_name: { + realName: 'name', + label: i18n._('Name'), + type: 'text', + required: true, + capitalize: false, + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + inventory_description: { + realName: 'description', + label: i18n._('Description'), + type: 'text', + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' + }, + organization: { + label: i18n._('Organization'), + type: 'lookup', + basePath: 'organizations', + list: 'OrganizationList', + sourceModel: 'organization', + sourceField: 'name', + awRequiredWhen: { + reqExpression: "organizationrequired", + init: "true" + }, + ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', + awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + class: 'Form-formGroup--fullWidth', + rows: 6, + "default": "---", + awPopOver: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

' + 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: { + 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', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', + type: 'collection', + title: i18n._('Hosts'), + iterator: 'host', + 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' + } + } + }, + //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/main.js b/awx/ui/client/src/inventories/main.js index 5ff397b7fd..3f71dee845 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -21,53 +21,36 @@ angular.module('inventory', [ ]) .factory('InventoryForm', InventoryForm) .factory('InventoryList', InventoryList) - .config(['$stateProvider', 'stateDefinitionsProvider', - function($stateProvider, stateDefinitionsProvider) { + .config(['$stateProvider', '$stateExtenderProvider', 'stateDefinitionsProvider', + function($stateProvider, $stateExtenderProvider, stateDefinitionsProvider) { // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree - let stateDefinitions = stateDefinitionsProvider.$get(); + let stateDefinitions = stateDefinitionsProvider.$get(), + stateExtender = $stateExtenderProvider.$get(); - $stateProvider.state({ - name: 'inventories', - url: '/inventories', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'inventories', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'InventoryList', - form: 'InventoryForm', - controllers: { - list: 'InventoryListController', - add: 'InventoryAddController', - edit: 'InventoryEditController' - }, - urls: { - list: '/inventories' - }, + function foobar() { + + let smartInventoryAdd = { + name: 'hosts.addSmartInventory', + url: '/smartinventory', + form: 'SmartInventoryForm', ncyBreadcrumb: { - label: N_('INVENTORIES') + label: "CREATE SMART INVENTORY" }, views: { - '@': { - templateUrl: templateUrl('inventories/inventories') - }, - 'list@inventories': { - templateProvider: function(InventoryList, generateList) { - let html = generateList.build({ - list: InventoryList, - mode: 'edit' + 'form@hosts': { + templateProvider: function(SmartInventoryForm, GenerateForm) { + return GenerateForm.buildHTML(SmartInventoryForm, { + mode: 'add', + related: false }); - return html; }, - controller: 'InventoryListController' + controller: 'SmartInventoryAddController' } } - }) - }); + }; - $stateProvider.state({ - name: 'hosts', - url: '/hosts', - lazyLoad: () => stateDefinitions.generateTree({ + let hosts = stateDefinitions.generateTree({ parent: 'hosts', // top-most node in the generated tree (will replace this state definition) modes: ['add', 'edit'], list: 'HostsList', @@ -109,7 +92,63 @@ angular.module('inventory', [ controller: 'HostListController' } } + }); + + return Promise.all([ + hosts + ]).then((generated) => { + return { + states: _.reduce(generated, (result, definition) => { + return result.concat(definition.states); + }, [ + stateExtender.buildDefinition(smartInventoryAdd) + ]) + }; + }); + + } + + $stateProvider.state({ + name: 'inventories', + url: '/inventories', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'inventories', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'InventoryList', + form: 'InventoryForm', + controllers: { + list: 'InventoryListController', + add: 'InventoryAddController', + edit: 'InventoryEditController' + }, + urls: { + list: '/inventories' + }, + ncyBreadcrumb: { + label: N_('INVENTORIES') + }, + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') + }, + 'list@inventories': { + templateProvider: function(InventoryList, generateList) { + let html = generateList.build({ + list: InventoryList, + mode: 'edit' + }); + return html; + }, + controller: 'InventoryListController' + } + } }) }); + + $stateProvider.state({ + name: 'hosts', + url: '/hosts', + lazyLoad: () => foobar() + }); } ]); From 8013f352ceff3f3886870591e71f7d94b4cde37d Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 7 Apr 2017 12:44:04 -0700 Subject: [PATCH 10/49] fixing linkage to user on inventories.permissions it was plain text instead of being a link to the user --- awx/ui/client/src/inventories/inventory.form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 7e87e56dcf..f0646c9a40 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -112,6 +112,7 @@ export default ['i18n', function(i18n) { }, fields: { username: { + key: true, label: i18n._('User'), linkBase: 'users', class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' From 2dd1901649bd794e20310c5c1305b7452c84fac4 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 10 Apr 2017 11:28:54 -0400 Subject: [PATCH 11/49] Set up dynamic inventory org lookup state --- .../add/smart-inventory-add.controller.js | 2 +- .../smart-inventory/smart-inventory.form.js | 117 +----------------- .../inventories/inventory-manage.service.js | 60 +++++++++ awx/ui/client/src/inventories/main.js | 70 +++++++++-- 4 files changed, 127 insertions(+), 122 deletions(-) create mode 100644 awx/ui/client/src/inventories/inventory-manage.service.js diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js index a97ea886d0..5ccac44ddc 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js @@ -51,7 +51,7 @@ function SmartInventoryAdd($scope, $location, scope: $scope, variable: 'variables', parse_variable: 'parseType', - field_id: 'inventory_variables' + field_id: 'smartinventory_variables' }); } diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js index d467217078..4ba3d7e6d7 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js @@ -9,11 +9,10 @@ export default ['i18n', function(i18n) { addTitle: i18n._('NEW SMART INVENTORY'), editTitle: '{{ inventory_name }}', - name: 'inventory', + name: 'smartinventory', basePath: 'inventory', - // the top-most node of this generated state tree + breadcrumbName: 'SMART INVENTORY', stateTree: 'hosts', - tabs: true, fields: { inventory_name: { @@ -79,116 +78,6 @@ export default ['i18n', function(i18n) { 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: { - 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', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', - type: 'collection', - title: i18n._('Hosts'), - iterator: 'host', - 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' - } - } - }, - //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' - } - } - } - } + related: {} };}]; diff --git a/awx/ui/client/src/inventories/inventory-manage.service.js b/awx/ui/client/src/inventories/inventory-manage.service.js new file mode 100644 index 0000000000..710c494aeb --- /dev/null +++ b/awx/ui/client/src/inventories/inventory-manage.service.js @@ -0,0 +1,60 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default + ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', + function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){ + return { + // 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; + }, + // data getters + getInventory: function(id){ + Wait('start'); + this.url = GetBasePath('inventory') + id; + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + getBreadcrumbs: function(groups){ + Wait('start'); + this.url = GetBasePath('groups') + '?' + _.map(groups, function(item){ + return '&or__id=' + item; + }).join(''); + Rest.setUrl(this.url); + return Rest.get() + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + rootHostsUrl: function(id){ + var url = GetBasePath('inventory') + id + '/hosts'; + return url; + }, + childHostsUrl: function(id){ + var url = GetBasePath('groups') + id + '/all_hosts'; + return url; + }, + childGroupsUrl: function(id){ + var url = GetBasePath('groups') + id + '/children'; + return url; + }, + rootGroupsUrl: function(id){ + var url = GetBasePath('inventory') + id+ '/root_groups'; + return url; + } + }; + }]; diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 3f71dee845..3ae1d9d7cf 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -12,6 +12,7 @@ import { templateUrl } from '../shared/template-url/template-url.factory'; import { N_ } from '../i18n'; import InventoryList from './inventory.list'; import InventoryForm from './inventory.form'; +import InventoryManageService from './inventory-manage.service'; export default angular.module('inventory', [ host.name, @@ -21,14 +22,15 @@ angular.module('inventory', [ ]) .factory('InventoryForm', InventoryForm) .factory('InventoryList', InventoryList) - .config(['$stateProvider', '$stateExtenderProvider', 'stateDefinitionsProvider', - function($stateProvider, $stateExtenderProvider, stateDefinitionsProvider) { + .service('InventoryManageService', InventoryManageService) + .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', + function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree let stateDefinitions = stateDefinitionsProvider.$get(), stateExtender = $stateExtenderProvider.$get(); - function foobar() { + function generateHostStates() { let smartInventoryAdd = { name: 'hosts.addSmartInventory', @@ -50,14 +52,67 @@ angular.module('inventory', [ } }; + let smartInventoryAddOrgLookup = { + searchPrefix: 'organization', + name: 'hosts.addSmartInventory.organization', + url: '/organization', + data: { + formChildState: true + }, + params: { + organization_search: { + value: { + page_size: '5' + }, + squash: true, + dynamic: true + } + }, + ncyBreadcrumb: { + skip: true + }, + views: { + 'related': { + templateProvider: function(ListDefinition, generateList) { + let list_html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + return `${list_html}`; + + } + } + }, + resolve: { + ListDefinition: ['OrganizationList', function(OrganizationList) { + let list = _.cloneDeep(OrganizationList); + list.lookupConfirmText = 'SELECT'; + return list; + }], + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', + (list, qs, $stateParams, GetBasePath) => { + let path = GetBasePath(list.name) || GetBasePath(list.basePath); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#form-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + }, + }; + let hosts = stateDefinitions.generateTree({ parent: 'hosts', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], + modes: ['edit'], list: 'HostsList', form: 'HostsForm', controllers: { list: 'HostListController', - add: 'HostAddController', edit: 'HostEditController' }, urls: { @@ -101,7 +156,8 @@ angular.module('inventory', [ states: _.reduce(generated, (result, definition) => { return result.concat(definition.states); }, [ - stateExtender.buildDefinition(smartInventoryAdd) + stateExtender.buildDefinition(smartInventoryAdd), + stateExtender.buildDefinition(smartInventoryAddOrgLookup) ]) }; }); @@ -148,7 +204,7 @@ angular.module('inventory', [ $stateProvider.state({ name: 'hosts', url: '/hosts', - lazyLoad: () => foobar() + lazyLoad: () => generateHostStates() }); } ]); From 3727c3a9a511ae5806e23abdfc5ce938cef41ac7 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 10 Apr 2017 12:08:48 -0400 Subject: [PATCH 12/49] Disable the SMART INVENTORY button until the user performs a search on the host list --- awx/ui/client/src/inventories/hosts/host.list.js | 3 ++- .../hosts/list/host-list.controller.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/hosts/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js index cc1a1aa613..ef6c19212e 100644 --- a/awx/ui/client/src/inventories/hosts/host.list.js +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -103,11 +103,12 @@ export default ['i18n', function(i18n) { smart_inventory: { mode: 'all', ngClick: "smartInventory()", - awToolTip: "Create a new Smart Inventory from results.", + awToolTip: "Create a new Smart Inventory from search results.", actionClass: 'btn List-buttonDefault', buttonContent: 'SMART INVENTORY', ngShow: 'canAdd', dataPlacement: "top", + ngDisabled: '!activeHostSearch' } } }; 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 index eb3df91631..398a18a549 100644 --- a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -15,6 +15,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, function init(){ $scope.canAdd = false; + $scope.activeHostSearch = false; rbacUiControlService.canAdd('hosts') .then(function(canAdd) { @@ -37,6 +38,21 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, setJobStatus(); }); + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + 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.activeHostSearch = hasMoreThanDefaultKeys ? true : false; + } + else { + $scope.activeHostSearch = false; + } + }); + } function setJobStatus(){ From f5141bcab902c24f4dd83ef7a9acaa63b761ed86 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Fri, 7 Apr 2017 17:03:25 -0700 Subject: [PATCH 13/49] Related Groups tab for inventories I've added a way to hookin a custom "related" state without needing to extend the state-generator directly. There is more work that needs to happen here but this gets the scaffolding in place for the related-groups tab. --- .../factories/get-hosts-status-msg.factory.js | 33 +++ .../get-source-type-options.factory.js | 37 +++ .../factories/get-sync-status-msg.factory.js | 77 ++++++ .../factories/groups-cancel-update.factory.js | 81 ++++++ .../factories/view-update-status.factory.js | 46 ++++ .../src/inventories/groups/groups.service.js | 113 +++++++++ .../list/build-groups-list-state.factory.js | 99 ++++++++ .../groups/list/group-list.controller.js | 236 ++++++++++++++++++ .../groups/list/groups-list.controller.js | 236 ++++++++++++++++++ .../groups/list/groups-list.partial.html | 79 ++++++ .../groups/list/inventory-groups.list.js | 165 ++++++++++++ .../src/inventories/groups/list/main.js | 15 ++ awx/ui/client/src/inventories/groups/main.js | 24 ++ .../hosts/add/host-add.controller.js | 4 +- .../edit/smart-inventory-edit.controller.js | 4 +- .../client/src/inventories/inventory.form.js | 36 +-- awx/ui/client/src/inventories/main.js | 7 +- .../list-generator/list-generator.factory.js | 4 +- .../src/shared/stateDefinitions.factory.js | 9 +- 19 files changed, 1267 insertions(+), 38 deletions(-) create mode 100644 awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js create mode 100644 awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js create mode 100644 awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js create mode 100644 awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js create mode 100644 awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js create mode 100644 awx/ui/client/src/inventories/groups/groups.service.js create mode 100644 awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js create mode 100644 awx/ui/client/src/inventories/groups/list/group-list.controller.js create mode 100644 awx/ui/client/src/inventories/groups/list/groups-list.controller.js create mode 100644 awx/ui/client/src/inventories/groups/list/groups-list.partial.html create mode 100644 awx/ui/client/src/inventories/groups/list/inventory-groups.list.js create mode 100644 awx/ui/client/src/inventories/groups/list/main.js create mode 100644 awx/ui/client/src/inventories/groups/main.js diff --git a/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js new file mode 100644 index 0000000000..19a846c414 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js @@ -0,0 +1,33 @@ +export default + function GetHostsStatusMsg() { + return function(params) { + var active_failures = params.active_failures, + total_hosts = params.total_hosts, + tip, failures, html_class; + + // Return values for use on host status indicator + + if (active_failures > 0) { + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; + html_class = 'error'; + failures = true; + } else { + failures = false; + if (total_hosts === 0) { + // no hosts + tip = "Contains 0 hosts."; + html_class = 'none'; + } else { + // many hosts with 0 failures + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; + html_class = 'success'; + } + } + + return { + tooltip: tip, + failures: failures, + 'class': html_class + }; + }; + } diff --git a/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js new file mode 100644 index 0000000000..befef8a499 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js @@ -0,0 +1,37 @@ +export default + function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + variable = params.variable; + + if (scope[variable] === undefined) { + scope[variable] = []; + Rest.setUrl(GetBasePath('inventory_sources')); + Rest.options() + .success(function (data) { + var i, choices = data.actions.GET.source.choices; + for (i = 0; i < choices.length; i++) { + if (choices[i][0] !== 'file') { + scope[variable].push({ + label: choices[i][1], + value: choices[i][0] + }); + } + } + scope.cloudCredentialRequired = false; + scope.$emit('sourceTypeOptionsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status + }); + }); + } + }; + } + +GetSourceTypeOptions.$inject = + [ 'Rest', + 'ProcessErrors', + 'GetBasePath' + ]; diff --git a/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js new file mode 100644 index 0000000000..2541abcc27 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js @@ -0,0 +1,77 @@ +export default + function GetSyncStatusMsg(Empty) { + 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', + stat, stat_class, status_tip; + + stat = status; + stat_class = stat; + + switch (status) { + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case 'ok': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'canceled': + status_tip = 'Sync canceled. Click to view log.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'pending': + status_tip = 'Sync pending.'; + launch_class = "btn-disabled"; + launch_tip = "Sync pending"; + break; + case 'updating': + case 'running': + launch_class = "btn-disabled"; + launch_tip = "Sync running"; + status_tip = "Sync running. Click to view log."; + 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, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip, + "schedule_tip": schedule_tip + }; + }; + } + +GetSyncStatusMsg.$inject = + [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js new file mode 100644 index 0000000000..1447d0aa1c --- /dev/null +++ b/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js @@ -0,0 +1,81 @@ +export default + function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + group = params.group; + + if (scope.removeCancelUpdate) { + scope.removeCancelUpdate(); + } + scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { + // 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 + }); + }); + }); + + if (scope.removeCheckCancel) { + scope.removeCheckCancel(); + } + scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { + // Check that we have access to cancelling an update + var url = (current_update) ? current_update : last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + scope.$emit('CancelUpdate', url); + //} else { + // Wait('stop'); + // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + + // 'the latest status.', 'alert-info'); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }); + + // Cancel the update process + if (Empty(group)) { + group = Find({ list: scope.groups, key: 'id', val: id }); + scope.selected_group_id = group.id; + } + + if (group && (group.status === 'running' || group.status === 'pending')) { + // We found the group, and there is a running update + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status + }); + }); + } + }; + } + +GroupsCancelUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js new file mode 100644 index 0000000000..1f3280b51c --- /dev/null +++ b/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js @@ -0,0 +1,46 @@ +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 }); + + 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") { + Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + + 'clicking the button.
', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('SourceReady', data); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' GET returned status: ' + status }); + }); + } + } + }; + } + +ViewUpdateStatus.$inject = + [ '$state', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Empty', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/groups/groups.service.js b/awx/ui/client/src/inventories/groups/groups.service.js new file mode 100644 index 0000000000..54ed90dfc7 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/groups.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('groups') + '?' + 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(group){ + Wait('start'); + this.url = GetBasePath('groups'); + Rest.setUrl(this.url); + return Rest.post(group) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + put: function(group){ + Wait('start'); + this.url = GetBasePath('groups') + group.id; + Rest.setUrl(this.url); + return Rest.put(group) + .success(this.success.bind(this)) + .error(this.error.bind(this)) + .finally(Wait('stop')); + }, + delete: function(id){ + Wait('start'); + this.url = GetBasePath('groups') + 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/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..f1af8925fd --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js @@ -0,0 +1,99 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$injector', + function(InventoryGroupsList, $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 = { + 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(InventoryGroupsList, generateList, $templateRequest, $stateParams, GetBasePath) { + let list = _.cloneDeep(InventoryGroupsList); + 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); + }] + } + }; + + if(params.controllers && params.controllers.related && params.controllers.related[field.name]) { + stateConfig.views.related.controller = params.controllers.related[field.name]; + } + else if(field.name === 'permissions') { + stateConfig.views.related.controller = 'PermissionsList'; + } + else { + // Generic controller + stateConfig.views.related.controller = ['$scope', 'ListDefinition', 'Dataset', + function($scope, list, Dataset) { + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[`${list.iterator}s`] = $scope[`${list.iterator}_dataset`].results; + } + ]; + } + + 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/group-list.controller.js b/awx/ui/client/src/inventories/groups/list/group-list.controller.js new file mode 100644 index 0000000000..8bc2bb8154 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/group-list.controller.js @@ -0,0 +1,236 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate', + 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate, + GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, + GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + + let list = InventoryGroupsList; + + 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 === "inventoryManage.editGroup") { + $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 group_status, hosts_status; + + group_status = GetSyncStatusMsg({ + status: group.summary_fields.inventory_source.status, + has_inventory_sources: group.has_inventory_sources, + source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) + }); + 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, + {status_class: group_status.class}, + {status_tooltip: group_status.tooltip}, + {launch_tooltip: group_status.launch_tip}, + {launch_class: group_status.launch_class}, + {group_schedule_tooltip: group_status.schedule_tip}, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}, + {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, + {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null}); + } + + $scope.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $scope.createGroup = function(){ + $state.go('inventoryManage.addGroup'); + }; + $scope.editGroup = function(id){ + $state.go('inventoryManage.editGroup', {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('inventoryManage', 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("inventoryManage", 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("inventoryManage", 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.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + 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; + _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $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(); + }); + + }]; 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..8bc2bb8154 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -0,0 +1,236 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate', + 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate, + GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, + GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + + let list = InventoryGroupsList; + + 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 === "inventoryManage.editGroup") { + $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 group_status, hosts_status; + + group_status = GetSyncStatusMsg({ + status: group.summary_fields.inventory_source.status, + has_inventory_sources: group.has_inventory_sources, + source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) + }); + 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, + {status_class: group_status.class}, + {status_tooltip: group_status.tooltip}, + {launch_tooltip: group_status.launch_tip}, + {launch_class: group_status.launch_class}, + {group_schedule_tooltip: group_status.schedule_tip}, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}, + {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, + {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null}); + } + + $scope.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $scope.createGroup = function(){ + $state.go('inventoryManage.addGroup'); + }; + $scope.editGroup = function(id){ + $state.go('inventoryManage.editGroup', {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('inventoryManage', 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("inventoryManage", 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("inventoryManage", 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.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + 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; + _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $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(); + }); + + }]; diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.partial.html b/awx/ui/client/src/inventories/groups/list/groups-list.partial.html new file mode 100644 index 0000000000..1a02f3a515 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/groups-list.partial.html @@ -0,0 +1,79 @@ + diff --git a/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js b/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js new file mode 100644 index 0000000000..0536161ab9 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js @@ -0,0 +1,165 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'groups', + iterator: 'group', + editTitle: '{{ inventory.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + 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, + mode: 'all', + 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)", + 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'}}" + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + 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", + 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(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(group.id)", + awToolTip: 'Edit group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(group.id)", + awToolTip: 'View group', + dataPlacement: "top", + ngShow: "!group.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteGroup(group)", + awToolTip: 'Delete group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.delete" + } + } +}; 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..b36162d4be --- /dev/null +++ b/awx/ui/client/src/inventories/groups/list/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildGroupListState from './build-groups-list-state.factory'; +import controller from './groups-list.controller'; +import InventoryGroupsList from './inventory-groups.list'; + +export default + angular.module('groupList', []) + .factory('buildGroupListState', buildGroupListState) + .value('InventoryGroupsList', InventoryGroupsList) + .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..d8af95c6f2 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/main.js @@ -0,0 +1,24 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import groupList from './list/main'; +import service from './groups.service'; +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'; + +export default + angular.module('group', [ + groupList.name + ]) + .factory('GetHostsStatusMsg', GetHostsStatusMsg) + .factory('GetSourceTypeOptions', GetSourceTypeOptions) + .factory('GetSyncStatusMsg', GetSyncStatusMsg) + .factory('GroupsCancelUpdate', GroupsCancelUpdate) + .factory('ViewUpdateStatus', ViewUpdateStatus) + .service('GroupManageService', service); diff --git a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js index 151d75401d..8e5b36ee7d 100644 --- a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js +++ b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js @@ -4,11 +4,11 @@ * All Rights Reserved *************************************************/ -function HostsAdd($scope) { +function HostsAdd() { console.log('inside host add'); } -export default ['$scope', HostsAdd +export default [ HostsAdd ]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js index 5cf8bf1612..26d10c9234 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js @@ -4,11 +4,11 @@ * All Rights Reserved *************************************************/ -function SmartInventoryEdit($scope) { +function SmartInventoryEdit() { console.log('inside smart inventory add'); } -export default ['$scope', SmartInventoryEdit +export default [ SmartInventoryEdit ]; diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index f0646c9a40..91c181a006 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -1,5 +1,5 @@ /************************************************* - * Copyright (c) 2015 Ansible, Inc. + * Copyright (c) 2017 Ansible, Inc. * * All Rights Reserved *************************************************/ @@ -10,8 +10,9 @@ * @description This form is for adding/editing an inventory */ -export default ['i18n', function(i18n) { - return { +export default ['i18n', 'buildGroupListState', +function(i18n,buildGroupListState) { + return { addTitle: i18n._('NEW INVENTORY'), editTitle: '{{ inventory_name }}', @@ -133,35 +134,10 @@ export default ['i18n', function(i18n) { }, groups: { name: 'groups', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/', - type: 'collection', + include: "InventoryGroupsList", title: i18n._('Groups'), iterator: 'group', - 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' - } - } + stateGeneratorFunction: buildGroupListState }, hosts: { name: 'hosts', diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 3ae1d9d7cf..be59ca6b4e 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -5,6 +5,7 @@ *************************************************/ import host from './hosts/main'; +import group from './groups/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; @@ -16,6 +17,7 @@ import InventoryManageService from './inventory-manage.service'; export default angular.module('inventory', [ host.name, + group.name, inventoryAdd.name, inventoryEdit.name, inventoryList.name @@ -175,7 +177,10 @@ angular.module('inventory', [ controllers: { list: 'InventoryListController', add: 'InventoryAddController', - edit: 'InventoryEditController' + edit: 'InventoryEditController', + related: { + groups: 'GroupsListController' + } }, urls: { list: '/inventories' diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 5f136d7a97..e986c685e4 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -170,7 +170,9 @@ export default ['$compile', 'Attr', 'Icon', } if (options.mode !== 'lookup' && (list.well === undefined || list.well)) { - html += `
`; + html += `
`; + html += (!list.wellOverride) ? "List-well" : ""; + html += `">`; // List actions html += "
"; html += "
"; diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index b42aaebd91..6e3a6ef426 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -9,7 +9,8 @@ * generateLookupNodes - Attaches to a form node. Builds an abstract '*.lookup' node with field-specific 'lookup.*' children e.g. {name: 'projects.add.lookup.organizations', ...} */ -export default ['$injector', '$stateExtender', '$log', 'i18n', function($injector, $stateExtender, $log, i18n) { +export default ['$injector', '$stateExtender', '$log', 'i18n', +function($injector, $stateExtender, $log, i18n) { return { /** * @ngdoc method @@ -557,7 +558,11 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto function buildListNodes(field) { let states = []; - if(field.iterator === 'notification'){ + if(field.iterator === 'group'){ + states.push(field.stateGeneratorFunction(field, formStateDefinition, params)); + states = _.flatten(states); + } + else if(field.iterator === 'notification'){ states.push(buildNotificationState(field)); states = _.flatten(states); } From a7dbd0019227d3f380686c9ed6994aa37e1fda9f Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 11 Apr 2017 11:35:50 -0700 Subject: [PATCH 14/49] fixing small issue i created with list-generator classes --- .../client/src/shared/list-generator/list-generator.factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index e986c685e4..14797c26b3 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -170,7 +170,7 @@ export default ['$compile', 'Attr', 'Icon', } if (options.mode !== 'lookup' && (list.well === undefined || list.well)) { - html += `
`; + html += `
`; html += (!list.wellOverride) ? "List-well" : ""; html += `">`; // List actions From c1ad7d69fd0b48dee69c9ebbcce06ec47971805b Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 12 Apr 2017 14:24:58 -0400 Subject: [PATCH 15/49] Moving some states around based on new smart inventory flow --- awx/ui/client/legacy-styles/lists.less | 8 +- .../client/src/inventories/hosts/host.list.js | 3 +- .../hosts/list/host-list.controller.js | 27 ++-- .../smart-inventory/smart-inventory.form.js | 113 +++++++++++++++- .../client/src/inventories/inventory.list.js | 26 +++- .../list/inventory-list.controller.js | 4 +- awx/ui/client/src/inventories/main.js | 124 +++++++++--------- .../list-generator/list-generator.factory.js | 4 +- 8 files changed, 227 insertions(+), 82 deletions(-) diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 7e36f610cc..5580df4119 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 { diff --git a/awx/ui/client/src/inventories/hosts/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js index ef6c19212e..34d5056d42 100644 --- a/awx/ui/client/src/inventories/hosts/host.list.js +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -19,6 +19,7 @@ export default ['i18n', function(i18n) { trackBy: 'host.id', basePath: 'hosts', title: false, + actionHolderClass: 'List-actionHolder List-actionHolder--leftAlign', fields: { toggleHost: { @@ -108,7 +109,7 @@ export default ['i18n', function(i18n) { buttonContent: 'SMART INVENTORY', ngShow: 'canAdd', dataPlacement: "top", - ngDisabled: '!activeHostSearch' + ngDisabled: '!enableSmartInventoryButton' } } }; 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 index 398a18a549..4f0b6f0b6a 100644 --- a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -15,7 +15,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, function init(){ $scope.canAdd = false; - $scope.activeHostSearch = false; + $scope.enableSmartInventoryButton = false; rbacUiControlService.canAdd('hosts') .then(function(canAdd) { @@ -39,17 +39,22 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, }); $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - 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.activeHostSearch = hasMoreThanDefaultKeys ? true : false; + if(toState.name === 'hosts.addSmartInventory') { + $scope.enableSmartInventoryButton = false; } else { - $scope.activeHostSearch = false; + 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; + } } }); @@ -110,7 +115,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, }; $scope.smartInventory = function() { - $state.go('hosts.addSmartInventory'); + $state.go('inventories.addSmartInventory'); }; } diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js index 4ba3d7e6d7..9404bb575c 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js @@ -78,6 +78,117 @@ export default ['i18n', function(i18n) { ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' } }, - related: {} + 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', + // awToolTip: i18n._('Please save before assigning permissions'), + // dataPlacement: 'top', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', + type: 'collection', + title: i18n._('Hosts'), + iterator: 'host', + 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' + } + } + }, + //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/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index 38c6f91874..fb6bac2a68 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -63,11 +63,31 @@ export default ['i18n', function(i18n) { actions: { add: { mode: 'all', // One of: edit, select, all - ngClick: 'addInventory()', + type: 'buttonDropdown', + basePaths: ['inventories'], awToolTip: i18n._('Create a new inventory'), - actionClass: 'btn List-buttonSubmit', + actionClass: 'btn List-dropdownSuccess', buttonContent: '+ ' + i18n._('ADD'), - ngShow: 'canAdd' + options: [ + { + optionContent: i18n._('Inventory'), + optionSref: 'inventories.add', + ngShow: 'canAddInventory' + }, + { + optionContent: i18n._('Smart Inventory'), + optionSref: 'inventories.addSmartInventory', + //TODO: this should have its own permission + ngShow: 'canAddInventory' + }, + { + optionContent: i18n._('SCM Inventory'), + optionSref: 'inventories.addSCMInventory', + //TODO: this should have its own permission + ngShow: 'canAddInventory' + } + ], + ngShow: 'canAddInventory || canAddSmartInventory || canAddSCMInventory' } }, diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 2f43f48100..0063b94fa4 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -23,8 +23,8 @@ function InventoriesList($scope, $rootScope, $location, $scope.canAdd = false; rbacUiControlService.canAdd('inventory') - .then(function(params) { - $scope.canAdd = params.canAdd; + .then(function(canAdd) { + $scope.canAddInventory = canAdd; }); $scope.$watchCollection(list.name, function(){ diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index be59ca6b4e..4f0f3b44fd 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -32,17 +32,17 @@ angular.module('inventory', [ let stateDefinitions = stateDefinitionsProvider.$get(), stateExtender = $stateExtenderProvider.$get(); - function generateHostStates() { + function generateInventoryStates() { let smartInventoryAdd = { - name: 'hosts.addSmartInventory', + name: 'inventories.addSmartInventory', url: '/smartinventory', form: 'SmartInventoryForm', ncyBreadcrumb: { label: "CREATE SMART INVENTORY" }, views: { - 'form@hosts': { + 'form@inventories': { templateProvider: function(SmartInventoryForm, GenerateForm) { return GenerateForm.buildHTML(SmartInventoryForm, { mode: 'add', @@ -56,7 +56,7 @@ angular.module('inventory', [ let smartInventoryAddOrgLookup = { searchPrefix: 'organization', - name: 'hosts.addSmartInventory.organization', + name: 'inventories.addSmartInventory.organization', url: '/organization', data: { formChildState: true @@ -108,7 +108,61 @@ angular.module('inventory', [ }, }; - let hosts = stateDefinitions.generateTree({ + let inventories = stateDefinitions.generateTree({ + parent: 'inventories', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'InventoryList', + form: 'InventoryForm', + controllers: { + list: 'InventoryListController', + add: 'InventoryAddController', + edit: 'InventoryEditController', + related: { + groups: 'GroupsListController' + } + }, + urls: { + list: '/inventories' + }, + ncyBreadcrumb: { + label: N_('INVENTORIES') + }, + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') + }, + 'list@inventories': { + templateProvider: function(InventoryList, generateList) { + let html = generateList.build({ + list: InventoryList, + mode: 'edit' + }); + return html; + }, + controller: 'InventoryListController' + } + } + }); + + return Promise.all([ + inventories + ]).then((generated) => { + return { + states: _.reduce(generated, (result, definition) => { + return result.concat(definition.states); + }, [ + stateExtender.buildDefinition(smartInventoryAdd), + stateExtender.buildDefinition(smartInventoryAddOrgLookup) + ]) + }; + }); + + } + + $stateProvider.state({ + name: 'hosts', + url: '/hosts', + lazyLoad: () => stateDefinitions.generateTree({ parent: 'hosts', // top-most node in the generated tree (will replace this state definition) modes: ['edit'], list: 'HostsList', @@ -149,67 +203,13 @@ angular.module('inventory', [ controller: 'HostListController' } } - }); - - return Promise.all([ - hosts - ]).then((generated) => { - return { - states: _.reduce(generated, (result, definition) => { - return result.concat(definition.states); - }, [ - stateExtender.buildDefinition(smartInventoryAdd), - stateExtender.buildDefinition(smartInventoryAddOrgLookup) - ]) - }; - }); - - } - - $stateProvider.state({ - name: 'inventories', - url: '/inventories', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'inventories', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'InventoryList', - form: 'InventoryForm', - controllers: { - list: 'InventoryListController', - add: 'InventoryAddController', - edit: 'InventoryEditController', - related: { - groups: 'GroupsListController' - } - }, - urls: { - list: '/inventories' - }, - ncyBreadcrumb: { - label: N_('INVENTORIES') - }, - views: { - '@': { - templateUrl: templateUrl('inventories/inventories') - }, - 'list@inventories': { - templateProvider: function(InventoryList, generateList) { - let html = generateList.build({ - list: InventoryList, - mode: 'edit' - }); - return html; - }, - controller: 'InventoryListController' - } - } }) }); $stateProvider.state({ - name: 'hosts', - url: '/hosts', - lazyLoad: () => generateHostStates() + name: 'inventories', + url: '/inventories', + lazyLoad: () => generateInventoryStates() }); } ]); diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 14797c26b3..ac2802d6f9 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -174,7 +174,9 @@ export default ['$compile', 'Attr', 'Icon', html += (!list.wellOverride) ? "List-well" : ""; html += `">`; // List actions - html += "
"; + html += "
"; html += "
"; html += `
`; From b85614082e125ac6ad059cabff8c987913043549 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 12 Apr 2017 11:25:47 -0700 Subject: [PATCH 16/49] Adding group add and group edit routes back in --- .../add/build-groups-add-state.factory.js | 46 +++ .../groups/add/groups-add.controller.js | 225 ++++++++++++ .../client/src/inventories/groups/add/main.js | 13 + .../groups/edit/groups-edit.controller.js | 247 +++++++++++++ .../src/inventories/groups/edit/main.js | 13 + .../src/inventories/groups/groups.form.js | 339 ++++++++++++++++++ ...nventory-groups.list.js => groups.list.js} | 0 .../list/build-groups-list-state.factory.js | 31 +- .../groups/list/group-list.controller.js | 236 ------------ .../groups/list/groups-list.controller.js | 10 +- .../src/inventories/groups/list/main.js | 8 +- awx/ui/client/src/inventories/groups/main.js | 10 +- .../src/inventories/inventories.partial.html | 1 + .../client/src/inventories/inventory.form.js | 10 +- .../src/shared/stateDefinitions.factory.js | 9 +- 15 files changed, 917 insertions(+), 281 deletions(-) create mode 100644 awx/ui/client/src/inventories/groups/add/build-groups-add-state.factory.js create mode 100644 awx/ui/client/src/inventories/groups/add/groups-add.controller.js create mode 100644 awx/ui/client/src/inventories/groups/add/main.js create mode 100644 awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js create mode 100644 awx/ui/client/src/inventories/groups/edit/main.js create mode 100644 awx/ui/client/src/inventories/groups/groups.form.js rename awx/ui/client/src/inventories/groups/{list/inventory-groups.list.js => groups.list.js} (100%) delete mode 100644 awx/ui/client/src/inventories/groups/list/group-list.controller.js 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..818e1890d2 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -0,0 +1,225 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'GroupForm', + 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'rbacUiControlService', 'ToJSON', + function($state, $stateParams, $scope, GroupForm, ParseTypeChange, + GenerateForm, inventoryData, GroupManageService, GetChoices, + GetBasePath, CreateSelect2, GetSourceTypeOptions, 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', + }); + 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, source, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + // group fields + var group = { + variables: json_data, + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id + }; + if ($scope.source) { + // inventory_source fields + params = { + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], + source_script: $scope.inventory_script, + source: $scope.source.value, + 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, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(',') + }; + source = $scope.source.value; + } else { + source = null; + } + switch (source) { + // no inventory source set, just create a new group + // '' is the value supplied for Manual source type + case null || '': + GroupManageService.post(group).then(res => { + // associate + if ($stateParams.group) { + return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + .then(() => $state.go('^', null, { reload: true })); + } else { + $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.post(group) + // associate to group + .then(res => { + if ($stateParams.group) { + GroupManageService.associateGroup(res.data, _.last($stateParams.group)); + return res; + } else { + return res; } + // pass the original POST response and not the association response + }) + .then(res => GroupManageService.putInventorySource( + // put the received group ID into inventory source payload + // and pass the related endpoint + _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) + .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); + break; + } + }; + $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: '#group_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#group_group_by', + multiple: true + }); + } + function initSourceSelect(){ + CreateSelect2({ + element: '#group_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/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/groups-edit.controller.js b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js new file mode 100644 index 0000000000..1355d5d33f --- /dev/null +++ b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js @@ -0,0 +1,247 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', + function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON, + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData) { + + init(); + + function init() { + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + // instantiate expected $scope values from inventorySourceData & groupData + _.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; + } + $scope = angular.extend($scope, groupData); + + // display custom inventory_script name + if (inventorySourceData.source === 'custom') { + $scope.inventory_script_name = inventorySourceData.summary_fields.source_script.name; + } + + $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', + }); + + initSources(); + } + + var initRegionSelect = function() { + CreateSelect2({ + element: '#group_source_regions', + multiple: true + }); + CreateSelect2({ + element: '#group_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, source, 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 + }; + if ($scope.source) { + // inventory_source fields + params = { + group: groupData.id, + source: $scope.source.value, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + source_script: $scope.inventory_script, + update_on_launch: $scope.update_on_launch, + update_cache_timeout: $scope.update_cache_timeout || 0, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(','), + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] + }; + source = $scope.source.value; + } else { + source = null; + } + switch (source) { + // no inventory source set, just create a new group + // '' is the value supplied for Manual source type + case null || '': + GroupManageService.put(group).then(() => $state.go($state.current, 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: '#group_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/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/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js new file mode 100644 index 0000000000..bdf9df2353 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -0,0 +1,339 @@ +/************************************************* + * 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 { + + + 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.editGroup', + detailsClick: "$state.go('inventories.edit.groups.editGroup')", + well: false, + 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" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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:
\n" + + "
tag-key=TowerManaged
\n" + + "Limit to hosts using either key pair:
\n" + + "
key-name=staging, key-name=production
\n" + + "Limit to hosts where the Name tag begins with test:
\n" + + "
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:

    " + + "
  • Availability Zone: zones » us-east-1b
  • " + + "
  • Image ID: images » ami-b007ab1e
  • " + + "
  • Instance ID: instances » i-ca11ab1e
  • " + + "
  • Instance Type: types » type_m1_medium
  • " + + "
  • Key Name: keys » key_testing
  • " + + "
  • Region: regions » us-east-1
  • " + + "
  • Security Group: security_groups » security_group_default
  • " + + "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + + "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + + "
  • Tag None: tags » tag_none
  • " + + "

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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/groups/list/inventory-groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js similarity index 100% rename from awx/ui/client/src/inventories/groups/list/inventory-groups.list.js rename to awx/ui/client/src/inventories/groups/groups.list.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 index f1af8925fd..db175bf3eb 100644 --- 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 @@ -3,10 +3,10 @@ * * All Rights Reserved *************************************************/ - -export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$injector', - function(InventoryGroupsList, $stateExtender, templateUrl, $injector){ - var val = function(field, formStateDefinition, params) { +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(), @@ -25,8 +25,8 @@ export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$inject }, views: { 'related': { - templateProvider: function(InventoryGroupsList, generateList, $templateRequest, $stateParams, GetBasePath) { - let list = _.cloneDeep(InventoryGroupsList); + templateProvider: function(GroupList, generateList, $templateRequest, $stateParams, GetBasePath) { + let list = _.cloneDeep(GroupList); if($stateParams && $stateParams.group) { list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/children'; } @@ -44,7 +44,7 @@ export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$inject return html.concat(template); }); }, - // controller: GroupsListController + controller: GroupsListController } }, resolve: { @@ -70,23 +70,6 @@ export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$inject } }; - if(params.controllers && params.controllers.related && params.controllers.related[field.name]) { - stateConfig.views.related.controller = params.controllers.related[field.name]; - } - else if(field.name === 'permissions') { - stateConfig.views.related.controller = 'PermissionsList'; - } - else { - // Generic controller - stateConfig.views.related.controller = ['$scope', 'ListDefinition', 'Dataset', - function($scope, list, Dataset) { - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[`${list.iterator}s`] = $scope[`${list.iterator}_dataset`].results; - } - ]; - } - state = $stateExtender.buildDefinition(stateConfig); // appy any default search parameters in form definition if (field.search) { diff --git a/awx/ui/client/src/inventories/groups/list/group-list.controller.js b/awx/ui/client/src/inventories/groups/list/group-list.controller.js deleted file mode 100644 index 8bc2bb8154..0000000000 --- a/awx/ui/client/src/inventories/groups/list/group-list.controller.js +++ /dev/null @@ -1,236 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - export default - ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate', - 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', - 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', - function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate, - GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, - GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ - - let list = InventoryGroupsList; - - 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 === "inventoryManage.editGroup") { - $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 group_status, hosts_status; - - group_status = GetSyncStatusMsg({ - status: group.summary_fields.inventory_source.status, - has_inventory_sources: group.has_inventory_sources, - source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) - }); - 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, - {status_class: group_status.class}, - {status_tooltip: group_status.tooltip}, - {launch_tooltip: group_status.launch_tip}, - {launch_class: group_status.launch_class}, - {group_schedule_tooltip: group_status.schedule_tip}, - {hosts_status_tip: hosts_status.tooltip}, - {hosts_status_class: hosts_status.class}, - {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, - {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null}); - } - - $scope.groupSelect = function(id){ - var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); - $state.go('inventoryManage', { - inventory_id: $stateParams.inventory_id, - group: group, - group_search: { - page_size: '20', - page: '1', - order_by: 'name', - } - }, {reload: true}); - }; - $scope.createGroup = function(){ - $state.go('inventoryManage.addGroup'); - }; - $scope.editGroup = function(id){ - $state.go('inventoryManage.editGroup', {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('inventoryManage', 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("inventoryManage", 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("inventoryManage", 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.$on(`ws-jobs`, function(e, data){ - var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); - - if (group === undefined || group === null) { - group = {}; - } - - if(data.status === 'failed' || data.status === 'successful'){ - let path; - if($stateParams && $stateParams.group && $stateParams.group.length > 0) { - path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; - } - else { - //reaches here if the user is on the root level group - path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; - } - 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; - _.forEach($scope[list.name], buildStatusIndicators); - }); - } else { - var status = GetSyncStatusMsg({ - status: data.status, - has_inventory_sources: group.has_inventory_sources, - source: group.source - }); - group.status = data.status; - group.status_class = status.class; - group.status_tooltip = status.tooltip; - group.launch_tooltip = status.launch_tip; - group.launch_class = status.launch_class; - } - }); - - $scope.cancelUpdate = function (id) { - GroupsCancelUpdate({ scope: $scope, id: id }); - }; - $scope.viewUpdateStatus = function (id) { - ViewUpdateStatus({ - scope: $scope, - group_id: id - }); - }; - $scope.showFailedHosts = function() { - $state.go('inventoryManage', {failed: true}, {reload: true}); - }; - $scope.scheduleGroup = function(id) { - // Add this group's id to the array of group id's so that it gets - // added to the breadcrumb trail - var groupsArr = $stateParams.group ? $stateParams.group : []; - groupsArr.push(id); - $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); - }; - // $scope.$parent governed by InventoryManageController, for unified multiSelect options - $scope.$on('multiSelectList.selectionChanged', (event, selection) => { - $scope.$parent.groupsSelected = selection.length > 0 ? true : false; - $scope.$parent.groupsSelectedItems = selection.selectedItems; - }); - - $scope.copyMoveGroup = function(id){ - $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); - }; - - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "inventoryManage.editGroup") { - $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(); - }); - - }]; diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 8bc2bb8154..a9db830841 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -4,14 +4,14 @@ * All Rights Reserved *************************************************/ export default - ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate', + ['$scope', '$rootScope', '$state', '$stateParams', 'GroupList', 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', - function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate, + function($scope, $rootScope, $state, $stateParams, GroupList, InventoryUpdate, GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ - let list = InventoryGroupsList; + let list = GroupList; init(); @@ -88,10 +88,10 @@ }, {reload: true}); }; $scope.createGroup = function(){ - $state.go('inventoryManage.addGroup'); + $state.go('inventories.edit.groups.add'); }; $scope.editGroup = function(id){ - $state.go('inventoryManage.editGroup', {group_id: id}); + $state.go('inventories.edit.groups.edit', {group_id: id}); }; $scope.deleteGroup = function(group){ $scope.toDelete = {}; diff --git a/awx/ui/client/src/inventories/groups/list/main.js b/awx/ui/client/src/inventories/groups/list/main.js index b36162d4be..37e4c974fa 100644 --- a/awx/ui/client/src/inventories/groups/list/main.js +++ b/awx/ui/client/src/inventories/groups/list/main.js @@ -4,12 +4,10 @@ * All Rights Reserved *************************************************/ -import buildGroupListState from './build-groups-list-state.factory'; +import buildGroupsListState from './build-groups-list-state.factory'; import controller from './groups-list.controller'; -import InventoryGroupsList from './inventory-groups.list'; export default - angular.module('groupList', []) - .factory('buildGroupListState', buildGroupListState) - .value('InventoryGroupsList', InventoryGroupsList) + 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 index d8af95c6f2..9461571a71 100644 --- a/awx/ui/client/src/inventories/groups/main.js +++ b/awx/ui/client/src/inventories/groups/main.js @@ -5,6 +5,10 @@ *************************************************/ import groupList from './list/main'; +import groupAdd from './add/main'; +import groupEdit from './edit/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'; import GetSourceTypeOptions from './factories/get-source-type-options.factory'; @@ -14,8 +18,12 @@ import ViewUpdateStatus from './factories/view-update-status.factory'; export default angular.module('group', [ - groupList.name + groupList.name, + groupAdd.name, + groupEdit.name ]) + .value('GroupForm', groupFormDefinition) + .value('GroupList', groupListDefinition) .factory('GetHostsStatusMsg', GetHostsStatusMsg) .factory('GetSourceTypeOptions', GetSourceTypeOptions) .factory('GetSyncStatusMsg', GetSyncStatusMsg) diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 871367dcb9..26d66de7af 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,4 +1,5 @@
+
diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 91c181a006..6755e72381 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -10,8 +10,8 @@ * @description This form is for adding/editing an inventory */ -export default ['i18n', 'buildGroupListState', -function(i18n,buildGroupListState) { +export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', +function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState) { return { addTitle: i18n._('NEW INVENTORY'), @@ -134,10 +134,12 @@ function(i18n,buildGroupListState) { }, groups: { name: 'groups', - include: "InventoryGroupsList", + include: "GroupList", title: i18n._('Groups'), iterator: 'group', - stateGeneratorFunction: buildGroupListState + listState: buildGroupsListState, + addState: buildGroupsAddState, + editState: buildGroupsEditState }, hosts: { name: 'hosts', diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 6e3a6ef426..107ea95b4e 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -246,7 +246,6 @@ function($injector, $stateExtender, $log, i18n) { * @returns {array} Array of state definitions [{...}, {...}, ...] */ generateFormListDefinitions: function(form, formStateDefinition, params) { - function buildRbacUserTeamDirective(){ let states = []; @@ -508,10 +507,6 @@ function($injector, $stateExtender, $log, i18n) { ] } }); - // // 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; } @@ -559,7 +554,9 @@ function($injector, $stateExtender, $log, i18n) { function buildListNodes(field) { let states = []; if(field.iterator === 'group'){ - states.push(field.stateGeneratorFunction(field, formStateDefinition, params)); + states.push(field.listState(field, formStateDefinition)); + states.push(field.addState(field, formStateDefinition, params)); + states.push(field.editState(field, formStateDefinition, params)); states = _.flatten(states); } else if(field.iterator === 'notification'){ From 09f99b04f255cb00b376297983ab649e53b1c8a5 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 12 Apr 2017 17:20:19 -0700 Subject: [PATCH 17/49] adding inventories related-host state --- .../client/src/inventories/inventory.form.js | 37 ++---- awx/ui/client/src/inventories/main.js | 7 +- .../related-hosts/add/host-add.controller.js | 63 +++++++++ .../src/inventories/related-hosts/add/main.js | 11 ++ .../edit/host-edit.controller.js | 79 +++++++++++ .../inventories/related-hosts/edit/main.js | 11 ++ .../list/build-host-list-state.factory.js | 89 +++++++++++++ .../list/host-list.controller.js | 123 ++++++++++++++++++ .../inventories/related-hosts/list/main.js | 13 ++ .../src/inventories/related-hosts/main.js | 27 ++++ .../related-hosts/related-host.form.js | 103 +++++++++++++++ .../related-hosts/related-host.list.js | 118 +++++++++++++++++ .../src/shared/stateDefinitions.factory.js | 6 + 13 files changed, 654 insertions(+), 33 deletions(-) create mode 100644 awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js create mode 100644 awx/ui/client/src/inventories/related-hosts/add/main.js create mode 100644 awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js create mode 100644 awx/ui/client/src/inventories/related-hosts/edit/main.js create mode 100644 awx/ui/client/src/inventories/related-hosts/list/build-host-list-state.factory.js create mode 100644 awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js create mode 100644 awx/ui/client/src/inventories/related-hosts/list/main.js create mode 100644 awx/ui/client/src/inventories/related-hosts/main.js create mode 100644 awx/ui/client/src/inventories/related-hosts/related-host.form.js create mode 100644 awx/ui/client/src/inventories/related-hosts/related-host.list.js diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 6755e72381..cf02ce202a 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -10,8 +10,10 @@ * @description This form is for adding/editing an inventory */ -export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', -function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState) { +export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', + 'buildGroupsEditState', 'buildHostListState', +function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, + buildHostListState) { return { addTitle: i18n._('NEW INVENTORY'), @@ -143,35 +145,12 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState) }, hosts: { name: 'hosts', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', - type: 'collection', + include: "RelatedHostsListDefinition", title: i18n._('Hosts'), iterator: 'host', - 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' - } - } + listState: buildHostListState, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState }, inventory_sources: { name: 'inventory_sources', diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 4f0f3b44fd..d8b6bcd33e 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -6,6 +6,7 @@ import host from './hosts/main'; import group from './groups/main'; +import relatedHost from './related-hosts/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; @@ -18,6 +19,7 @@ export default angular.module('inventory', [ host.name, group.name, + relatedHost.name, inventoryAdd.name, inventoryEdit.name, inventoryList.name @@ -116,10 +118,7 @@ angular.module('inventory', [ controllers: { list: 'InventoryListController', add: 'InventoryAddController', - edit: 'InventoryEditController', - related: { - groups: 'GroupsListController' - } + edit: 'InventoryEditController' }, urls: { list: '/inventories' diff --git a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js new file mode 100644 index 0000000000..7042800ee2 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js @@ -0,0 +1,63 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', + 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', 'ToJSON', + function($state, $stateParams, $scope, HostForm, ParseTypeChange, + GenerateForm, HostManageService, rbacUiControlService, GetBasePath, ToJSON) { + + init(); + + function init() { + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/hosts") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + $scope.parseType = 'yaml'; + $scope.host = { enabled: true }; + // apply form definition's default field values + GenerateForm.applyDefaults(HostForm, $scope); + + ParseTypeChange({ + scope: $scope, + field_id: 'host_variables', + variable: 'variables', + parse_variable: 'parseType' + }); + } + $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), + params = { + variables: json_data,// $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + name: $scope.name, + description: $scope.description, + enabled: $scope.host.enabled, + inventory: $stateParams.inventory_id + }; + HostManageService.post(params).then(function(res) { + // assign the host to current group if not at the root level + if ($stateParams.group) { + HostManageService.associateGroup(res.data, _.last($stateParams.group)).then(function() { + $state.go('inventoryManage.editHost', { host_id: res.data.id }, { reload: true }); + }); + } else { + $state.go('inventoryManage.editHost', { host_id: res.data.id }, { reload: true }); + } + }); + }; + } +]; diff --git a/awx/ui/client/src/inventories/related-hosts/add/main.js b/awx/ui/client/src/inventories/related-hosts/add/main.js new file mode 100644 index 0000000000..7a55327c9f --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/add/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './host-add.controller'; + +export default +angular.module('relatedHostsAdd', []) + .controller('RelatedHostAddController', controller); diff --git a/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js new file mode 100644 index 0000000000..250c65e4d7 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js @@ -0,0 +1,79 @@ +/************************************************* + * 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.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/related-hosts/edit/main.js b/awx/ui/client/src/inventories/related-hosts/edit/main.js new file mode 100644 index 0000000000..854a520053 --- /dev/null +++ b/awx/ui/client/src/inventories/related-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('relatedHostEdit', []) + .controller('HostEditController', controller); diff --git a/awx/ui/client/src/inventories/related-hosts/list/build-host-list-state.factory.js b/awx/ui/client/src/inventories/related-hosts/list/build-host-list-state.factory.js new file mode 100644 index 0000000000..8ad2d71c3d --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/list/build-host-list-state.factory.js @@ -0,0 +1,89 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import RelatedHostListController from './host-list.controller'; +export default ['RelatedHostsListDefinition', '$stateExtender', 'templateUrl', '$injector', + function(RelatedHostsListDefinition, $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(RelatedHostsListDefinition, generateList, $stateParams, GetBasePath) { + let list = _.cloneDeep(RelatedHostsListDefinition); + if($stateParams && $stateParams.group) { + list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/all_hosts'; + } + else { + //reaches here if the user is on the root level group + list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts'; + } + let html = generateList.build({ + list: list, + mode: 'edit' + }); + return html; + }, + controller: RelatedHostListController + } + }, + 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`]); + } + ], + hostsUrl: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { + return $stateParams.group && $stateParams.group.length > 0 ? + // nested context - provide all hosts managed by nodes + InventoryManageService.childHostsUrl(_.last($stateParams.group)) : + // root context - provide all hosts in an inventory + InventoryManageService.rootHostsUrl($stateParams.inventory_id); + }], + hostsDataset: ['RelatedHostsListDefinition', 'QuerySet', '$stateParams', 'hostsUrl', (list, qs, $stateParams, hostsUrl) => { + let path = hostsUrl; + 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/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js new file mode 100644 index 0000000000..9281ff8105 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -0,0 +1,123 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +// import HostManageService from './../hosts/host.service'; +export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePath', + 'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait', + 'HostManageService', 'SetStatus', + function($scope, RelatedHostsListDefinition, $rootScope, GetBasePath, + rbacUiControlService, Dataset, $state, $filter, Prompt, Wait, + HostManageService, SetStatus) { + + let list = RelatedHostsListDefinition; + + 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('inventories.edit.hosts.add'); + }; + $scope.editHost = function(id){ + $state.go('inventories.edit.hosts.edit', {host_id: id}); + }; + $scope.deleteHost = function(id, name){ + var body = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; + var action = function(){ + delete $rootScope.promptActionBtnClass; + Wait('start'); + HostManageService.delete(id).then(() => { + $('#prompt-modal').modal('hide'); + if (parseInt($state.params.host_id) === id) { + $state.go("hosts", null, {reload: true}); + } else { + $state.go($state.current.name, null, {reload: true}); + } + Wait('stop'); + }); + }; + // Prompt depends on having $rootScope.promptActionBtnClass available... + Prompt({ + hdr: 'Delete Host', + body: body, + action: action, + actionText: 'DELETE', + }); + $rootScope.promptActionBtnClass = 'Modal-errorButton'; + }; + + $scope.toggleHost = function(event, host) { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + + host.enabled = !host.enabled; + + HostManageService.put(host).then(function(){ + $state.go($state.current, null, {reload: true}); + }); + }; + + $scope.smartInventory = function() { + $state.go('inventories.addSmartInventory'); + }; +}]; diff --git a/awx/ui/client/src/inventories/related-hosts/list/main.js b/awx/ui/client/src/inventories/related-hosts/list/main.js new file mode 100644 index 0000000000..5bdc8f2bc8 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/list/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildHostListState from './build-host-list-state.factory'; +import RelatedHostListController from './host-list.controller'; + +export default +angular.module('relatedHostList', []) + .factory('buildHostListState', buildHostListState) + .controller('RelatedHostListController', RelatedHostListController); diff --git a/awx/ui/client/src/inventories/related-hosts/main.js b/awx/ui/client/src/inventories/related-hosts/main.js new file mode 100644 index 0000000000..717c359046 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/main.js @@ -0,0 +1,27 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import relatedHostAdd from './add/main'; + import relatedHostEdit from './edit/main'; + import relatedHostList from './list/main'; + import relatedHostsListDefinition from './related-host.list'; + import relatedHostsFormDefinition from './related-host.form'; + // import HostManageService from './hosts.service'; + // import SetStatus from './set-status.factory'; + // import SetEnabledMsg from './set-enabled-msg.factory'; + // import SmartInventory from './smart-inventory/main'; + +export default +angular.module('relatedHost', [ + relatedHostAdd.name, + relatedHostEdit.name, + relatedHostList.name + ]) + .value('RelatedHostsFormDefinition', relatedHostsFormDefinition) + .value('RelatedHostsListDefinition', relatedHostsListDefinition); + // .factory('SetStatus', SetStatus) + // .factory('SetEnabledMsg', SetEnabledMsg) + // .service('HostManageService', HostManageService); diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.form.js b/awx/ui/client/src/inventories/related-hosts/related-host.form.js new file mode 100644 index 0000000000..efb64a4785 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/related-host.form.js @@ -0,0 +1,103 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:Hosts + * @description This form is for adding/editing a host on the inventory page +*/ + +export default ['i18n', function(i18n) { + return { + + addTitle: i18n._('CREATE HOST'), + editTitle: '{{ host.name }}', + name: 'host', + basePath: 'hosts', + well: false, + formLabelSize: 'col-lg-3', + formFieldSize: 'col-lg-9', + iterator: 'host', + headerFields:{ + enabled: { + class: 'Form-header-field', + ngClick: 'toggleHostEnabled(host)', + type: 'toggle', + 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.") + + "

", + 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
" + + "127.0.0.1
" + + "10.1.0.140:25
" + + "server.example.com:25" + + "
", + 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: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

' + 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)' + } + }, + }; + }]; diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js new file mode 100644 index 0000000000..3b4140e74a --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -0,0 +1,118 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'hosts', + iterator: 'host', + editTitle: '{{ selected_group }}', + showTitle: false, + well: true, + wellOverride: true, + index: false, + hover: true, + // hasChildren: true, + multiSelect: true, + trackBy: 'host.id', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/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: "{{ 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: 'Hosts', + ngClick: "editHost(host.id)", + ngClass: "{ 'host-disabled-label': !host.enabled }", + columnClass: 'col-lg-6 col-md-8 col-sm-8 col-xs-7', + dataHostId: "{{ host.id }}", + dataType: "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(host.id)", + awToolTip: 'Copy or move host to another group', + dataPlacement: "top", + ngShow: 'host.summary_fields.user_capabilities.edit' + }, + 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' + }, + "delete": { + //label: 'Delete', + ngClick: "deleteHost(host.id, host.name)", + icon: 'icon-trash', + awToolTip: 'Delete host', + dataPlacement: 'top', + ngShow: 'host.summary_fields.user_capabilities.delete' + } + }, + + actions: { + 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/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 107ea95b4e..ad995d62bf 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -559,6 +559,12 @@ function($injector, $stateExtender, $log, i18n) { states.push(field.editState(field, formStateDefinition, params)); states = _.flatten(states); } + else if(field.iterator === 'host'){ + states.push(field.listState(field, formStateDefinition)); + } + // if(field && field.listState){ + // states.push(field.listState(field, formStateDefinition)); + // } else if(field.iterator === 'notification'){ states.push(buildNotificationState(field)); states = _.flatten(states); From e411d5c69b385b72c68675abe9871bfac4753dcb Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 13 Apr 2017 10:49:06 -0400 Subject: [PATCH 18/49] More inventory state setup. This should get basic/smart inventory forms working in conjunction --- .../smart-inventory/smart-inventory.form.js | 33 +- awx/ui/client/src/inventories/main.js | 322 +++++++++++------- 2 files changed, 211 insertions(+), 144 deletions(-) diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js index 9404bb575c..a8055d8838 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default ['i18n', function(i18n) { +export default ['i18n', 'buildHostListState', function(i18n, buildHostListState) { return { addTitle: i18n._('NEW SMART INVENTORY'), @@ -126,35 +126,12 @@ export default ['i18n', function(i18n) { }, hosts: { name: 'hosts', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/hosts/', - type: 'collection', + include: "RelatedHostsListDefinition", title: i18n._('Hosts'), iterator: 'host', - 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' - } - } + listState: buildHostListState, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState }, //this is a placeholder for when we're ready for completed jobs completed_jobs: { diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index d8b6bcd33e..d7c93dfc22 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -34,129 +34,219 @@ angular.module('inventory', [ let stateDefinitions = stateDefinitionsProvider.$get(), stateExtender = $stateExtenderProvider.$get(); - function generateInventoryStates() { - let smartInventoryAdd = { - name: 'inventories.addSmartInventory', - url: '/smartinventory', - form: 'SmartInventoryForm', - ncyBreadcrumb: { - label: "CREATE SMART INVENTORY" - }, - views: { - 'form@inventories': { - templateProvider: function(SmartInventoryForm, GenerateForm) { - return GenerateForm.buildHTML(SmartInventoryForm, { - mode: 'add', - related: false - }); + function generateInventoryStates() { + + let basicInventoryAdd = stateDefinitions.generateTree({ + name: 'inventories.add', // top-most node in the generated tree (will replace this state definition) + url: '/basic_inventory/add', + modes: ['add'], + form: 'InventoryForm', + controllers: { + add: 'InventoryAddController' + } + }); + + let basicInventoryEdit = stateDefinitions.generateTree({ + name: 'inventories.edit', + url: '/basic_inventory/:inventory_id', + modes: ['edit'], + form: 'InventoryForm', + controllers: { + edit: 'InventoryEditController' + } + }); + + let smartInventoryAdd = stateDefinitions.generateTree({ + name: 'inventories.addSmartInventory', // top-most node in the generated tree (will replace this state definition) + url: '/smart_inventory/add', + modes: ['add'], + form: 'SmartInventoryForm', + controllers: { + add: 'SmartInventoryAddController' + } + }); + + let smartInventoryEdit = stateDefinitions.generateTree({ + name: 'inventories.editSmartInventory', + url: '/smart_inventory/:inventory_id', + modes: ['edit'], + form: 'SmartInventoryForm', + controllers: { + edit: 'SmartInventoryEditController' + } + }); + + return Promise.all([ + basicInventoryAdd, + basicInventoryEdit, + smartInventoryAdd, + smartInventoryEdit + ]).then((generated) => { + return { + states: _.reduce(generated, (result, definition) => { + return result.concat(definition.states); + }, [ + stateExtender.buildDefinition({ + name: 'inventories', // top-most node in the generated tree (will replace this state definition) + route: '/inventories', + ncyBreadcrumb: { + label: N_('INVENTORIES') }, - controller: 'SmartInventoryAddController' - } - } - }; - - let smartInventoryAddOrgLookup = { - searchPrefix: 'organization', - name: 'inventories.addSmartInventory.organization', - url: '/organization', - data: { - formChildState: true - }, - params: { - organization_search: { - value: { - page_size: '5' + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') + }, + 'list@inventories': { + templateProvider: function(InventoryList, generateList) { + let html = generateList.build({ + list: InventoryList, + mode: 'edit' + }); + return html; + }, + controller: 'InventoryListController' + } }, - squash: true, - dynamic: true - } - }, - ncyBreadcrumb: { - skip: true - }, - views: { - 'related': { - templateProvider: function(ListDefinition, generateList) { - let list_html = generateList.build({ - mode: 'lookup', - list: ListDefinition, - input_type: 'radio' - }); - return `${list_html}`; - + searchPrefix: 'inventory', + resolve: { + Dataset: ['InventoryList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] } - } - }, - resolve: { - ListDefinition: ['OrganizationList', function(OrganizationList) { - let list = _.cloneDeep(OrganizationList); - list.lookupConfirmText = 'SELECT'; - return list; - }], - Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.name) || GetBasePath(list.basePath); - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ] - }, - onExit: function($state) { - if ($state.transition) { - $('#form-modal').modal('hide'); - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open'); - } - }, + }) + ]) }; + }); - let inventories = stateDefinitions.generateTree({ - parent: 'inventories', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'InventoryList', - form: 'InventoryForm', - controllers: { - list: 'InventoryListController', - add: 'InventoryAddController', - edit: 'InventoryEditController' - }, - urls: { - list: '/inventories' - }, - ncyBreadcrumb: { - label: N_('INVENTORIES') - }, - views: { - '@': { - templateUrl: templateUrl('inventories/inventories') - }, - 'list@inventories': { - templateProvider: function(InventoryList, generateList) { - let html = generateList.build({ - list: InventoryList, - mode: 'edit' - }); - return html; - }, - controller: 'InventoryListController' - } - } - }); + } - return Promise.all([ - inventories - ]).then((generated) => { - return { - states: _.reduce(generated, (result, definition) => { - return result.concat(definition.states); - }, [ - stateExtender.buildDefinition(smartInventoryAdd), - stateExtender.buildDefinition(smartInventoryAddOrgLookup) - ]) - }; - }); - - } + // function generateInventoryStates() { + // + // let smartInventoryAdd = { + // name: 'inventories.addSmartInventory', + // url: '/smartinventory', + // form: 'SmartInventoryForm', + // ncyBreadcrumb: { + // label: "CREATE SMART INVENTORY" + // }, + // views: { + // 'form@inventories': { + // templateProvider: function(SmartInventoryForm, GenerateForm) { + // return GenerateForm.buildHTML(SmartInventoryForm, { + // mode: 'add', + // related: false + // }); + // }, + // controller: 'SmartInventoryAddController' + // } + // } + // }; + // + // let smartInventoryAddOrgLookup = { + // searchPrefix: 'organization', + // name: 'inventories.addSmartInventory.organization', + // url: '/organization', + // data: { + // formChildState: true + // }, + // params: { + // organization_search: { + // value: { + // page_size: '5' + // }, + // squash: true, + // dynamic: true + // } + // }, + // ncyBreadcrumb: { + // skip: true + // }, + // views: { + // 'related': { + // templateProvider: function(ListDefinition, generateList) { + // let list_html = generateList.build({ + // mode: 'lookup', + // list: ListDefinition, + // input_type: 'radio' + // }); + // return `${list_html}`; + // + // } + // } + // }, + // resolve: { + // ListDefinition: ['OrganizationList', function(OrganizationList) { + // let list = _.cloneDeep(OrganizationList); + // list.lookupConfirmText = 'SELECT'; + // return list; + // }], + // Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', + // (list, qs, $stateParams, GetBasePath) => { + // let path = GetBasePath(list.name) || GetBasePath(list.basePath); + // return qs.search(path, $stateParams[`${list.iterator}_search`]); + // } + // ] + // }, + // onExit: function($state) { + // if ($state.transition) { + // $('#form-modal').modal('hide'); + // $('.modal-backdrop').remove(); + // $('body').removeClass('modal-open'); + // } + // }, + // }; + // + // let inventories = stateDefinitions.generateTree({ + // parent: 'inventories', // top-most node in the generated tree (will replace this state definition) + // modes: ['add', 'edit'], + // list: 'InventoryList', + // form: 'InventoryForm', + // controllers: { + // list: 'InventoryListController', + // add: 'InventoryAddController', + // edit: 'InventoryEditController' + // }, + // urls: { + // list: '/inventories' + // }, + // ncyBreadcrumb: { + // label: N_('INVENTORIES') + // }, + // views: { + // '@': { + // templateUrl: templateUrl('inventories/inventories') + // }, + // 'list@inventories': { + // templateProvider: function(InventoryList, generateList) { + // let html = generateList.build({ + // list: InventoryList, + // mode: 'edit' + // }); + // return html; + // }, + // controller: 'InventoryListController' + // } + // } + // }); + // + // return Promise.all([ + // inventories + // ]).then((generated) => { + // return { + // states: _.reduce(generated, (result, definition) => { + // return result.concat(definition.states); + // }, [ + // stateExtender.buildDefinition(smartInventoryAdd), + // stateExtender.buildDefinition(smartInventoryAddOrgLookup) + // ]) + // }; + // }); + // + // } $stateProvider.state({ name: 'hosts', From 9131ee59a29f3aea34f852f23f670b3379389dac Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 13 Apr 2017 13:18:57 -0400 Subject: [PATCH 19/49] Pass host filter through to the smart inv form. Fixed inventory list name link --- .../hosts/list/host-list.controller.js | 15 ++++++++++++++- awx/ui/client/src/inventories/inventory.list.js | 2 +- awx/ui/client/src/inventories/main.js | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) 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 index 4f0b6f0b6a..7f89b64e99 100644 --- a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -115,7 +115,20 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, }; $scope.smartInventory = function() { - $state.go('inventories.addSmartInventory'); + // Gather up search terms and pass them to the add smart inventory form + let stateParamsCopy = angular.copy($state.params.host_search); + let defaults = _.find($state.$current.path, (step) => { + if(step && step.params && step.params.hasOwnProperty(`host_search`)){ + return step.params.hasOwnProperty(`host_search`); + } + }).params[`host_search`].config.value; + + // Strip defaults out of the state params copy + angular.forEach(Object.keys(defaults), function(value) { + delete stateParamsCopy[value]; + }); + + $state.go('inventories.addSmartInventory', {hostfilter: JSON.stringify(stateParamsCopy)}); }; } diff --git a/awx/ui/client/src/inventories/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index fb6bac2a68..557a1a8a9c 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -47,7 +47,7 @@ export default ['i18n', function(i18n) { modalColumnClass: 'col-md-11', awToolTip: "{{ inventory.description }}", awTipPlacement: "top" - linkTo: '/#/inventories/{{inventory.id}}' + linkTo: '/#/inventories/basic_inventory/{{inventory.id}}' }, organization: { label: i18n._('Organization'), diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index d7c93dfc22..0e3532bd2e 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -59,7 +59,7 @@ angular.module('inventory', [ let smartInventoryAdd = stateDefinitions.generateTree({ name: 'inventories.addSmartInventory', // top-most node in the generated tree (will replace this state definition) - url: '/smart_inventory/add', + url: '/smart_inventory/add?hostfilter', modes: ['add'], form: 'SmartInventoryForm', controllers: { From 62fafc9870f8b249e99d138f8e13273ad0e725cf Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 13 Apr 2017 15:25:56 -0700 Subject: [PATCH 20/49] adding related-host-add/edit routes for inventories --- .../src/inventories/inventories.partial.html | 1 + .../client/src/inventories/inventory.form.js | 9 ++-- .../add/build-host-add-state.factory.js | 46 +++++++++++++++++++ .../related-hosts/add/host-add.controller.js | 6 +-- .../src/inventories/related-hosts/add/main.js | 2 + .../edit/host-edit.controller.js | 8 ++-- .../inventories/related-hosts/edit/main.js | 5 +- .../src/inventories/related-hosts/main.js | 9 +--- .../src/shared/stateDefinitions.factory.js | 24 +++++----- 9 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 awx/ui/client/src/inventories/related-hosts/add/build-host-add-state.factory.js diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 26d66de7af..875d23486b 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,5 +1,6 @@
+
diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index cf02ce202a..317800d0fb 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -11,9 +11,10 @@ */ export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', - 'buildGroupsEditState', 'buildHostListState', + 'buildGroupsEditState', 'buildHostListState', 'buildHostAddState', + 'buildHostEditState', function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, - buildHostListState) { + buildHostListState, buildHostAddState, buildHostEditState) { return { addTitle: i18n._('NEW INVENTORY'), @@ -149,8 +150,8 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, title: i18n._('Hosts'), iterator: 'host', listState: buildHostListState, - // addState: buildGroupsAddState, - // editState: buildGroupsEditState + addState: buildHostAddState, + editState: buildHostEditState }, inventory_sources: { name: 'inventory_sources', diff --git a/awx/ui/client/src/inventories/related-hosts/add/build-host-add-state.factory.js b/awx/ui/client/src/inventories/related-hosts/add/build-host-add-state.factory.js new file mode 100644 index 0000000000..4a1ed4c9d7 --- /dev/null +++ b/awx/ui/client/src/inventories/related-hosts/add/build-host-add-state.factory.js @@ -0,0 +1,46 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +import RelatedHostAddController from './host-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: { + 'hostForm@inventories': { + templateProvider: function(GenerateForm, RelatedHostsFormDefinition) { + let form = RelatedHostsFormDefinition; + return GenerateForm.buildHTML(form, { + mode: 'add', + related: false + }); + }, + controller: RelatedHostAddController + } + }, + resolve: { + 'FormDefinition': [params.form, function(definition) { + return definition; + }] + } + }; + + state = $stateExtender.buildDefinition(stateConfig); + return state; + }; + return val; + } +]; diff --git a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js index 7042800ee2..6bb5d0ab40 100644 --- a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js @@ -4,9 +4,9 @@ * All Rights Reserved *************************************************/ -export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', +export default ['$state', '$stateParams', '$scope', 'RelatedHostsFormDefinition', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', 'ToJSON', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, + function($state, $stateParams, $scope, RelatedHostsFormDefinition, ParseTypeChange, GenerateForm, HostManageService, rbacUiControlService, GetBasePath, ToJSON) { init(); @@ -21,7 +21,7 @@ export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange $scope.parseType = 'yaml'; $scope.host = { enabled: true }; // apply form definition's default field values - GenerateForm.applyDefaults(HostForm, $scope); + GenerateForm.applyDefaults(RelatedHostsFormDefinition, $scope); ParseTypeChange({ scope: $scope, diff --git a/awx/ui/client/src/inventories/related-hosts/add/main.js b/awx/ui/client/src/inventories/related-hosts/add/main.js index 7a55327c9f..48128f9a38 100644 --- a/awx/ui/client/src/inventories/related-hosts/add/main.js +++ b/awx/ui/client/src/inventories/related-hosts/add/main.js @@ -4,8 +4,10 @@ * All Rights Reserved *************************************************/ +import buildHostAddState from './build-host-add-state.factory'; import controller from './host-add.controller'; export default angular.module('relatedHostsAdd', []) + .factory('buildHostAddState', buildHostAddState) .controller('RelatedHostAddController', controller); diff --git a/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js index 250c65e4d7..048388f791 100644 --- a/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/edit/host-edit.controller.js @@ -34,10 +34,10 @@ }; var init = function(){ - $scope.host = host.data; - $scope.name = host.data.name; - $scope.description = host.data.description; - $scope.variables = getVars(host.data.variables); + $scope.host = host; + $scope.name = host.name; + $scope.description = host.description; + $scope.variables = getVars(host.variables); ParseTypeChange({ scope: $scope, field_id: 'host_variables', diff --git a/awx/ui/client/src/inventories/related-hosts/edit/main.js b/awx/ui/client/src/inventories/related-hosts/edit/main.js index 854a520053..731113f6e5 100644 --- a/awx/ui/client/src/inventories/related-hosts/edit/main.js +++ b/awx/ui/client/src/inventories/related-hosts/edit/main.js @@ -3,9 +3,10 @@ * * All Rights Reserved *************************************************/ - +import buildHostEditState from './build-host-edit-state.factory'; import controller from './host-edit.controller'; export default angular.module('relatedHostEdit', []) - .controller('HostEditController', controller); + .factory('buildHostEditState', buildHostEditState) + .controller('RelatedHostEditController', controller); diff --git a/awx/ui/client/src/inventories/related-hosts/main.js b/awx/ui/client/src/inventories/related-hosts/main.js index 717c359046..2f892270b3 100644 --- a/awx/ui/client/src/inventories/related-hosts/main.js +++ b/awx/ui/client/src/inventories/related-hosts/main.js @@ -9,10 +9,6 @@ import relatedHostList from './list/main'; import relatedHostsListDefinition from './related-host.list'; import relatedHostsFormDefinition from './related-host.form'; - // import HostManageService from './hosts.service'; - // import SetStatus from './set-status.factory'; - // import SetEnabledMsg from './set-enabled-msg.factory'; - // import SmartInventory from './smart-inventory/main'; export default angular.module('relatedHost', [ @@ -20,8 +16,5 @@ angular.module('relatedHost', [ relatedHostEdit.name, relatedHostList.name ]) - .value('RelatedHostsFormDefinition', relatedHostsFormDefinition) + .factory('RelatedHostsFormDefinition', relatedHostsFormDefinition) .value('RelatedHostsListDefinition', relatedHostsListDefinition); - // .factory('SetStatus', SetStatus) - // .factory('SetEnabledMsg', SetEnabledMsg) - // .service('HostManageService', HostManageService); diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index ad995d62bf..a5e9797f38 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -553,18 +553,20 @@ function($injector, $stateExtender, $log, i18n) { function buildListNodes(field) { let states = []; - if(field.iterator === 'group'){ - states.push(field.listState(field, formStateDefinition)); - states.push(field.addState(field, formStateDefinition, params)); - states.push(field.editState(field, formStateDefinition, params)); - states = _.flatten(states); + if(field && (field.listState || field.addState || field.editState)){ + if(field && field.listState){ + states.push(field.listState(field, formStateDefinition)); + states = _.flatten(states); + } + if(field && field.addState){ + states.push(field.addState(field, formStateDefinition, params)); + states = _.flatten(states); + } + if(field && field.editState){ + states.push(field.editState(field, formStateDefinition, params)); + states = _.flatten(states); + } } - else if(field.iterator === 'host'){ - states.push(field.listState(field, formStateDefinition)); - } - // if(field && field.listState){ - // states.push(field.listState(field, formStateDefinition)); - // } else if(field.iterator === 'notification'){ states.push(buildNotificationState(field)); states = _.flatten(states); From cc80cd854995e5e42721979eb00d5b152e2de634 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 18 Apr 2017 12:20:58 -0400 Subject: [PATCH 21/49] Dynamic inventory add form work --- .../add/smart-inventory-add.controller.js | 2 + ...ynamic-inventory-host-filter.controller.js | 15 +++ ...dynamic-inventory-host-filter.directive.js | 25 ++++ ...dynamic-inventory-host-filter.partial.html | 12 ++ .../host-filter-modal.directive.js | 81 ++++++++++++ .../host-filter-modal.partial.html | 21 +++ .../inventories/hosts/smart-inventory/main.js | 6 +- .../smart-inventory/smart-inventory.form.js | 11 ++ awx/ui/client/src/inventories/main.js | 124 ------------------ .../list-generator/list-generator.factory.js | 8 +- .../shared/smart-search/queryset.service.js | 26 ++++ .../smart-search/smart-search.controller.js | 36 +---- .../src/shared/stateDefinitions.factory.js | 5 + 13 files changed, 216 insertions(+), 156 deletions(-) create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.controller.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.directive.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.partial.html create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js create mode 100644 awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.partial.html diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js index 5ccac44ddc..32fff4da0a 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/add/smart-inventory-add.controller.js @@ -53,6 +53,8 @@ function SmartInventoryAdd($scope, $location, parse_variable: 'parseType', field_id: 'smartinventory_variables' }); + + $scope.dynamic_hosts = $state.params.hostfilter ? JSON.parse($state.params.hostfilter) : ''; } // Save diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.controller.js new file mode 100644 index 0000000000..6a96c0d74c --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.controller.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', 'QuerySet', + function($scope, qs) { + $scope.hostFilterTags = []; + + $scope.$watch('hostFilter', function(){ + $scope.hostFilterTags = qs.stripDefaultParams($scope.hostFilter); + }); + } +]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.directive.js b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.directive.js new file mode 100644 index 0000000000..7d336abae7 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.directive.js @@ -0,0 +1,25 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import dynamicInventoryHostFilterController from './dynamic-inventory-host-filter.controller'; + +export default ['templateUrl', '$compile', + function(templateUrl, $compile) { + return { + scope: { + hostFilter: '=' + }, + restrict: 'E', + templateUrl: templateUrl('inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter'), + controller: dynamicInventoryHostFilterController, + link: function(scope) { + scope.openHostFilterModal = function() { + $('#content-container').append($compile('')(scope)); + }; + } + }; + } +]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.partial.html b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.partial.html new file mode 100644 index 0000000000..6b3546d2b9 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/dynamic-inventory-host-filter.partial.html @@ -0,0 +1,12 @@ +
+ + + + + + {{tag}} + + +
diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js new file mode 100644 index 0000000000..6a29ec99a2 --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js @@ -0,0 +1,81 @@ +export default ['templateUrl', function(templateUrl) { + return { + restrict: 'E', + scope: { + hostFilter: '=' + }, + templateUrl: templateUrl('inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal'), + link: function(scope, element) { + + $('#host-filter-modal').on('hidden.bs.modal', function () { + $('#host-filter-modal').off('hidden.bs.modal'); + $(element).remove(); + }); + + scope.showModal = function() { + $('#host-filter-modal').modal('show'); + }; + + scope.destroyModal = function() { + $('#host-filter-modal').modal('hide'); + }; + + }, + controller: ['$scope', 'QuerySet', 'GetBasePath', 'HostsList', '$compile', 'generateList', function($scope, qs, GetBasePath, HostsList, $compile, GenerateList) { + + function init() { + + $scope.host_default_params = { + order_by: 'name', + page_size: 5 + }; + + $scope.host_queryset = _.merge({ + order_by: 'name', + page_size: 5 + }, $scope.hostFilter ? $scope.hostFilter : {}); + + // Fire off the initial search + qs.search(GetBasePath('hosts'), $scope.host_queryset) + .then(res => { + $scope.host_dataset = res.data; + $scope.hosts = $scope.host_dataset.results; + + let hostList = _.cloneDeep(HostsList); + delete hostList.fields.toggleHost; + delete hostList.fields.active_failures; + delete hostList.fields.inventory_name; + let html = GenerateList.build({ + list: hostList, + input_type: 'foobar', + mode: 'lookup' + }); + + $scope.list = hostList; + + $('#foobar').append($compile(html)($scope)); + + $scope.showModal(); + }); + } + + init(); + + $scope.cancelForm = function() { + $scope.destroyModal(); + }; + + $scope.saveForm = function() { + // Strip defaults out of the state params copy + angular.forEach(Object.keys($scope.host_default_params), function(value) { + delete $scope.host_queryset[value]; + }); + + $scope.hostFilter = angular.copy($scope.host_queryset); + + $scope.destroyModal(); + }; + + }] + }; +}]; diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.partial.html b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.partial.html new file mode 100644 index 0000000000..3f5a33e48d --- /dev/null +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.partial.html @@ -0,0 +1,21 @@ + diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/main.js b/awx/ui/client/src/inventories/hosts/smart-inventory/main.js index 7fa368df70..a560ac0459 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/main.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/main.js @@ -7,10 +7,14 @@ import smartInventoryAdd from './add/main'; import smartInventoryEdit from './edit/main'; import SmartInventoryForm from './smart-inventory.form'; + import dynamicInventoryHostFilter from './dynamic-inventory-host-filter/dynamic-inventory-host-filter.directive'; + import hostFilterModal from './dynamic-inventory-host-filter/host-filter-modal/host-filter-modal.directive'; export default angular.module('smartInventory', [ smartInventoryAdd.name, smartInventoryEdit.name ]) - .factory('SmartInventoryForm', SmartInventoryForm); + .factory('SmartInventoryForm', SmartInventoryForm) + .directive('dynamicInventoryHostFilter', dynamicInventoryHostFilter) + .directive('hostFilterModal', hostFilterModal); diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js index a8055d8838..7be1881920 100644 --- a/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories/hosts/smart-inventory/smart-inventory.form.js @@ -43,6 +43,17 @@ export default ['i18n', 'buildHostListState', function(i18n, buildHostListState) ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' }, + dynamic_hosts: { + label: i18n._('Dynamic Hosts'), + type: 'custom', + control: '', + basePath: 'hosts', + list: 'HostsList', + sourceModel: 'host', + sourceField: 'name', + required: true + // TODO: add required, ngDisabled, awLookupWhen (?) + }, variables: { label: i18n._('Variables'), type: 'textarea', diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 0e3532bd2e..c9de579972 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -124,130 +124,6 @@ angular.module('inventory', [ } - // function generateInventoryStates() { - // - // let smartInventoryAdd = { - // name: 'inventories.addSmartInventory', - // url: '/smartinventory', - // form: 'SmartInventoryForm', - // ncyBreadcrumb: { - // label: "CREATE SMART INVENTORY" - // }, - // views: { - // 'form@inventories': { - // templateProvider: function(SmartInventoryForm, GenerateForm) { - // return GenerateForm.buildHTML(SmartInventoryForm, { - // mode: 'add', - // related: false - // }); - // }, - // controller: 'SmartInventoryAddController' - // } - // } - // }; - // - // let smartInventoryAddOrgLookup = { - // searchPrefix: 'organization', - // name: 'inventories.addSmartInventory.organization', - // url: '/organization', - // data: { - // formChildState: true - // }, - // params: { - // organization_search: { - // value: { - // page_size: '5' - // }, - // squash: true, - // dynamic: true - // } - // }, - // ncyBreadcrumb: { - // skip: true - // }, - // views: { - // 'related': { - // templateProvider: function(ListDefinition, generateList) { - // let list_html = generateList.build({ - // mode: 'lookup', - // list: ListDefinition, - // input_type: 'radio' - // }); - // return `${list_html}`; - // - // } - // } - // }, - // resolve: { - // ListDefinition: ['OrganizationList', function(OrganizationList) { - // let list = _.cloneDeep(OrganizationList); - // list.lookupConfirmText = 'SELECT'; - // return list; - // }], - // Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', - // (list, qs, $stateParams, GetBasePath) => { - // let path = GetBasePath(list.name) || GetBasePath(list.basePath); - // return qs.search(path, $stateParams[`${list.iterator}_search`]); - // } - // ] - // }, - // onExit: function($state) { - // if ($state.transition) { - // $('#form-modal').modal('hide'); - // $('.modal-backdrop').remove(); - // $('body').removeClass('modal-open'); - // } - // }, - // }; - // - // let inventories = stateDefinitions.generateTree({ - // parent: 'inventories', // top-most node in the generated tree (will replace this state definition) - // modes: ['add', 'edit'], - // list: 'InventoryList', - // form: 'InventoryForm', - // controllers: { - // list: 'InventoryListController', - // add: 'InventoryAddController', - // edit: 'InventoryEditController' - // }, - // urls: { - // list: '/inventories' - // }, - // ncyBreadcrumb: { - // label: N_('INVENTORIES') - // }, - // views: { - // '@': { - // templateUrl: templateUrl('inventories/inventories') - // }, - // 'list@inventories': { - // templateProvider: function(InventoryList, generateList) { - // let html = generateList.build({ - // list: InventoryList, - // mode: 'edit' - // }); - // return html; - // }, - // controller: 'InventoryListController' - // } - // } - // }); - // - // return Promise.all([ - // inventories - // ]).then((generated) => { - // return { - // states: _.reduce(generated, (result, definition) => { - // return result.concat(definition.states); - // }, [ - // stateExtender.buildDefinition(smartInventoryAdd), - // stateExtender.buildDefinition(smartInventoryAddOrgLookup) - // ]) - // }; - // }); - // - // } - $stateProvider.state({ name: 'hosts', url: '/hosts', diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index ac2802d6f9..78eb66f5b7 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -316,7 +316,11 @@ export default ['$compile', 'Attr', 'Icon', if (options.mode === 'lookup') { if (options.input_type === "radio") { //added by JT so that lookup forms can be either radio inputs or check box inputs innerTable += ` `; - } else { // its assumed that options.input_type = checkbox + } + else if (options.input_type === "foobar") { + + } + else { // its assumed that options.input_type = checkbox innerTable += ""; @@ -483,7 +487,7 @@ export default ['$compile', 'Attr', 'Icon', if (list.multiSelect) { html += buildSelectAll().prop('outerHTML'); - } else if (options.mode === 'lookup') { + } else if (options.mode === 'lookup' && options.input_type !== 'foobar') { html += ""; } diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index c2800f0bf2..f647b48dd4 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -274,6 +274,32 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear hdr: 'Error!', msg: `Invalid search term entered. GET returned: ${status}` }); + }, + // Removes state definition defaults and pagination terms + stripDefaultParams(params, defaults) { + if(defaults) { + let stripped =_.pick(params, (value, key) => { + // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value + return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null; + }); + let strippedCopy = _.cloneDeep(stripped); + if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){ + for (var key in strippedCopy) { + if (strippedCopy.hasOwnProperty(key)) { + let value = strippedCopy[key]; + if(_.isArray(value)){ + let index = _.indexOf(value, defaults[key]); + value = value.splice(index, 1)[0]; + } + } + } + stripped = strippedCopy; + } + return _(strippedCopy).map(this.decodeParam).flatten().value(); + } + else { + return _(params).map(this.decodeParam).flatten().value(); + } } }; } diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index e008a0db78..a6211f2e56 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -22,7 +22,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' queryset = _.cloneDeep($scope.querySet); } else { - queryset = $stateParams[`${$scope.iterator}_search`]; + queryset = $state.params[`${$scope.iterator}_search`]; } // build $scope.tags from $stateParams.QuerySet, build fieldset key @@ -30,7 +30,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' function init() { path = GetBasePath($scope.basePath) || $scope.basePath; - $scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); qs.initFieldset(path, $scope.djangoModel).then((data) => { $scope.models = data.models; $scope.options = data.options.data; @@ -69,7 +69,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' }); $scope.searchTerm = null; - $scope.searchTags = stripDefaultParams(queryset); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); } } }); @@ -86,28 +86,6 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' }); } - // Removes state definition defaults and pagination terms - function stripDefaultParams(params) { - let stripped =_.pick(params, (value, key) => { - // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value - return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null; - }); - let strippedCopy = _.cloneDeep(stripped); - if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){ - for (var key in strippedCopy) { - if (strippedCopy.hasOwnProperty(key)) { - let value = strippedCopy[key]; - if(_.isArray(value)){ - let index = _.indexOf(value, defaults[key]); - value = value.splice(index, 1)[0]; - } - } - } - stripped = strippedCopy; - } - return _(strippedCopy).map(qs.decodeParam).flatten().value(); - } - function setDefaults(term) { if ($scope.list.defaultSearchParams) { return $scope.list.defaultSearchParams(encodeURIComponent(term)); @@ -136,7 +114,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' $scope.dataset = res.data; $scope.collection = res.data.results; }); - $scope.searchTags = stripDefaultParams(queryset); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); }; // remove tag, merge new queryset, $state.go @@ -208,7 +186,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' $scope.dataset = res.data; $scope.collection = res.data.results; }); - $scope.searchTags = stripDefaultParams(queryset); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); }; // add a search tag, merge new queryset, $state.go() @@ -311,7 +289,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' }); $scope.searchTerm = null; - $scope.searchTags = stripDefaultParams(queryset); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); } }; @@ -333,7 +311,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', ' }); $scope.searchTerm = null; - $scope.searchTags = stripDefaultParams(queryset); + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); }; } ]; diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index a5e9797f38..b131ff4c95 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -699,6 +699,11 @@ function($injector, $stateExtender, $log, i18n) { organization: null }; } + else if(field.sourceModel === 'host') { + params = { + page_size: '5' + }; + } else { params = { page_size: '5', From 8173ec4014ff5ce4c999f2cd90154b242baa1ccb Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 18 Apr 2017 15:06:39 -0400 Subject: [PATCH 22/49] Added missing comma --- awx/ui/client/src/inventories/inventory.list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index 557a1a8a9c..240639b4db 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -46,7 +46,7 @@ export default ['i18n', function(i18n) { columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent', modalColumnClass: 'col-md-11', awToolTip: "{{ inventory.description }}", - awTipPlacement: "top" + awTipPlacement: "top", linkTo: '/#/inventories/basic_inventory/{{inventory.id}}' }, organization: { From ad494bbb40adab78dfd2501d6d6e4b83aac03911 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 19 Apr 2017 10:04:17 -0400 Subject: [PATCH 23/49] Navigate the user to the inventory edit form after creating basic inventory --- awx/ui/client/src/inventories/add/inventory-add.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 02e608af6c..dd965bd362 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -77,7 +77,7 @@ function InventoriesAdd($scope, $location, .success(function(data) { var inventory_id = data.id; Wait('stop'); - $location.path('/inventories/' + inventory_id); + $state.go('inventories.edit', {inventory_id: inventory_id}); }) .error(function(data, status) { ProcessErrors($scope, data, status, form, { From a9ea639d9b8a9696ac6372554fc44601ccc0c577 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 19 Apr 2017 12:50:13 -0400 Subject: [PATCH 24/49] Hook up system tracking from the host list --- .../list/host-list.controller.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js index 9281ff8105..f89b6c6606 100644 --- a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -61,6 +61,24 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa } }); + $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(){ @@ -120,4 +138,13 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa $scope.smartInventory = function() { $state.go('inventories.addSmartInventory'); }; + + $scope.systemTracking = function(){ + var hostIds = _.map($scope.hostsSelected, (host) => host.id); + $state.go('systemTracking', { + inventoryId: $state.params.inventory_id, + hosts: $scope.$parent.hostsSelectedItems, + hostIds: hostIds + }); + }; }]; From c22e4a5023e84dc8c4feecb7a2c8593d032acbca Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 19 Apr 2017 16:42:39 -0700 Subject: [PATCH 25/49] Adding first pass at inventory sources CRUD workflow and breaking out sources info from groups and simplifying groups workflows to disclude source info --- .../groups/add/groups-add.controller.js | 187 +-------- .../groups/edit/groups-edit.controller.js | 201 +-------- .../src/inventories/groups/groups.form.js | 390 +++--------------- .../src/inventories/groups/groups.list.js | 13 - .../groups/list/groups-list.controller.js | 16 +- .../src/inventories/inventories.partial.html | 1 + .../client/src/inventories/inventory.form.js | 38 +- awx/ui/client/src/inventories/main.js | 2 + .../manage/inventory-manage.route.js | 141 ------- .../manage/inventory-manage.service.js | 60 --- .../add/build-sources-add-state.factory.js | 46 +++ .../src/inventories/sources/add/main.js | 13 + .../sources/add/sources-add.controller.js | 225 ++++++++++ .../edit/build-sources-edit-state.factory.js | 49 +++ .../src/inventories/sources/edit/main.js | 13 + .../sources/edit/sources-edit.controller.js | 248 +++++++++++ .../factories/get-hosts-status-msg.factory.js | 33 ++ .../get-source-type-options.factory.js | 37 ++ .../factories/get-sync-status-msg.factory.js | 77 ++++ .../factories/groups-cancel-update.factory.js | 81 ++++ .../factories/view-update-status.factory.js | 46 +++ .../list/build-sources-list-state.factory.js | 75 ++++ .../src/inventories/sources/list/main.js | 13 + .../sources/list/sources-list.controller.js | 213 ++++++++++ .../sources/list/sources-list.partial.html | 79 ++++ awx/ui/client/src/inventories/sources/main.js | 22 + .../src/inventories/sources/sources.form.js | 337 +++++++++++++++ .../src/inventories/sources/sources.list.js | 146 +++++++ .../inventories/sources/sources.service.js | 113 +++++ .../src/shared/stateDefinitions.factory.js | 17 +- 30 files changed, 1978 insertions(+), 954 deletions(-) delete mode 100644 awx/ui/client/src/inventories/manage/inventory-manage.route.js delete mode 100644 awx/ui/client/src/inventories/manage/inventory-manage.service.js create mode 100644 awx/ui/client/src/inventories/sources/add/build-sources-add-state.factory.js create mode 100644 awx/ui/client/src/inventories/sources/add/main.js create mode 100644 awx/ui/client/src/inventories/sources/add/sources-add.controller.js create mode 100644 awx/ui/client/src/inventories/sources/edit/build-sources-edit-state.factory.js create mode 100644 awx/ui/client/src/inventories/sources/edit/main.js create mode 100644 awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js create mode 100644 awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js create mode 100644 awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js create mode 100644 awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js create mode 100644 awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js create mode 100644 awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js create mode 100644 awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js create mode 100644 awx/ui/client/src/inventories/sources/list/main.js create mode 100644 awx/ui/client/src/inventories/sources/list/sources-list.controller.js create mode 100644 awx/ui/client/src/inventories/sources/list/sources-list.partial.html create mode 100644 awx/ui/client/src/inventories/sources/main.js create mode 100644 awx/ui/client/src/inventories/sources/sources.form.js create mode 100644 awx/ui/client/src/inventories/sources/sources.list.js create mode 100644 awx/ui/client/src/inventories/sources/sources.service.js 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 index 818e1890d2..7b995e7f6a 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -31,195 +31,32 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', field_id: 'group_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, source, json_data; + 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: inventoryData.id }; - if ($scope.source) { - // inventory_source fields - params = { - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], - source_script: $scope.inventory_script, - source: $scope.source.value, - 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, - // comma-delimited strings - group_by: _.map($scope.group_by, 'value').join(','), - source_regions: _.map($scope.source_regions, 'value').join(',') - }; - source = $scope.source.value; - } else { - source = null; - } - switch (source) { - // no inventory source set, just create a new group - // '' is the value supplied for Manual source type - case null || '': - GroupManageService.post(group).then(res => { - // associate - if ($stateParams.group) { - return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) - .then(() => $state.go('^', null, { reload: true })); - } else { - $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.post(group) - // associate to group - .then(res => { - if ($stateParams.group) { - GroupManageService.associateGroup(res.data, _.last($stateParams.group)); - return res; - } else { - return res; } - // pass the original POST response and not the association response - }) - .then(res => GroupManageService.putInventorySource( - // put the received group ID into inventory source payload - // and pass the related endpoint - _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) - .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); - break; - } + + GroupManageService.post(group).then(res => { + if ($stateParams.group) { + return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + .then(() => $state.go('^', null, { reload: true })); + } else { + $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { 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: '#group_source_regions', - multiple: true - }); - CreateSelect2({ - element: '#group_group_by', - multiple: true - }); - } - function initSourceSelect(){ - CreateSelect2({ - element: '#group_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/groups/edit/groups-edit.controller.js b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js index 1355d5d33f..27f444d69a 100644 --- a/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', - 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON, - ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData) { + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData) { init(); @@ -16,17 +16,8 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac .then(function(canAdd) { $scope.canAdd = canAdd; }); - // instantiate expected $scope values from inventorySourceData & groupData - _.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; - } - $scope = angular.extend($scope, groupData); - // display custom inventory_script name - if (inventorySourceData.source === 'custom') { - $scope.inventory_script_name = inventorySourceData.summary_fields.source_script.name; - } + $scope = angular.extend($scope, groupData); $scope.$watch('summary_fields.user_capabilities.edit', function(val) { $scope.canAdd = val; @@ -43,36 +34,14 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac variable: 'variables', }); - initSources(); } - var initRegionSelect = function() { - CreateSelect2({ - element: '#group_source_regions', - multiple: true - }); - CreateSelect2({ - element: '#group_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, source, json_data; + var json_data; json_data = ToJSON($scope.parseType, $scope.variables, true); // group fields var group = { @@ -82,166 +51,8 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbac inventory: $scope.inventory, id: groupData.id }; - if ($scope.source) { - // inventory_source fields - params = { - group: groupData.id, - source: $scope.source.value, - credential: $scope.credential, - overwrite: $scope.overwrite, - overwrite_vars: $scope.overwrite_vars, - source_script: $scope.inventory_script, - update_on_launch: $scope.update_on_launch, - update_cache_timeout: $scope.update_cache_timeout || 0, - // comma-delimited strings - group_by: _.map($scope.group_by, 'value').join(','), - source_regions: _.map($scope.source_regions, 'value').join(','), - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] - }; - source = $scope.source.value; - } else { - source = null; - } - switch (source) { - // no inventory source set, just create a new group - // '' is the value supplied for Manual source type - case null || '': - GroupManageService.put(group).then(() => $state.go($state.current, 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; - } + GroupManageService.put(group).then(() => $state.go($state.current, null, { reload: true })); }; - $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: '#group_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/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index bdf9df2353..5199e7bb34 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -11,329 +11,69 @@ */ export default { + 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.editGroup', + detailsClick: "$state.go('inventories.edit.groups.editGroup')", + well: false, + 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" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + tab: 'properties' + } + }, - - 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.editGroup', - detailsClick: "$state.go('inventories.edit.groups.editGroup')", - well: false, - 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" + - "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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:
\n" + - "
tag-key=TowerManaged
\n" + - "Limit to hosts using either key pair:
\n" + - "
key-name=staging, key-name=production
\n" + - "Limit to hosts where the Name tag begins with test:
\n" + - "
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:

    " + - "
  • Availability Zone: zones » us-east-1b
  • " + - "
  • Image ID: images » ami-b007ab1e
  • " + - "
  • Instance ID: instances » i-ca11ab1e
  • " + - "
  • Instance Type: types » type_m1_medium
  • " + - "
  • Key Name: keys » key_testing
  • " + - "
  • Region: regions » us-east-1
  • " + - "
  • Security Group: security_groups » security_group_default
  • " + - "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + - "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + - "
  • Tag None: tags » tag_none
  • " + - "

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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)' - } - } - }; + 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/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 0536161ab9..20baec28b1 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -17,19 +17,6 @@ export default { 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, diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index a9db830841..2889b745f9 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -50,13 +50,8 @@ group = {}; } - let group_status, hosts_status; + let hosts_status; - group_status = GetSyncStatusMsg({ - status: group.summary_fields.inventory_source.status, - has_inventory_sources: group.has_inventory_sources, - source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null ) - }); hosts_status = GetHostsStatusMsg({ active_failures: group.hosts_with_active_failures, total_hosts: group.total_hosts, @@ -64,15 +59,8 @@ group_id: group.id }); _.assign(group, - {status_class: group_status.class}, - {status_tooltip: group_status.tooltip}, - {launch_tooltip: group_status.launch_tip}, - {launch_class: group_status.launch_class}, - {group_schedule_tooltip: group_status.schedule_tip}, {hosts_status_tip: hosts_status.tooltip}, - {hosts_status_class: hosts_status.class}, - {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null}, - {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null}); + {hosts_status_class: hosts_status.class}); } $scope.groupSelect = function(id){ diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 875d23486b..032a4ec352 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,6 +1,7 @@
+
diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index 317800d0fb..b59b2ebe39 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -12,9 +12,11 @@ export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', 'buildHostListState', 'buildHostAddState', - 'buildHostEditState', + 'buildHostEditState', 'buildSourcesListState', 'buildSourcesAddState', + 'buildSourcesEditState', function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, - buildHostListState, buildHostAddState, buildHostEditState) { + buildHostListState, buildHostAddState, buildHostEditState, + buildSourcesListState, buildSourcesAddState,buildSourcesEditState) { return { addTitle: i18n._('NEW INVENTORY'), @@ -155,35 +157,13 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, }, inventory_sources: { name: 'inventory_sources', - // awToolTip: i18n._('Please save before assigning permissions'), - // dataPlacement: 'top', - basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', - type: 'collection', + include: "SourcesListDefinition", + includeForm: "SourcesFormDefinition", title: i18n._('Sources'), iterator: 'inventory_source', - 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' - } - } + listState: buildSourcesListState, + addState: buildSourcesAddState, + editState: buildSourcesEditState }, //this is a placeholder for when we're ready for completed jobs completed_jobs: { diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index c9de579972..74a3af0e1b 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -6,6 +6,7 @@ import host from './hosts/main'; import group from './groups/main'; +import sources from './sources/main'; import relatedHost from './related-hosts/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; @@ -19,6 +20,7 @@ export default angular.module('inventory', [ host.name, group.name, + sources.name, relatedHost.name, inventoryAdd.name, inventoryEdit.name, diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.route.js b/awx/ui/client/src/inventories/manage/inventory-manage.route.js deleted file mode 100644 index ca7589a3e1..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.route.js +++ /dev/null @@ -1,141 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; -import InventoriesManage from './inventory-manage.controller'; -import BreadcrumbsController from './breadcrumbs/breadcrumbs.controller'; -import HostsListController from './hosts/hosts-list.controller'; -import GroupsListController from './groups/groups-list.controller'; - -export default { - name: 'inventoryManage', - data: { - socket: { - "groups": { - "jobs": ["status_changed"] - } - } - }, - // instead of a single 'searchPrefix' attribute, provide hard-coded search params - url: '/inventories/:inventory_id/manage?{group:int}{group_search:queryset}{host_search:queryset}', - params: { - group: { - array: true - }, - group_search: { - value: { - page_size: '20', - page: '1', - order_by: 'name', - }, - squash: true, - dynamic: true - }, - host_search: { - value: { - page_size: '20', - page: '1', - order_by: 'name', - }, - squash: true, - dynamic: true - } - }, - ncyBreadcrumb: { - skip: true // Never display this state in ncy-breadcrumb. - }, - // enforce uniqueness in group param - onEnter: function($stateParams) { - $stateParams.group = _.uniq($stateParams.group); - }, - resolve: { - groupsUrl: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide this node's children - InventoryManageService.childGroupsUrl(_.last($stateParams.group)) : - // root context - provide root nodes - InventoryManageService.rootGroupsUrl($stateParams.inventory_id); - }], - hostsUrl: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide all hosts managed by nodes - InventoryManageService.childHostsUrl(_.last($stateParams.group)) : - // root context - provide all hosts in an inventory - InventoryManageService.rootHostsUrl($stateParams.inventory_id); - }], - inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data); - }], - breadCrumbData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) { - return $stateParams.group && $stateParams.group.length > 0 ? - // nested context - provide breadcrumb data - InventoryManageService.getBreadcrumbs($stateParams.group).then(res => res.data.results) : - // root context - false; - }], - groupsDataset: ['InventoryGroups', 'QuerySet', '$stateParams', 'groupsUrl', (list, qs, $stateParams, groupsUrl) => { - let path = groupsUrl; - return qs.search(path, $stateParams[`${list.iterator}_search`]); - }], - hostsDataset: ['InventoryHosts', 'QuerySet', '$stateParams', 'hostsUrl', (list, qs, $stateParams, hostsUrl) => { - let path = hostsUrl; - return qs.search(path, $stateParams[`${list.iterator}_search`]); - }] - }, - views: { - // target the ui-view with name "groupBreadcrumbs" at the root view - 'groupBreadcrumbs@': { - controller: BreadcrumbsController, - templateUrl: templateUrl('inventories/manage/breadcrumbs/breadcrumbs') - }, - // target the un-named ui-view @ root level - '@': { - templateUrl: templateUrl('inventories/manage/inventory-manage'), - controller: InventoriesManage - }, - // target ui-views with name@inventoryManage state - 'groupsList@inventoryManage': { - templateProvider: function(InventoryGroups, generateList, $templateRequest, $stateParams, GetBasePath) { - let list = _.cloneDeep(InventoryGroups); - 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' - }); - html = generateList.wrapPanel(html); - // Include the custom group delete modal template - return $templateRequest(templateUrl('inventories/manage/groups/groups-list')).then((template) => { - return html.concat(template); - }); - }, - controller: GroupsListController - }, - 'hostsList@inventoryManage': { - templateProvider: function(InventoryHosts, generateList, $stateParams, GetBasePath) { - let list = _.cloneDeep(InventoryHosts); - if($stateParams && $stateParams.group) { - list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/all_hosts'; - } - else { - //reaches here if the user is on the root level group - list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts'; - } - let html = generateList.build({ - list: list, - mode: 'edit' - }); - return generateList.wrapPanel(html); - }, - controller: HostsListController - } - } -}; diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.service.js b/awx/ui/client/src/inventories/manage/inventory-manage.service.js deleted file mode 100644 index 710c494aeb..0000000000 --- a/awx/ui/client/src/inventories/manage/inventory-manage.service.js +++ /dev/null @@ -1,60 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){ - return { - // 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; - }, - // data getters - getInventory: function(id){ - Wait('start'); - this.url = GetBasePath('inventory') + id; - Rest.setUrl(this.url); - return Rest.get() - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - getBreadcrumbs: function(groups){ - Wait('start'); - this.url = GetBasePath('groups') + '?' + _.map(groups, function(item){ - return '&or__id=' + item; - }).join(''); - Rest.setUrl(this.url); - return Rest.get() - .success(this.success.bind(this)) - .error(this.error.bind(this)) - .finally(Wait('stop')); - }, - rootHostsUrl: function(id){ - var url = GetBasePath('inventory') + id + '/hosts'; - return url; - }, - childHostsUrl: function(id){ - var url = GetBasePath('groups') + id + '/all_hosts'; - return url; - }, - childGroupsUrl: function(id){ - var url = GetBasePath('groups') + id + '/children'; - return url; - }, - rootGroupsUrl: function(id){ - var url = GetBasePath('inventory') + id+ '/root_groups'; - return url; - } - }; - }]; 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..ecfe09940f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -0,0 +1,225 @@ +/************************************************* + * 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, source, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + if ($scope.source) { + params = { + name: $scope.name, + description: $scope.description, + inventory: inventoryData.id, + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], + source_script: $scope.inventory_script, + source: $scope.source.value, + 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(',') + }; + source = $scope.source.value; + } else { + source = null; + } + // switch (source) { + // // no inventory source set, just create a new group + // // '' is the value supplied for Manual source type + // case null || '': + // GroupManageService.post(group).then(res => { + // // associate + // if ($stateParams.group) { + // return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + // .then(() => $state.go('^', null, { reload: true })); + // } else { + // $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.post(group) + // // associate to group + // .then(res => { + // if ($stateParams.group) { + // GroupManageService.associateGroup(res.data, _.last($stateParams.group)); + // return res; + // } else { + // return res; } + // // pass the original POST response and not the association response + // }) + // .then(res => GroupManageService.putInventorySource( + // // put the received group ID into inventory source payload + // // and pass the related endpoint + // _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) + // .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); + SourcesService.post({params}).then(function(){ + $state.go('.', null, {reload: true}); + }); + // break; + // } + }; + $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..29b988f349 --- /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/: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.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..978be62478 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -0,0 +1,248 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$state', '$stateParams', '$scope', 'ParseVariableString', + 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'inventorySourceData', 'SourcesService', + function($state, $stateParams, $scope, ParseVariableString, + rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, + GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, + inventorySourceData, SourcesService) { + + 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, source, json_data; + json_data = ToJSON($scope.parseType, $scope.variables, true); + + if ($scope.source) { + // inventory_source fields + params = { + id: $scope.id, + variables: json_data, + name: $scope.name, + description: $scope.description, + inventory: $scope.inventory, + source: $scope.source.value, + credential: $scope.credential, + overwrite: $scope.overwrite, + overwrite_vars: $scope.overwrite_vars, + source_script: $scope.inventory_script, + update_on_launch: $scope.update_on_launch, + update_cache_timeout: $scope.update_cache_timeout || 0, + // comma-delimited strings + group_by: _.map($scope.group_by, 'value').join(','), + source_regions: _.map($scope.source_regions, 'value').join(','), + instance_filters: $scope.instance_filters, + source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] + }; + source = $scope.source.value; + } else { + 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($state.current, 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/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js new file mode 100644 index 0000000000..19a846c414 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js @@ -0,0 +1,33 @@ +export default + function GetHostsStatusMsg() { + return function(params) { + var active_failures = params.active_failures, + total_hosts = params.total_hosts, + tip, failures, html_class; + + // Return values for use on host status indicator + + if (active_failures > 0) { + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; + html_class = 'error'; + failures = true; + } else { + failures = false; + if (total_hosts === 0) { + // no hosts + tip = "Contains 0 hosts."; + html_class = 'none'; + } else { + // many hosts with 0 failures + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; + html_class = 'success'; + } + } + + return { + tooltip: tip, + failures: failures, + 'class': html_class + }; + }; + } diff --git a/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js new file mode 100644 index 0000000000..befef8a499 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-source-type-options.factory.js @@ -0,0 +1,37 @@ +export default + function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + variable = params.variable; + + if (scope[variable] === undefined) { + scope[variable] = []; + Rest.setUrl(GetBasePath('inventory_sources')); + Rest.options() + .success(function (data) { + var i, choices = data.actions.GET.source.choices; + for (i = 0; i < choices.length; i++) { + if (choices[i][0] !== 'file') { + scope[variable].push({ + label: choices[i][1], + value: choices[i][0] + }); + } + } + scope.cloudCredentialRequired = false; + scope.$emit('sourceTypeOptionsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status + }); + }); + } + }; + } + +GetSourceTypeOptions.$inject = + [ 'Rest', + 'ProcessErrors', + 'GetBasePath' + ]; diff --git a/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js new file mode 100644 index 0000000000..2541abcc27 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js @@ -0,0 +1,77 @@ +export default + function GetSyncStatusMsg(Empty) { + 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', + stat, stat_class, status_tip; + + stat = status; + stat_class = stat; + + switch (status) { + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case 'ok': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'canceled': + status_tip = 'Sync canceled. Click to view log.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'pending': + status_tip = 'Sync pending.'; + launch_class = "btn-disabled"; + launch_tip = "Sync pending"; + break; + case 'updating': + case 'running': + launch_class = "btn-disabled"; + launch_tip = "Sync running"; + status_tip = "Sync running. Click to view log."; + 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, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip, + "schedule_tip": schedule_tip + }; + }; + } + +GetSyncStatusMsg.$inject = + [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js new file mode 100644 index 0000000000..1447d0aa1c --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js @@ -0,0 +1,81 @@ +export default + function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + group = params.group; + + if (scope.removeCancelUpdate) { + scope.removeCancelUpdate(); + } + scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { + // 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 + }); + }); + }); + + if (scope.removeCheckCancel) { + scope.removeCheckCancel(); + } + scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { + // Check that we have access to cancelling an update + var url = (current_update) ? current_update : last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + scope.$emit('CancelUpdate', url); + //} else { + // Wait('stop'); + // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + + // 'the latest status.', 'alert-info'); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }); + + // Cancel the update process + if (Empty(group)) { + group = Find({ list: scope.groups, key: 'id', val: id }); + scope.selected_group_id = group.id; + } + + if (group && (group.status === 'running' || group.status === 'pending')) { + // We found the group, and there is a running update + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status + }); + }); + } + }; + } + +GroupsCancelUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js new file mode 100644 index 0000000000..1f3280b51c --- /dev/null +++ b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js @@ -0,0 +1,46 @@ +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 }); + + 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") { + Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + + 'clicking the button.
', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('SourceReady', data); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' GET returned status: ' + status }); + }); + } + } + }; + } + +ViewUpdateStatus.$inject = + [ '$state', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Empty', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js new file mode 100644 index 0000000000..eb124f2842 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js @@ -0,0 +1,75 @@ +/************************************************* +* Copyright (c) 2017 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ +import SourcesListController from './sources-list.controller'; +export default ['SourcesListDefinition', '$stateExtender', 'templateUrl', '$injector', + function(SourcesListDefinition, $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(SourcesListDefinition, generateList) { + let list = _.cloneDeep(SourcesListDefinition); + 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: SourcesListController + } + }, + 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/sources/list/main.js b/awx/ui/client/src/inventories/sources/list/main.js new file mode 100644 index 0000000000..c33538ab1f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import buildSourcesListState from './build-sources-list-state.factory'; +import controller from './sources-list.controller'; + +export default + angular.module('sourcesList', []) + .factory('buildSourcesListState', buildSourcesListState) + .controller('SourcesListController', controller); diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js new file mode 100644 index 0000000000..21c9003f5b --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -0,0 +1,213 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'SourcesListDefinition', + 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', + 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', + 'inventoryData', '$filter', 'Prompt', 'Wait', 'SourcesService', + function($scope, $rootScope, $state, $stateParams, SourcesListDefinition, + InventoryUpdate, GroupManageService, GroupsCancelUpdate, + ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData, $filter, Prompt, + Wait, SourcesService){ + + let list = SourcesListDefinition; + + 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 === "inventoryManage.editGroup") { + $scope.rowBeingEdited = $state.params.group_id; + $scope.listBeingEdited = "groups"; + } + + $scope.inventory_id = $stateParams.inventory_id; + _.forEach($scope[list.name], buildStatusIndicators); + + } + + function buildStatusIndicators(inventory_source){ + if (inventory_source === undefined || inventory_source === null) { + inventory_source = {}; + } + + let inventory_source_status, hosts_status; + + inventory_source_status = GetSyncStatusMsg({ + status: inventory_source.status, + has_inventory_sources: inventory_source.has_inventory_sources, + source: ( (inventory_source) ? inventory_source.source : null ) + }); + hosts_status = GetHostsStatusMsg({ + active_failures: inventory_source.hosts_with_active_failures, + total_hosts: inventory_source.total_hosts, + inventory_id: $scope.inventory_id, + // group_id: group.id + }); + _.assign(inventory_source, + {status_class: inventory_source_status.class}, + {status_tooltip: inventory_source_status.tooltip}, + {launch_tooltip: inventory_source_status.launch_tip}, + {launch_class: inventory_source_status.launch_class}, + {group_schedule_tooltip: inventory_source_status.schedule_tip}, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}, + {source: inventory_source ? inventory_source.source : null}, + {status: inventory_source ? inventory_source.status : null}); + } + + $scope.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $scope.createSource = function(){ + $state.go('inventories.edit.inventory_sources.add'); + }; + $scope.editSource = function(id){ + $state.go('inventories.edit.inventory_sources.edit', {source_id: id}); + }; + $scope.deleteSource = function(inventory_source){ + var body = '
Are you sure you want to permanently delete the inventory source below from the inventory?
' + $filter('sanitize')(inventory_source.name) + '
'; + var action = function(){ + delete $rootScope.promptActionBtnClass; + Wait('start'); + SourcesService.delete(inventory_source.id).then(() => { + $('#prompt-modal').modal('hide'); + // if (parseInt($state.params.source_id) === id) { + // $state.go("sources", null, {reload: true}); + // } else { + $state.go($state.current.name, null, {reload: true}); + // } + Wait('stop'); + }); + }; + // Prompt depends on having $rootScope.promptActionBtnClass available... + Prompt({ + hdr: 'Delete Source', + body: body, + action: action, + actionText: 'DELETE', + }); + $rootScope.promptActionBtnClass = 'Modal-errorButton'; + }; + + $scope.updateSource = function(inventory_source) { + InventoryUpdate({ + scope: $scope, + url: inventory_source.related.update + }); + }; + + $scope.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + 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; + // _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $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(); + }); + + }]; diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.partial.html b/awx/ui/client/src/inventories/sources/list/sources-list.partial.html new file mode 100644 index 0000000000..1a02f3a515 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/list/sources-list.partial.html @@ -0,0 +1,79 @@ + diff --git a/awx/ui/client/src/inventories/sources/main.js b/awx/ui/client/src/inventories/sources/main.js new file mode 100644 index 0000000000..bb515628b7 --- /dev/null +++ b/awx/ui/client/src/inventories/sources/main.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import sourcesList from './list/main'; +import sourcesAdd from './add/main'; +import sourcesEdit from './edit/main'; +import sourcesFormDefinition from './sources.form'; +import sourcesListDefinition from './sources.list'; +import service from './sources.service'; + +export default + angular.module('sources', [ + sourcesList.name, + sourcesAdd.name, + sourcesEdit.name + ]) + .value('SourcesFormDefinition', sourcesFormDefinition) + .value('SourcesListDefinition', sourcesListDefinition) + .service('SourcesService', service); diff --git a/awx/ui/client/src/inventories/sources/sources.form.js b/awx/ui/client/src/inventories/sources/sources.form.js new file mode 100644 index 0000000000..32968fc3db --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.form.js @@ -0,0 +1,337 @@ +/************************************************* + * 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 { + addTitle: 'CREATE SOURCE', + editTitle: '{{ name }}', + showTitle: true, + name: 'inventory_source', + basePath: 'inventory_sources', + parent: 'inventories.edit.sources', + // 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.editGroup', + detailsClick: "$state.go('inventories.edit.inventory_sources.edit')", + well: false, + 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" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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:
\n" + + "
tag-key=TowerManaged
\n" + + "Limit to hosts using either key pair:
\n" + + "
key-name=staging, key-name=production
\n" + + "Limit to hosts where the Name tag begins with test:
\n" + + "
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:

    " + + "
  • Availability Zone: zones » us-east-1b
  • " + + "
  • Image ID: images » ami-b007ab1e
  • " + + "
  • Instance ID: instances » i-ca11ab1e
  • " + + "
  • Instance Type: types » type_m1_medium
  • " + + "
  • Key Name: keys » key_testing
  • " + + "
  • Region: regions » us-east-1
  • " + + "
  • Security Group: security_groups » security_group_default
  • " + + "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + + "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + + "
  • Tag None: tags » tag_none
  • " + + "

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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..cfd714072f --- /dev/null +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -0,0 +1,146 @@ +/************************************************* + * 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, + multiSelect: 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' + }, + // 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", + // 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: "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/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index b131ff4c95..785a1414e7 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -246,6 +246,7 @@ function($injector, $stateExtender, $log, i18n) { * @returns {array} Array of state definitions [{...}, {...}, ...] */ generateFormListDefinitions: function(form, formStateDefinition, params) { + var that = this; function buildRbacUserTeamDirective(){ let states = []; @@ -559,11 +560,23 @@ function($injector, $stateExtender, $log, i18n) { states = _.flatten(states); } if(field && field.addState){ - states.push(field.addState(field, formStateDefinition, params)); + let formState = field.addState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any add-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + } states = _.flatten(states); } if(field && field.editState){ - states.push(field.editState(field, formStateDefinition, params)); + let formState = field.editState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any edit-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + } states = _.flatten(states); } } From e58e5931403c05f8c2315e36dea5952b415ebf1d Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 19 Apr 2017 16:59:54 -0400 Subject: [PATCH 26/49] First pass implementation of fact searching on hosts --- .../client/src/inventories/hosts/host.list.js | 4 ++++ .../list-generator/list-generator.factory.js | 4 ++++ .../shared/smart-search/queryset.service.js | 20 +++++++++++++++++-- .../smart-search/smart-search.controller.js | 16 ++++++++------- .../smart-search/smart-search.directive.js | 4 +++- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/inventories/hosts/host.list.js b/awx/ui/client/src/inventories/hosts/host.list.js index 34d5056d42..8118431df5 100644 --- a/awx/ui/client/src/inventories/hosts/host.list.js +++ b/awx/ui/client/src/inventories/hosts/host.list.js @@ -10,6 +10,10 @@ export default ['i18n', function(i18n) { 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, diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 78eb66f5b7..dea1d2383c 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -198,11 +198,15 @@ export default ['$compile', 'Attr', 'Icon', list.searchSize = 'col-lg-7 col-md-12 col-sm-12 col-xs-12'; } if (options.showSearch === undefined || options.showSearch === true) { + let nonstandardSearchParam = list.nonstandardSearchParam && list.nonstandardSearchParam.param ? list.nonstandardSearchParam.param : undefined; + let nonstandardSearchParamRoot = list.nonstandardSearchParam && list.nonstandardSearchParam.root ? list.nonstandardSearchParam.root : undefined; html += `
Date: Thu, 20 Apr 2017 12:34:47 -0700 Subject: [PATCH 27/49] fixing sources-add function the params were being sent as a nested function. --- .../src/inventories/sources/add/sources-add.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ecfe09940f..32dc70a7e6 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -108,7 +108,7 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', // // and pass the related endpoint // _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) // .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); - SourcesService.post({params}).then(function(){ + SourcesService.post(params).then(function(){ $state.go('.', null, {reload: true}); }); // break; From 78ff5f5301dbbb648474875e01846beef8a72c7e Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 20 Apr 2017 15:39:31 -0400 Subject: [PATCH 28/49] Removed scm inventory from add inventory dropdown --- awx/ui/client/src/inventories/inventory.list.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/awx/ui/client/src/inventories/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index 240639b4db..da0c21351f 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -79,12 +79,6 @@ export default ['i18n', function(i18n) { optionSref: 'inventories.addSmartInventory', //TODO: this should have its own permission ngShow: 'canAddInventory' - }, - { - optionContent: i18n._('SCM Inventory'), - optionSref: 'inventories.addSCMInventory', - //TODO: this should have its own permission - ngShow: 'canAddInventory' } ], ngShow: 'canAddInventory || canAddSmartInventory || canAddSCMInventory' From 0fa9aa6bcb02d695a66e11c4096cda3814c3174b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 20 Apr 2017 22:54:43 -0700 Subject: [PATCH 29/49] Adding nested-groups (related tab) and completed jobs to inventories --- ...-inventory-completed-jobs-state.factory.js | 80 +++++++ .../completed_jobs/completed_jobs.list.js | 88 +++++++ .../src/inventories/completed_jobs/main.js | 13 + .../src/inventories/groups/groups.form.js | 144 ++++++----- .../src/inventories/groups/groups.list.js | 10 +- awx/ui/client/src/inventories/groups/main.js | 6 +- .../inventories/groups/nested-groups/main.js | 17 ++ .../nested-groups-list.controller.js | 224 ++++++++++++++++++ .../nested-groups/nested-groups.form.js | 98 ++++++++ .../nested-groups/nested-groups.list.js | 146 ++++++++++++ .../client/src/inventories/inventory.form.js | 59 ++--- awx/ui/client/src/inventories/main.js | 2 + .../src/shared/stateDefinitions.factory.js | 2 + 13 files changed, 779 insertions(+), 110 deletions(-) create mode 100644 awx/ui/client/src/inventories/completed_jobs/build-inventory-completed-jobs-state.factory.js create mode 100644 awx/ui/client/src/inventories/completed_jobs/completed_jobs.list.js create mode 100644 awx/ui/client/src/inventories/completed_jobs/main.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/main.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js 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/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index 5199e7bb34..c8655fa4e6 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -10,70 +10,88 @@ * @description This form is for adding/editing a Group on the inventory page */ -export default { - 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.editGroup', - detailsClick: "$state.go('inventories.edit.groups.editGroup')", - well: false, - fields: { - name: { - label: 'Name', - type: 'text', - ngDisabled: '!(group_obj.summary_fields.user_capabilities.edit || canAdd)', - required: true, - tab: 'properties' +export default ['i18n', 'nestedGroupListState', +function(i18n, nestedGroupListState){ + 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" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

View JSON examples at www.json.org

' + + '

View YAML examples at docs.ansible.com

', + dataContainer: 'body', + 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" + - "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

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)' + 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)' + } }, - 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, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState + }, + } - } -}; + }; +}]; diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 20baec28b1..ed95f5a394 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -31,17 +31,9 @@ export default { 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'}}" } }, diff --git a/awx/ui/client/src/inventories/groups/main.js b/awx/ui/client/src/inventories/groups/main.js index 9461571a71..b189cae4a2 100644 --- a/awx/ui/client/src/inventories/groups/main.js +++ b/awx/ui/client/src/inventories/groups/main.js @@ -7,6 +7,7 @@ import groupList from './list/main'; import groupAdd from './add/main'; import groupEdit from './edit/main'; +import nestedGroups from './nested-groups/main'; import groupFormDefinition from './groups.form'; import groupListDefinition from './groups.list'; import service from './groups.service'; @@ -20,9 +21,10 @@ export default angular.module('group', [ groupList.name, groupAdd.name, - groupEdit.name + groupEdit.name, + nestedGroups.name ]) - .value('GroupForm', groupFormDefinition) + .factory('GroupForm', groupFormDefinition) .value('GroupList', groupListDefinition) .factory('GetHostsStatusMsg', GetHostsStatusMsg) .factory('GetSourceTypeOptions', GetSourceTypeOptions) 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..4d322b4270 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/main.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import nestedGroupListState from './nested-groups-list-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) + .value('NestedGroupListDefinition', nestedGroupListDefinition) + .factory('NestedGroupFormDefinition', nestedGroupFormDefinition) + .controller('NestedGroupsListController', controller); 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..67007647b5 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -0,0 +1,224 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'NestedGroupListDefinition', 'InventoryUpdate', + 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', + 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + function($scope, $rootScope, $state, $stateParams, NestedGroupListDefinition, InventoryUpdate, + GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, + GetSyncStatusMsg, 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 === "inventoryManage.editGroup") { + $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.groupSelect = function(id){ + var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); + $state.go('inventoryManage', { + inventory_id: $stateParams.inventory_id, + group: group, + group_search: { + page_size: '20', + page: '1', + order_by: 'name', + } + }, {reload: true}); + }; + $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('inventoryManage', 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("inventoryManage", 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("inventoryManage", 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.$on(`ws-jobs`, function(e, data){ + var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); + + if (group === undefined || group === null) { + group = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path; + if($stateParams && $stateParams.group && $stateParams.group.length > 0) { + path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; + } + else { + //reaches here if the user is on the root level group + path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + } + 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; + _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + group.status = data.status; + group.status_class = status.class; + group.status_tooltip = status.tooltip; + group.launch_tooltip = status.launch_tip; + group.launch_class = status.launch_class; + } + }); + + $scope.cancelUpdate = function (id) { + GroupsCancelUpdate({ scope: $scope, id: id }); + }; + $scope.viewUpdateStatus = function (id) { + ViewUpdateStatus({ + scope: $scope, + group_id: id + }); + }; + $scope.showFailedHosts = function() { + $state.go('inventoryManage', {failed: true}, {reload: true}); + }; + $scope.scheduleGroup = function(id) { + // Add this group's id to the array of group id's so that it gets + // added to the breadcrumb trail + var groupsArr = $stateParams.group ? $stateParams.group : []; + groupsArr.push(id); + $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options + $scope.$on('multiSelectList.selectionChanged', (event, selection) => { + $scope.$parent.groupsSelected = selection.length > 0 ? true : false; + $scope.$parent.groupsSelectedItems = selection.selectedItems; + }); + + $scope.copyMoveGroup = function(id){ + $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + }; + + var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { + if (toState.name === "inventoryManage.editGroup") { + $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(); + }); + + }]; 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..9d4645018d --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.form.js @@ -0,0 +1,98 @@ +/************************************************* + * 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', + // 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" + + "
{
  \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n" + + '

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, + // addState: buildGroupsAddState, + // editState: buildGroupsEditState + }, + + } + }; +}]; 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..1e28c83454 --- /dev/null +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -0,0 +1,146 @@ +/************************************************* + * 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: "{{ 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', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + 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", + 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(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(group.id)", + awToolTip: 'Edit group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.edit" + }, + view: { + //label: 'Edit', + mode: 'all', + ngClick: "editGroup(group.id)", + awToolTip: 'View group', + dataPlacement: "top", + ngShow: "!group.summary_fields.user_capabilities.edit" + }, + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "deleteGroup(group)", + awToolTip: 'Delete group', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index b59b2ebe39..c79be281f4 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -13,10 +13,29 @@ export default ['i18n', 'buildGroupsListState', 'buildGroupsAddState', 'buildGroupsEditState', 'buildHostListState', 'buildHostAddState', 'buildHostEditState', 'buildSourcesListState', 'buildSourcesAddState', - 'buildSourcesEditState', + 'buildSourcesEditState', 'buildInventoryCompletedJobsState', + 'InventoryCompletedJobsList', function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, buildHostListState, buildHostAddState, buildHostEditState, - buildSourcesListState, buildSourcesAddState,buildSourcesEditState) { + buildSourcesListState, buildSourcesAddState,buildSourcesEditState, + buildInventoryCompletedJobsState, InventoryCompletedJobsList) { + + var completed_jobs_object = { + name: 'completed_jobs', + index: false, + basePath: "unified_jobs", + include: "InventoryCompletedJobsList", + title: i18n._('Completed Jobs'), + iterator: 'completed_job', + generateList: true, + listState: buildInventoryCompletedJobsState, + search: { + "or__job__inventory": '' + } + }; + let clone = _.clone(InventoryCompletedJobsList); + completed_jobs_object = angular.extend(clone, completed_jobs_object); + return { addTitle: i18n._('NEW INVENTORY'), @@ -96,7 +115,7 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, name: 'permissions', awToolTip: i18n._('Please save before assigning permissions'), dataPlacement: 'top', - basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/access_list/', + basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/access_list/', type: 'collection', title: i18n._('Permissions'), iterator: 'permission', @@ -165,39 +184,7 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, addState: buildSourcesAddState, editState: buildSourcesEditState }, - //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' - } - } - } + completed_jobs: completed_jobs_object } };}]; diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 74a3af0e1b..0d9c4c3f84 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -8,6 +8,7 @@ import host from './hosts/main'; import group from './groups/main'; import sources from './sources/main'; import relatedHost from './related-hosts/main'; +import inventoryCompletedJobs from './completed_jobs/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; @@ -22,6 +23,7 @@ angular.module('inventory', [ group.name, sources.name, relatedHost.name, + inventoryCompletedJobs.name, inventoryAdd.name, inventoryEdit.name, inventoryList.name diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 785a1414e7..9b9ce3dbf5 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -576,6 +576,7 @@ function($injector, $stateExtender, $log, i18n) { if(field.includeForm){ let form = field.includeForm ? $injector.get(field.includeForm) : field; states.push(that.generateLookupNodes(form, formState)); + states.push(that.generateFormListDefinitions(form, formState, params)); } states = _.flatten(states); } @@ -678,6 +679,7 @@ function($injector, $stateExtender, $log, i18n) { if (field.search) { state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); } + return state; } return _(form.related).map(buildListNodes).flatten().value(); From 8734ad738fce3d4958bcc8a651f79b68a53d91d4 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 24 Apr 2017 11:36:46 -0400 Subject: [PATCH 30/49] Implement adhoc commands for normal inventories --- awx/ui/client/legacy-styles/lists.less | 7 +- .../src/inventories/adhoc/adhoc.controller.js | 308 ++++++++++++++++++ .../src/inventories/adhoc/adhoc.form.js | 159 +++++++++ .../src/inventories/adhoc/adhoc.partial.html | 1 + .../src/inventories/adhoc/adhoc.route.js | 28 ++ awx/ui/client/src/inventories/adhoc/main.js | 7 + .../src/inventories/groups/groups.list.js | 5 +- .../groups/list/groups-list.controller.js | 44 ++- .../nested-groups-list.controller.js | 9 + .../nested-groups/nested-groups.list.js | 5 +- .../src/inventories/inventories.partial.html | 1 + awx/ui/client/src/inventories/main.js | 61 +++- .../list/host-list.controller.js | 11 +- .../related-hosts/related-host.list.js | 13 + 14 files changed, 638 insertions(+), 21 deletions(-) create mode 100644 awx/ui/client/src/inventories/adhoc/adhoc.controller.js create mode 100644 awx/ui/client/src/inventories/adhoc/adhoc.form.js create mode 100644 awx/ui/client/src/inventories/adhoc/adhoc.partial.html create mode 100644 awx/ui/client/src/inventories/adhoc/adhoc.route.js create mode 100644 awx/ui/client/src/inventories/adhoc/main.js diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 5580df4119..96b24aeb69 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -203,7 +203,7 @@ table, tbody { .List-buttonDefault { background-color: @btn-bg; color: @btn-txt; - border-color: @btn-bord; + border-color: @b7grey; } .List-buttonDefault:hover, @@ -212,6 +212,11 @@ table, tbody { color: @btn-txt; } +.List-buttonDefault[disabled] { + color: @d7grey; + border-color: @d7grey; +} + .List-searchDropdown { border-top-left-radius: 5px!important; border-bottom-left-radius: 5px!important; diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js new file mode 100644 index 0000000000..4b8e25c051 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js @@ -0,0 +1,308 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Adhoc + * @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran. +*/ +function adhocController($q, $scope, $stateParams, + $state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm, + GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, + KindChange, Wait, ParseTypeChange) { + + ClearScope(); + + // this is done so that we can access private functions for testing, but + // we don't want to populate the "public" scope with these internal + // functions + var privateFn = {}; + this.privateFn = privateFn; + + var id = $stateParams.inventory_id, + hostPattern = $stateParams.pattern; + + // note: put any urls that the controller will use in here!!!! + privateFn.setAvailableUrls = function() { + return { + adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/', + inventoryUrl: GetBasePath('inventory') + id + '/', + machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh' + }; + }; + + var urls = privateFn.setAvailableUrls(); + + // set the default options for the selects of the adhoc form + privateFn.setFieldDefaults = function(verbosity_options, forks_default) { + var verbosity; + for (verbosity in verbosity_options) { + if (verbosity_options[verbosity].isDefault) { + $scope.verbosity = verbosity_options[verbosity]; + } + } + $("#forks-number").spinner("value", forks_default); + $scope.forks = forks_default; + }; + + // set when "working" starts and stops + privateFn.setLoadingStartStop = function() { + var asyncHelper = {}, + formReadyPromise = 0; + + Wait('start'); + + if (asyncHelper.removeChoicesReady) { + asyncHelper.removeChoicesReady(); + } + asyncHelper.removeChoicesReady = $scope.$on('adhocFormReady', + isFormDone); + + // check to see if all requests have completed + function isFormDone() { + formReadyPromise++; + + if (formReadyPromise === 2) { + privateFn.setFieldDefaults($scope.adhoc_verbosity_options, + $scope.forks_field.default); + + CreateSelect2({ + element: '#adhoc_module_name', + multiple: false + }); + + CreateSelect2({ + element: '#adhoc_verbosity', + multiple: false + }); + + Wait('stop'); + } + } + }; + + // set the arguments help to watch on change of the module + privateFn.instantiateArgumentHelp = function() { + $scope.$watch('module_name', function(val) { + if (val) { + // give the docs for the selected module in the popover + $scope.argsPopOver = '

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

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

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

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

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

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

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

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

', + dataTitle: 'Limit', + dataPlacement: 'right', + dataContainer: 'body' + }, + credential: { + label: 'Machine Credential', + type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + sourceModel: 'credential', + sourceField: 'name', + class: 'squeeze', + awPopOver: '

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

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

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

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

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

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

', + dataTitle: 'Forks', + dataPlacement: 'right', + dataContainer: "body" + }, + extra_vars: { + label: i18n._('Extra Variables'), + type: 'textarea', + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + rows: 6, + "default": "---", + column: 2, + awPopOver: "

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

" + + "JSON:
\n" + + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + + "YAML:
\n" + + "
---
somevar: somevalue
password: magic
\n", + dataTitle: i18n._('Extra Variables'), + dataPlacement: 'right', + dataContainer: "body" + } + }, + buttons: { + reset: { + ngClick: 'formReset()', + ngDisabled: true, + label: 'Reset', + 'class': 'btn btn-sm Form-cancelButton' + }, + launch: { + label: 'Save', + ngClick: 'launchJob()', + ngDisabled: true, + 'class': 'btn btn-sm List-buttonSubmit launchButton' + } + }, + + related: {} + }; +}]; diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.partial.html b/awx/ui/client/src/inventories/adhoc/adhoc.partial.html new file mode 100644 index 0000000000..7d2a014836 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.partial.html @@ -0,0 +1 @@ +
diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.route.js b/awx/ui/client/src/inventories/adhoc/adhoc.route.js new file mode 100644 index 0000000000..392de3965e --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/adhoc.route.js @@ -0,0 +1,28 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import {templateUrl} from '../../shared/template-url/template-url.factory'; + import { N_ } from '../../i18n'; + +export default { + url: '/adhoc', + params:{ + pattern: { + value: 'all', + squash: true + } + }, + name: 'inventories.edit.adhoc', + views: { + 'adhocForm@inventories': { + templateUrl: templateUrl('inventories/adhoc/adhoc'), + controller: 'adhocController' + } + }, + ncyBreadcrumb: { + label: N_("RUN COMMAND") + } +}; diff --git a/awx/ui/client/src/inventories/adhoc/main.js b/awx/ui/client/src/inventories/adhoc/main.js new file mode 100644 index 0000000000..1931e8f1d1 --- /dev/null +++ b/awx/ui/client/src/inventories/adhoc/main.js @@ -0,0 +1,7 @@ +import adhocController from './adhoc.controller'; +import form from './adhoc.form'; + +export default + angular.module('adhoc', []) + .controller('adhocController', adhocController) + .factory('adhocForm', form); diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index ed95f5a394..76c847c01b 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -48,9 +48,8 @@ export default { }, launch: { mode: 'all', - // $scope.$parent is governed by InventoryManageController, - ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', - ngClick: '$parent.setAdhocPattern()', + ngDisabled: '!groupsSelected', + ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", dataTipWatch: "adhocCommandTooltip", actionClass: 'btn List-buttonDefault', diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 2889b745f9..003e9ef65c 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -16,14 +16,14 @@ init(); function init(){ - $scope.inventory_id = $stateParams.inventory_id; - $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; - $scope.canAdd = false; + $scope.inventory_id = $stateParams.inventory_id; + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + $scope.canAdd = false; - rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); // Search init $scope.list = list; @@ -43,6 +43,22 @@ $scope.inventory_id = $stateParams.inventory_id; _.forEach($scope[list.name], buildStatusIndicators); + $scope.$on('selectedOrDeselected', function(e, value) { + let item = value.value; + + if (value.isSelected) { + if(!$scope.groupsSelected) { + $scope.groupsSelected = []; + } + $scope.groupsSelected.push(item); + } else { + _.remove($scope.groupsSelected, { id: item.id }); + if($scope.groupsSelected.length === 0) { + $scope.groupsSelected = null; + } + } + }); + } function buildStatusIndicators(group){ @@ -195,11 +211,6 @@ groupsArr.push(id); $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); }; - // $scope.$parent governed by InventoryManageController, for unified multiSelect options - $scope.$on('multiSelectList.selectionChanged', (event, selection) => { - $scope.$parent.groupsSelected = selection.length > 0 ? true : false; - $scope.$parent.groupsSelectedItems = selection.selectedItems; - }); $scope.copyMoveGroup = function(id){ $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); @@ -221,4 +232,13 @@ cleanUpStateChangeListener(); }); + $scope.setAdhocPattern = function(){ + var pattern = _($scope.groupsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 67007647b5..7f7425ee84 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -221,4 +221,13 @@ cleanUpStateChangeListener(); }); + $scope.setAdhocPattern = function(){ + var pattern = _($scope.groupsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; + }]; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js index 1e28c83454..ba958b0175 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -50,9 +50,8 @@ export default { }, launch: { mode: 'all', - // $scope.$parent is governed by InventoryManageController, - ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected', - ngClick: '$parent.setAdhocPattern()', + ngDisabled: '!groupsSelected', + ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", dataTipWatch: "adhocCommandTooltip", actionClass: 'btn List-buttonDefault', diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 032a4ec352..767cd197fa 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,4 +1,5 @@
+
diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 0d9c4c3f84..e4dc100382 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -4,6 +4,7 @@ * All Rights Reserved *************************************************/ +import adhoc from './adhoc/main'; import host from './hosts/main'; import group from './groups/main'; import sources from './sources/main'; @@ -17,8 +18,10 @@ import { N_ } from '../i18n'; import InventoryList from './inventory.list'; import InventoryForm from './inventory.form'; import InventoryManageService from './inventory-manage.service'; +import adHocRoute from './adhoc/adhoc.route'; export default angular.module('inventory', [ + adhoc.name, host.name, group.name, sources.name, @@ -81,6 +84,60 @@ angular.module('inventory', [ } }); + let adhocCredentialLookup = { + searchPrefix: 'credential', + name: 'inventories.edit.adhoc.credential', + url: '/credential', + data: { + formChildState: true + }, + params: { + credential_search: { + value: { + page_size: '5' + }, + squash: true, + dynamic: true + } + }, + ncyBreadcrumb: { + skip: true + }, + views: { + 'related': { + templateProvider: function(ListDefinition, generateList) { + let list_html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + return `${list_html}`; + + } + } + }, + resolve: { + ListDefinition: ['CredentialList', function(CredentialList) { + let list = _.cloneDeep(CredentialList); + list.lookupConfirmText = 'SELECT'; + return list; + }], + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', + (list, qs, $stateParams, GetBasePath) => { + let path = GetBasePath(list.name) || GetBasePath(list.basePath); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#form-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + } + }; + return Promise.all([ basicInventoryAdd, basicInventoryEdit, @@ -121,7 +178,9 @@ angular.module('inventory', [ } ] } - }) + }), + stateExtender.buildDefinition(adHocRoute), + stateExtender.buildDefinition(adhocCredentialLookup) ]) }; }); diff --git a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js index f89b6c6606..e9eaea0973 100644 --- a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -143,8 +143,17 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa var hostIds = _.map($scope.hostsSelected, (host) => host.id); $state.go('systemTracking', { inventoryId: $state.params.inventory_id, - hosts: $scope.$parent.hostsSelectedItems, + hosts: $scope.hostsSelected, hostIds: hostIds }); }; + + $scope.setAdhocPattern = function(){ + var pattern = _($scope.hostsSelected) + .map(function(item){ + return item.name; + }).value().join(':'); + + $state.go('^.adhoc', {pattern: pattern}); + }; }]; diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js index 3b4140e74a..e936e3c590 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -83,6 +83,19 @@ export default { }, actions: { + launch: { + mode: 'all', + ngDisabled: '!hostsSelected', + ngClick: 'setAdhocPattern()', + awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", + dataTipWatch: "adhocCommandTooltip", + actionClass: 'btn List-buttonDefault', + buttonContent: 'RUN COMMANDS', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + // TODO: we don't always want to show this + ngShow: true + }, system_tracking: { buttonContent: 'System Tracking', ngClick: 'systemTracking()', From 0de2f7deb4f6be00966467d9fce1d902f57c8cfb Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 24 Apr 2017 21:03:12 -0400 Subject: [PATCH 31/49] Hooked up inventory source scheduling. Fixed various small bugs --- .../src/inventories/adhoc/adhoc.controller.js | 2 +- .../groups/add/groups-add.controller.js | 2 +- .../src/inventories/groups/groups.list.js | 3 +- .../groups/list/groups-list.controller.js | 33 ++----- .../nested-groups-list.controller.js | 33 ++----- .../hosts/add/host-add.controller.js | 14 --- .../client/src/inventories/hosts/add/main.js | 11 --- awx/ui/client/src/inventories/hosts/main.js | 2 - awx/ui/client/src/inventories/main.js | 95 ++++++++++++++++++- .../related-hosts/add/host-add.controller.js | 4 +- .../related-hosts/related-host.list.js | 3 +- .../sources/add/sources-add.controller.js | 44 ++++----- .../edit/build-sources-edit-state.factory.js | 4 +- .../sources/edit/sources-edit.controller.js | 50 +++++----- .../sources/list/sources-list.controller.js | 88 +---------------- .../src/inventories/sources/sources.list.js | 1 - .../launchjob.factory.js | 2 +- awx/ui/client/src/shared/form-generator.js | 6 +- 18 files changed, 169 insertions(+), 228 deletions(-) delete mode 100644 awx/ui/client/src/inventories/hosts/add/host-add.controller.js delete mode 100644 awx/ui/client/src/inventories/hosts/add/main.js diff --git a/awx/ui/client/src/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories/adhoc/adhoc.controller.js index 4b8e25c051..fcff3cb3a1 100644 --- a/awx/ui/client/src/inventories/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/groups/add/groups-add.controller.js b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js index 7b995e7f6a..2afb50be49 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -53,7 +53,7 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) .then(() => $state.go('^', null, { reload: true })); } else { - $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true }); + $state.go('^.edit', { group_id: res.data.id }, { reload: true }); } }); diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 76c847c01b..bbf1280824 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -50,8 +50,9 @@ export default { 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.", + 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.", dataTipWatch: "adhocCommandTooltip", + dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: true, diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 003e9ef65c..ba36eb6d7a 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -35,7 +35,7 @@ // 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.editGroup") { + if($state.current.name === "inventories.edit.groups") { $scope.rowBeingEdited = $state.params.group_id; $scope.listBeingEdited = "groups"; } @@ -79,18 +79,6 @@ {hosts_status_class: hosts_status.class}); } - $scope.groupSelect = function(id){ - var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); - $state.go('inventoryManage', { - inventory_id: $stateParams.inventory_id, - group: group, - group_search: { - page_size: '20', - page: '1', - order_by: 'name', - } - }, {reload: true}); - }; $scope.createGroup = function(){ $state.go('inventories.edit.groups.add'); }; @@ -116,7 +104,7 @@ // 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('inventoryManage', null, {reload: true}); + $state.go('.', null, {reload: true}); }); switch($scope.deleteOption){ @@ -124,7 +112,7 @@ GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id) .then(() => { if (parseInt($state.params.group_id) === $scope.toDelete.id) { - $state.go("inventoryManage", null, {reload: true}); + $state.go("^", null, {reload: true}); } else { $state.go($state.current, null, {reload: true}); } @@ -136,7 +124,7 @@ default: GroupManageService.delete($scope.toDelete.id).then(() => { if (parseInt($state.params.group_id) === $scope.toDelete.id) { - $state.go("inventoryManage", null, {reload: true}); + $state.go("^", null, {reload: true}); } else { $state.go($state.current, null, {reload: true}); } @@ -202,22 +190,15 @@ }); }; $scope.showFailedHosts = function() { - $state.go('inventoryManage', {failed: true}, {reload: true}); - }; - $scope.scheduleGroup = function(id) { - // Add this group's id to the array of group id's so that it gets - // added to the breadcrumb trail - var groupsArr = $stateParams.group ? $stateParams.group : []; - groupsArr.push(id); - $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + // TODO: implement }; $scope.copyMoveGroup = function(id){ - $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + // TODO: implement }; var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "inventoryManage.editGroup") { + if (toState.name === "inventories.edit.groups.edit") { $scope.rowBeingEdited = toParams.group_id; $scope.listBeingEdited = "groups"; } diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 7f7425ee84..541c517b15 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -35,7 +35,7 @@ // 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.editGroup") { + if($state.current.name === "inventories.edit.groups.edit.nested_groups.edit") { $scope.rowBeingEdited = $state.params.group_id; $scope.listBeingEdited = "groups"; } @@ -63,18 +63,6 @@ {hosts_status_class: hosts_status.class}); } - $scope.groupSelect = function(id){ - var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); - $state.go('inventoryManage', { - inventory_id: $stateParams.inventory_id, - group: group, - group_search: { - page_size: '20', - page: '1', - order_by: 'name', - } - }, {reload: true}); - }; $scope.createGroup = function(){ $state.go('inventories.edit.groups.add'); }; @@ -100,7 +88,7 @@ // 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('inventoryManage', null, {reload: true}); + $state.go('.', null, {reload: true}); }); switch($scope.deleteOption){ @@ -108,7 +96,7 @@ GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id) .then(() => { if (parseInt($state.params.group_id) === $scope.toDelete.id) { - $state.go("inventoryManage", null, {reload: true}); + $state.go("^", null, {reload: true}); } else { $state.go($state.current, null, {reload: true}); } @@ -120,7 +108,7 @@ default: GroupManageService.delete($scope.toDelete.id).then(() => { if (parseInt($state.params.group_id) === $scope.toDelete.id) { - $state.go("inventoryManage", null, {reload: true}); + $state.go("^", null, {reload: true}); } else { $state.go($state.current, null, {reload: true}); } @@ -186,14 +174,7 @@ }); }; $scope.showFailedHosts = function() { - $state.go('inventoryManage', {failed: true}, {reload: true}); - }; - $scope.scheduleGroup = function(id) { - // Add this group's id to the array of group id's so that it gets - // added to the breadcrumb trail - var groupsArr = $stateParams.group ? $stateParams.group : []; - groupsArr.push(id); - $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + // TODO: implement }; // $scope.$parent governed by InventoryManageController, for unified multiSelect options $scope.$on('multiSelectList.selectionChanged', (event, selection) => { @@ -202,11 +183,11 @@ }); $scope.copyMoveGroup = function(id){ - $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); + // TODO: implement }; var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "inventoryManage.editGroup") { + if (toState.name === "inventories.edit.groups.edit.nested_groups.edit") { $scope.rowBeingEdited = toParams.group_id; $scope.listBeingEdited = "groups"; } diff --git a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js deleted file mode 100644 index 8e5b36ee7d..0000000000 --- a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js +++ /dev/null @@ -1,14 +0,0 @@ -/************************************************* - * Copyright (c) 2017 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -function HostsAdd() { - -console.log('inside host add'); - -} - -export default [ HostsAdd -]; diff --git a/awx/ui/client/src/inventories/hosts/add/main.js b/awx/ui/client/src/inventories/hosts/add/main.js deleted file mode 100644 index 9f1f083b96..0000000000 --- a/awx/ui/client/src/inventories/hosts/add/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2017 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import controller from './host-add.controller'; - -export default -angular.module('hostsAdd', []) - .controller('HostAddController', controller); diff --git a/awx/ui/client/src/inventories/hosts/main.js b/awx/ui/client/src/inventories/hosts/main.js index fe21c6a43e..39d36fb762 100644 --- a/awx/ui/client/src/inventories/hosts/main.js +++ b/awx/ui/client/src/inventories/hosts/main.js @@ -4,7 +4,6 @@ * All Rights Reserved *************************************************/ - import hostAdd from './add/main'; import hostEdit from './edit/main'; import hostList from './list/main'; import HostsList from './host.list'; @@ -16,7 +15,6 @@ export default angular.module('host', [ - hostAdd.name, hostEdit.name, hostList.name, SmartInventory.name, diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index e4dc100382..f2dc2fc58e 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -36,12 +36,9 @@ angular.module('inventory', [ .service('InventoryManageService', InventoryManageService) .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { - // When stateDefinition.lazyLoad() resolves, states matching name.** or /url** will be de-registered and replaced with resolved states - // This means inventoryManage states will not be registered correctly on page refresh, unless they're registered at the same time as the inventories state tree let stateDefinitions = stateDefinitionsProvider.$get(), stateExtender = $stateExtenderProvider.$get(); - function generateInventoryStates() { let basicInventoryAdd = stateDefinitions.generateTree({ @@ -138,6 +135,93 @@ angular.module('inventory', [ } }; + let listSchedules = { + name: 'inventories.edit.inventory_sources.edit.schedules', + url: '/schedules', + searchPrefix: 'schedule', + ncyBreadcrumb: { + label: N_('SCHEDULES') + }, + resolve: { + Dataset: ['ScheduleList', 'QuerySet', '$stateParams', 'GetBasePath', 'inventorySourceData', + function(list, qs, $stateParams, GetBasePath, inventorySourceData) { + let path = `${inventorySourceData.related.schedules}`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + ParentObject: ['inventorySourceData', function(inventorySourceData) { + return inventorySourceData; + }], + UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', + function(Rest, GetBasePath, $stateParams, $q) { + Rest.setUrl(GetBasePath('unified_jobs')); + var val = $q.defer(); + Rest.options() + .then(function(data) { + val.resolve(data.data); + }, function(data) { + val.reject(data); + }); + return val.promise; + }], + ScheduleList: ['SchedulesList', 'inventorySourceData', + (SchedulesList, inventorySourceData) => { + let list = _.cloneDeep(SchedulesList); + list.basePath = `${inventorySourceData.related.schedules}`; + return list; + } + ] + }, + views: { + // clear form template when views render in this substate + 'form': { + templateProvider: () => '' + }, + // target the un-named ui-view @ root level + '@': { + templateProvider: function(ScheduleList, generateList, ParentObject) { + // include name of parent resource in listTitle + ScheduleList.listTitle = `${ParentObject.name}
` + N_('SCHEDULES'); + let html = generateList.build({ + list: ScheduleList, + mode: 'edit' + }); + html = generateList.wrapPanel(html); + return "
" + generateList.insertFormView() + html + "
"; + }, + controller: 'schedulerListController' + } + } + }; + + let addSchedule = { + name: 'inventories.edit.inventory_sources.edit.schedules.add', + url: '/add', + ncyBreadcrumb: { + label: N_("CREATE SCHEDULE") + }, + views: { + 'form': { + controller: 'schedulerAddController', + templateUrl: templateUrl("scheduler/schedulerForm") + } + } + }; + + let editSchedule = { + name: 'inventories.edit.inventory_sources.edit.schedules.edit', + url: '/:schedule_id', + ncyBreadcrumb: { + label: "{{schedule_obj.name}}" + }, + views: { + 'form': { + templateUrl: templateUrl("scheduler/schedulerForm"), + controller: 'schedulerEditController', + } + } + }; + return Promise.all([ basicInventoryAdd, basicInventoryEdit, @@ -180,7 +264,10 @@ angular.module('inventory', [ } }), stateExtender.buildDefinition(adHocRoute), - stateExtender.buildDefinition(adhocCredentialLookup) + stateExtender.buildDefinition(adhocCredentialLookup), + stateExtender.buildDefinition(listSchedules), + stateExtender.buildDefinition(addSchedule), + stateExtender.buildDefinition(editSchedule) ]) }; }); diff --git a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js index 6bb5d0ab40..7b9eb9ba3b 100644 --- a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js @@ -52,10 +52,10 @@ export default ['$state', '$stateParams', '$scope', 'RelatedHostsFormDefinition' // assign the host to current group if not at the root level if ($stateParams.group) { HostManageService.associateGroup(res.data, _.last($stateParams.group)).then(function() { - $state.go('inventoryManage.editHost', { host_id: res.data.id }, { reload: true }); + $state.go('^.edit', { host_id: res.data.id }, { reload: true }); }); } else { - $state.go('inventoryManage.editHost', { host_id: res.data.id }, { reload: true }); + $state.go('^.edit', { host_id: res.data.id }, { reload: true }); } }); }; diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js index e936e3c590..1b0a970086 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -87,8 +87,9 @@ export default { 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.", + 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.", dataTipWatch: "adhocCommandTooltip", + dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: true, 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 index 32dc70a7e6..15813fe765 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -50,31 +50,31 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', }; $scope.formSave = function() { - var params, source, json_data; + 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 = { - name: $scope.name, - description: $scope.description, - inventory: inventoryData.id, - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'], - source_script: $scope.inventory_script, - source: $scope.source.value, - 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(',') - }; - source = $scope.source.value; + 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 { - source = null; + params.source = null; } // switch (source) { // // no inventory source set, just create a new group @@ -109,7 +109,7 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', // _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) // .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); SourcesService.post(params).then(function(){ - $state.go('.', null, {reload: true}); + $state.go('^.edit', null, {reload: true}); }); // break; // } 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 index 29b988f349..5862650f27 100644 --- 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 @@ -14,7 +14,7 @@ export default ['$stateExtender', 'templateUrl', '$injector', breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(), stateConfig = { name: `${formStateDefinition.name}.${list.iterator}s.edit`, - url: `/edit/:source_id`, + url: `/edit/:inventory_source_id`, ncyBreadcrumb: { parent: `${formStateDefinition.name}`, label: `${breadcrumbLabel}` @@ -36,7 +36,7 @@ export default ['$stateExtender', 'templateUrl', '$injector', return definition; }], inventorySourceData: ['$stateParams', 'SourcesService', function($stateParams, SourcesService) { - return SourcesService.get({id: $stateParams.source_id }).then(res => res.data.results[0]); + return SourcesService.get({id: $stateParams.inventory_source_id }).then(res => res.data.results[0]); }] } }; 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 index 978be62478..220e89a0f9 100644 --- a/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories/sources/edit/sources-edit.controller.js @@ -7,11 +7,11 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', - 'inventorySourceData', 'SourcesService', + 'inventorySourceData', 'SourcesService', 'inventoryData', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON,ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, - inventorySourceData, SourcesService) { + inventorySourceData, SourcesService, inventoryData) { init(); @@ -76,39 +76,37 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', $state.go('^'); }; $scope.formSave = function() { - var params, source, json_data; + 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) { - // inventory_source fields - params = { - id: $scope.id, - variables: json_data, - name: $scope.name, - description: $scope.description, - inventory: $scope.inventory, - source: $scope.source.value, - credential: $scope.credential, - overwrite: $scope.overwrite, - overwrite_vars: $scope.overwrite_vars, - source_script: $scope.inventory_script, - update_on_launch: $scope.update_on_launch, - update_cache_timeout: $scope.update_cache_timeout || 0, - // comma-delimited strings - group_by: _.map($scope.group_by, 'value').join(','), - source_regions: _.map($scope.source_regions, 'value').join(','), - instance_filters: $scope.instance_filters, - source_vars: $scope[$scope.source.value + '_variables'] === '---' || $scope[$scope.source.value + '_variables'] === '{}' ? null : $scope[$scope.source.value + '_variables'] - }; - source = $scope.source.value; + 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 { - source = null; + 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($state.current, null, { reload: true })); + 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' diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js index 21c9003f5b..78411433d1 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -39,10 +39,6 @@ // 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.editGroup") { - $scope.rowBeingEdited = $state.params.group_id; - $scope.listBeingEdited = "groups"; - } $scope.inventory_id = $stateParams.inventory_id; _.forEach($scope[list.name], buildStatusIndicators); @@ -79,23 +75,11 @@ {status: inventory_source ? inventory_source.status : null}); } - $scope.groupSelect = function(id){ - var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value(); - $state.go('inventoryManage', { - inventory_id: $stateParams.inventory_id, - group: group, - group_search: { - page_size: '20', - page: '1', - order_by: 'name', - } - }, {reload: true}); - }; $scope.createSource = function(){ $state.go('inventories.edit.inventory_sources.add'); }; $scope.editSource = function(id){ - $state.go('inventories.edit.inventory_sources.edit', {source_id: id}); + $state.go('inventories.edit.inventory_sources.edit', {inventory_source_id: id}); }; $scope.deleteSource = function(inventory_source){ var body = '
Are you sure you want to permanently delete the inventory source below from the inventory?
' + $filter('sanitize')(inventory_source.name) + '
'; @@ -129,42 +113,6 @@ }); }; - $scope.$on(`ws-jobs`, function(e, data){ - var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); - - if (group === undefined || group === null) { - group = {}; - } - - if(data.status === 'failed' || data.status === 'successful'){ - let path; - if($stateParams && $stateParams.group && $stateParams.group.length > 0) { - path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; - } - else { - //reaches here if the user is on the root level group - path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; - } - 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; - // _.forEach($scope[list.name], buildStatusIndicators); - }); - } else { - var status = GetSyncStatusMsg({ - status: data.status, - has_inventory_sources: group.has_inventory_sources, - source: group.source - }); - group.status = data.status; - group.status_class = status.class; - group.status_tooltip = status.tooltip; - group.launch_tooltip = status.launch_tip; - group.launch_class = status.launch_class; - } - }); - $scope.cancelUpdate = function (id) { GroupsCancelUpdate({ scope: $scope, id: id }); }; @@ -174,40 +122,10 @@ group_id: id }); }; - $scope.showFailedHosts = function() { - $state.go('inventoryManage', {failed: true}, {reload: true}); - }; - $scope.scheduleGroup = function(id) { + $scope.scheduleSource = function(id) { // Add this group's id to the array of group id's so that it gets // added to the breadcrumb trail - var groupsArr = $stateParams.group ? $stateParams.group : []; - groupsArr.push(id); - $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true}); + $state.go('inventories.edit.inventory_sources.edit.schedules', {inventory_source_id: id}, {reload: true}); }; - // $scope.$parent governed by InventoryManageController, for unified multiSelect options - $scope.$on('multiSelectList.selectionChanged', (event, selection) => { - $scope.$parent.groupsSelected = selection.length > 0 ? true : false; - $scope.$parent.groupsSelectedItems = selection.selectedItems; - }); - - $scope.copyMoveGroup = function(id){ - $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); - }; - - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { - if (toState.name === "inventoryManage.editGroup") { - $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(); - }); }]; diff --git a/awx/ui/client/src/inventories/sources/sources.list.js b/awx/ui/client/src/inventories/sources/sources.list.js index cfd714072f..c8c44bf571 100644 --- a/awx/ui/client/src/inventories/sources/sources.list.js +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -12,7 +12,6 @@ export default { wellOverride: true, index: false, hover: true, - multiSelect: true, trackBy: 'inventory_source.id', basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/inventory_sources/', 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 += `
` + + let detailsTabSelected = this.form.activeEditState ? `$state.is('${this.form.activeEditState}') || $state.is('${this.form.stateTree}.edit') || $state.$current.data.formChildState` : `$state.is('${this.form.stateTree}.edit') || $state.$current.data.formChildState`; + html += `ng-class="{'is-selected': ${detailsTabSelected} }">` + `${details}
`; for (itm in this.form.related) { @@ -1506,7 +1507,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat `aw-tip-placement="${collection.dataPlacement}" ` + `data-tip-watch="${collection.dataTipWatch}" `; } - html += `ng-class="{'is-selected' : $state.is('${this.form.activeEditState}.${itm}') || $state.is('${this.form.stateTree}.edit.${itm}') ` ; + let relatedTabSelected = this.form.activeEditState ? `$state.includes('${this.form.activeEditState}.${itm}') || $state.includes('${this.form.stateTree}.edit.${itm}')` : `$state.includes('${this.form.stateTree}.edit.${itm}')`; + html += `ng-class="{'is-selected' : ${relatedTabSelected}` ; if(this.form.related[itm].disabled){ html += `, 'Form-tab--disabled' : ${this.form.related[itm].disabled }`; } From e435b6505a3ace9c6b4c60ffa54007d5e3194fae Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 25 Apr 2017 11:55:46 -0400 Subject: [PATCH 32/49] Implemented host facts related tab --- .../ansible_facts/ansible_facts.controller.js | 27 +++ .../ansible_facts/ansible_facts.partial.html | 10 + .../src/inventories/ansible_facts/main.js | 11 ++ .../client/src/inventories/hosts/host.form.js | 10 + awx/ui/client/src/inventories/main.js | 171 +++++++++++++----- .../related-hosts/related-host.form.js | 10 + .../src/shared/stateDefinitions.factory.js | 87 ++++----- 7 files changed, 234 insertions(+), 92 deletions(-) create mode 100644 awx/ui/client/src/inventories/ansible_facts/ansible_facts.controller.js create mode 100644 awx/ui/client/src/inventories/ansible_facts/ansible_facts.partial.html create mode 100644 awx/ui/client/src/inventories/ansible_facts/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/hosts/host.form.js b/awx/ui/client/src/inventories/hosts/host.form.js index efb64a4785..350e2c11cf 100644 --- a/awx/ui/client/src/inventories/hosts/host.form.js +++ b/awx/ui/client/src/inventories/hosts/host.form.js @@ -21,6 +21,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 +101,13 @@ export default ['i18n', function(i18n) { ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' } }, + + related: { + ansible_facts: { + name: 'ansible_facts', + title: i18n._('Facts'), + skipGenerator: true + } + } }; }]; diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index f2dc2fc58e..d753bc8cd5 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -19,6 +19,7 @@ import InventoryList from './inventory.list'; import InventoryForm from './inventory.form'; import InventoryManageService from './inventory-manage.service'; import adHocRoute from './adhoc/adhoc.route'; +import ansibleFacts from './ansible_facts/main'; export default angular.module('inventory', [ adhoc.name, @@ -29,7 +30,8 @@ angular.module('inventory', [ inventoryCompletedJobs.name, inventoryAdd.name, inventoryEdit.name, - inventoryList.name + inventoryList.name, + ansibleFacts.name ]) .factory('InventoryForm', InventoryForm) .factory('InventoryList', InventoryList) @@ -222,6 +224,32 @@ angular.module('inventory', [ } }; + let relatedHostsAnsibleFacts = { + name: 'inventories.edit.hosts.edit.ansible_facts', + url: '/ansible_facts', + ncyBreadcrumb: { + label: N_("FACTS") + }, + views: { + 'related': { + controller: 'AnsibleFactsController', + templateUrl: templateUrl('inventories/ansible_facts/ansible_facts') + } + }, + resolve: { + Facts: ['$stateParams', 'GetBasePath', 'Rest', + function($stateParams, GetBasePath, Rest) { + let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; + Rest.setUrl(ansibleFactsUrl); + return Rest.get() + .success(function(data) { + return data; + }); + } + ] + } + }; + return Promise.all([ basicInventoryAdd, basicInventoryEdit, @@ -267,64 +295,107 @@ angular.module('inventory', [ stateExtender.buildDefinition(adhocCredentialLookup), stateExtender.buildDefinition(listSchedules), stateExtender.buildDefinition(addSchedule), - stateExtender.buildDefinition(editSchedule) + stateExtender.buildDefinition(editSchedule), + stateExtender.buildDefinition(relatedHostsAnsibleFacts) ]) }; }); } - $stateProvider.state({ - name: 'hosts', - url: '/hosts', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'hosts', // top-most node in the generated tree (will replace this state definition) - modes: ['edit'], - list: 'HostsList', - form: 'HostsForm', - controllers: { - list: 'HostListController', - edit: 'HostEditController' - }, - urls: { - list: '/hosts' - }, - resolve: { - edit: { - host: ['Rest', '$stateParams', 'GetBasePath', - function(Rest, $stateParams, GetBasePath) { - let path = GetBasePath('hosts') + $stateParams.host_id; - Rest.setUrl(path); - return Rest.get(); - } - ] - } - }, - ncyBreadcrumb: { - label: N_('HOSTS') - }, - views: { - '@': { - templateUrl: templateUrl('inventories/inventories') - }, - 'list@hosts': { - templateProvider: function(HostsList, generateList) { - let html = generateList.build({ - list: HostsList, - mode: 'edit' - }); - return html; - }, - controller: 'HostListController' - } + let generateHostStates = function(){ + let hostTree = stateDefinitions.generateTree({ + parent: 'hosts', // top-most node in the generated tree (will replace this state definition) + modes: ['edit'], + list: 'HostsList', + form: 'HostsForm', + controllers: { + list: 'HostListController', + edit: 'HostEditController' + }, + urls: { + list: '/hosts' + }, + resolve: { + edit: { + host: ['Rest', '$stateParams', 'GetBasePath', + function(Rest, $stateParams, GetBasePath) { + let path = GetBasePath('hosts') + $stateParams.host_id; + Rest.setUrl(path); + return Rest.get(); + } + ] } - }) + }, + ncyBreadcrumb: { + label: N_('HOSTS') + }, + views: { + '@': { + templateUrl: templateUrl('inventories/inventories') + }, + 'list@hosts': { + templateProvider: function(HostsList, generateList) { + let html = generateList.build({ + list: HostsList, + mode: 'edit' + }); + return html; + }, + controller: 'HostListController' + } + } }); - $stateProvider.state({ - name: 'inventories', - url: '/inventories', - lazyLoad: () => generateInventoryStates() + let hostAnsibleFacts = { + name: 'hosts.edit.ansible_facts', + url: '/ansible_facts', + ncyBreadcrumb: { + label: N_("FACTS") + }, + views: { + 'related': { + controller: 'AnsibleFactsController', + templateUrl: templateUrl('inventories/ansible_facts/ansible_facts') + } + }, + resolve: { + Facts: ['$stateParams', 'GetBasePath', 'Rest', + function($stateParams, GetBasePath, Rest) { + let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; + Rest.setUrl(ansibleFactsUrl); + return Rest.get() + .success(function(data) { + return data; + }); + } + ] + } + }; + + return Promise.all([ + hostTree + ]).then((generated) => { + return { + states: _.reduce(generated, (result, definition) => { + return result.concat(definition.states); + }, [ + stateExtender.buildDefinition(hostAnsibleFacts) + ]) + }; }); + }; + + $stateProvider.state({ + name: 'hosts', + url: '/hosts', + lazyLoad: () => generateHostStates() + }); + + $stateProvider.state({ + name: 'inventories', + url: '/inventories', + lazyLoad: () => generateInventoryStates() + }); } ]); diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.form.js b/awx/ui/client/src/inventories/related-hosts/related-host.form.js index efb64a4785..07f6798e11 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.form.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.form.js @@ -21,6 +21,8 @@ export default ['i18n', function(i18n) { formLabelSize: 'col-lg-3', formFieldSize: 'col-lg-9', iterator: 'host', + activeEditState: 'inventories.edit.hosts.edit', + stateTree: 'inventories.edit.hosts', headerFields:{ enabled: { class: 'Form-header-field', @@ -99,5 +101,13 @@ export default ['i18n', function(i18n) { ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' } }, + + related: { + ansible_facts: { + name: 'ansible_facts', + title: i18n._('Facts'), + skipGenerator: true + } + } }; }]; diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 9b9ce3dbf5..f1bc9ba79c 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -554,53 +554,56 @@ function($injector, $stateExtender, $log, i18n) { function buildListNodes(field) { let states = []; - if(field && (field.listState || field.addState || field.editState)){ - if(field && field.listState){ - states.push(field.listState(field, formStateDefinition)); + if(!field.skipGenerator) { + if(field && (field.listState || field.addState || field.editState)){ + if(field && field.listState){ + states.push(field.listState(field, formStateDefinition)); + states = _.flatten(states); + } + if(field && field.addState){ + let formState = field.addState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any add-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + } + states = _.flatten(states); + } + if(field && field.editState){ + let formState = field.editState(field, formStateDefinition, params); + states.push(formState); + // intent here is to add lookup states for any edit-forms + if(field.includeForm){ + let form = field.includeForm ? $injector.get(field.includeForm) : field; + states.push(that.generateLookupNodes(form, formState)); + states.push(that.generateFormListDefinitions(form, formState, params)); + } + states = _.flatten(states); + } + } + else if(field.iterator === 'notification'){ + states.push(buildNotificationState(field)); states = _.flatten(states); } - if(field && field.addState){ - let formState = field.addState(field, formStateDefinition, params); - states.push(formState); - // intent here is to add lookup states for any add-forms - if(field.includeForm){ - let form = field.includeForm ? $injector.get(field.includeForm) : field; - states.push(that.generateLookupNodes(form, formState)); + else{ + states.push(buildListDefinition(field)); + if (field.iterator === 'permission' && field.actions && field.actions.add) { + if (form.name === 'user' || form.name === 'team'){ + states.push(buildRbacUserTeamDirective()); + } + else { + states.push(buildRbacResourceDirective()); + } } - states = _.flatten(states); - } - if(field && field.editState){ - let formState = field.editState(field, formStateDefinition, params); - states.push(formState); - // intent here is to add lookup states for any edit-forms - if(field.includeForm){ - let form = field.includeForm ? $injector.get(field.includeForm) : field; - states.push(that.generateLookupNodes(form, formState)); - states.push(that.generateFormListDefinitions(form, formState, params)); - } - states = _.flatten(states); - } - } - else if(field.iterator === 'notification'){ - states.push(buildNotificationState(field)); - states = _.flatten(states); - } - else{ - states.push(buildListDefinition(field)); - if (field.iterator === 'permission' && field.actions && field.actions.add) { - if (form.name === 'user' || form.name === 'team'){ - states.push(buildRbacUserTeamDirective()); - } - else { - states.push(buildRbacResourceDirective()); - } - } - else if (field.iterator === 'user' && field.actions && field.actions.add) { - if(form.name === 'team' || form.name === 'organization') { - states.push(buildRbacUserDirective()); + else if (field.iterator === 'user' && field.actions && field.actions.add) { + if(form.name === 'team' || form.name === 'organization') { + states.push(buildRbacUserDirective()); + } } } } + states = _.flatten(states); return states; } @@ -679,7 +682,7 @@ function($injector, $stateExtender, $log, i18n) { if (field.search) { state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); } - + return state; } return _(form.related).map(buildListNodes).flatten().value(); From 9fb8b6579f73a0a5185a54d9cdf81d033a76e0e1 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 25 Apr 2017 17:46:11 -0400 Subject: [PATCH 33/49] Hooked up copy/move hosts and groups --- .../copy-move/copy-move-groups.controller.js | 72 ++++++++++++++ .../copy-move/copy-move-groups.list.js | 24 +++++ .../copy-move/copy-move-hosts.controller.js | 50 ++++++++++ .../copy-move/copy-move.block.less | 33 +++++++ .../copy-move/copy-move.partial.html | 21 ++++ .../inventories/copy-move/copy-move.route.js | 98 +++++++++++++++++++ .../client/src/inventories/copy-move/main.js | 15 +++ .../groups/list/groups-list.controller.js | 2 +- .../src/inventories/inventories.partial.html | 1 + awx/ui/client/src/inventories/main.js | 89 +++++++---------- .../list/host-list.controller.js | 4 + 11 files changed, 356 insertions(+), 53 deletions(-) create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move-groups.controller.js create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move-hosts.controller.js create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move.block.less create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move.partial.html create mode 100644 awx/ui/client/src/inventories/copy-move/copy-move.route.js create mode 100644 awx/ui/client/src/inventories/copy-move/main.js 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/copy-move/copy-move-groups.list.js b/awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js new file mode 100644 index 0000000000..a207fd8e06 --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move-groups.list.js @@ -0,0 +1,24 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + + + +export default { + name: 'groups', + iterator: 'copy', + selectTitle: 'Copy Groups', + index: false, + well: false, + emptyListText: 'PLEASE CREATE ADDITIONAL GROUPS / HOSTS TO PERFORM THIS ACTION', + fields: { + name: { + key: true, + label: 'Target Group Name' + } + }, + 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/copy-move/copy-move.block.less b/awx/ui/client/src/inventories/copy-move/copy-move.block.less new file mode 100644 index 0000000000..608f59654a --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move.block.less @@ -0,0 +1,33 @@ +@import "./client/src/shared/branding/colors.default.less"; + +#Inventory-copyMovePanel { + .List-searchRow { + width: 50%; + } + .Form-header { + width: 50%; + margin-top: -20px; + } + + .Form-saveButton { + &:disabled { + border-color: @default-icon-hov; + } + } +} +.copyMove-choices { + float: right; + width: 25%; + text-align: right; +} +.copyMove-buttons{ + height: 30px; + margin-top: 20px; + + button { + margin-left: 20px; + } +} +.copyMove-root{ + margin-top: 10px; +} diff --git a/awx/ui/client/src/inventories/copy-move/copy-move.partial.html b/awx/ui/client/src/inventories/copy-move/copy-move.partial.html new file mode 100644 index 0000000000..030f0c7e3a --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/copy-move.partial.html @@ -0,0 +1,21 @@ +
+
+
{{item.name}}
+
+
+ + +
+
+
+ Use the inventory root +
+
+ + +
+
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/copy-move/main.js b/awx/ui/client/src/inventories/copy-move/main.js new file mode 100644 index 0000000000..a9fbd47660 --- /dev/null +++ b/awx/ui/client/src/inventories/copy-move/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import CopyMoveGroupsController from './copy-move-groups.controller'; +import CopyMoveHostsController from './copy-move-hosts.controller'; +import CopyMoveGroupList from './copy-move-groups.list'; + +export default +angular.module('manageCopyMove', []) + .controller('CopyMoveGroupsController', CopyMoveGroupsController) + .controller('CopyMoveHostsController', CopyMoveHostsController) + .value('CopyMoveGroupList', CopyMoveGroupList); diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index ba36eb6d7a..9e66fa5c9d 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -194,7 +194,7 @@ }; $scope.copyMoveGroup = function(id){ - // TODO: implement + $state.go('inventories.edit.groups.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); }; var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index 767cd197fa..d8fc0b2c60 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,4 +1,5 @@
+
diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index d753bc8cd5..5df33882a7 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -20,6 +20,8 @@ import InventoryForm from './inventory.form'; import InventoryManageService from './inventory-manage.service'; import adHocRoute from './adhoc/adhoc.route'; import ansibleFacts from './ansible_facts/main'; +import { copyMoveGroupRoute, copyMoveHostRoute } from './copy-move/copy-move.route'; +import copyMove from './copy-move/main'; export default angular.module('inventory', [ adhoc.name, @@ -31,7 +33,8 @@ angular.module('inventory', [ inventoryAdd.name, inventoryEdit.name, inventoryList.name, - ansibleFacts.name + ansibleFacts.name, + copyMove.name ]) .factory('InventoryForm', InventoryForm) .factory('InventoryList', InventoryList) @@ -41,6 +44,34 @@ angular.module('inventory', [ let stateDefinitions = stateDefinitionsProvider.$get(), stateExtender = $stateExtenderProvider.$get(); + function factsConfig(stateName) { + return { + name: stateName, + url: '/ansible_facts', + ncyBreadcrumb: { + label: N_("FACTS") + }, + views: { + 'related': { + controller: 'AnsibleFactsController', + templateUrl: templateUrl('inventories/ansible_facts/ansible_facts') + } + }, + resolve: { + Facts: ['$stateParams', 'GetBasePath', 'Rest', + function($stateParams, GetBasePath, Rest) { + let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; + Rest.setUrl(ansibleFactsUrl); + return Rest.get() + .success(function(data) { + return data; + }); + } + ] + } + }; + } + function generateInventoryStates() { let basicInventoryAdd = stateDefinitions.generateTree({ @@ -224,31 +255,7 @@ angular.module('inventory', [ } }; - let relatedHostsAnsibleFacts = { - name: 'inventories.edit.hosts.edit.ansible_facts', - url: '/ansible_facts', - ncyBreadcrumb: { - label: N_("FACTS") - }, - views: { - 'related': { - controller: 'AnsibleFactsController', - templateUrl: templateUrl('inventories/ansible_facts/ansible_facts') - } - }, - resolve: { - Facts: ['$stateParams', 'GetBasePath', 'Rest', - function($stateParams, GetBasePath, Rest) { - let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; - Rest.setUrl(ansibleFactsUrl); - return Rest.get() - .success(function(data) { - return data; - }); - } - ] - } - }; + let relatedHostsAnsibleFacts = factsConfig('inventories.edit.hosts.edit.ansible_facts'); return Promise.all([ basicInventoryAdd, @@ -296,7 +303,9 @@ angular.module('inventory', [ stateExtender.buildDefinition(listSchedules), stateExtender.buildDefinition(addSchedule), stateExtender.buildDefinition(editSchedule), - stateExtender.buildDefinition(relatedHostsAnsibleFacts) + stateExtender.buildDefinition(relatedHostsAnsibleFacts), + stateExtender.buildDefinition(copyMoveGroupRoute), + stateExtender.buildDefinition(copyMoveHostRoute) ]) }; }); @@ -347,31 +356,7 @@ angular.module('inventory', [ } }); - let hostAnsibleFacts = { - name: 'hosts.edit.ansible_facts', - url: '/ansible_facts', - ncyBreadcrumb: { - label: N_("FACTS") - }, - views: { - 'related': { - controller: 'AnsibleFactsController', - templateUrl: templateUrl('inventories/ansible_facts/ansible_facts') - } - }, - resolve: { - Facts: ['$stateParams', 'GetBasePath', 'Rest', - function($stateParams, GetBasePath, Rest) { - let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; - Rest.setUrl(ansibleFactsUrl); - return Rest.get() - .success(function(data) { - return data; - }); - } - ] - } - }; + let hostAnsibleFacts = factsConfig('hosts.edit.ansible_facts'); return Promise.all([ hostTree diff --git a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js index e9eaea0973..f2f960ceea 100644 --- a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -156,4 +156,8 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa $state.go('^.adhoc', {pattern: pattern}); }; + + $scope.copyMoveHost = function(id) { + $state.go('inventories.edit.hosts.copyMoveHost', {host_id: id}); + }; }]; From 0bec96ff28ce480b96cc61f366ab948604f7a930 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 25 Apr 2017 19:06:07 -0400 Subject: [PATCH 34/49] Fixed jshint error --- .../groups/nested-groups/nested-groups-list.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 541c517b15..31e55fa992 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -182,7 +182,7 @@ $scope.$parent.groupsSelectedItems = selection.selectedItems; }); - $scope.copyMoveGroup = function(id){ + $scope.copyMoveGroup = function(){ // TODO: implement }; From fe633038167e32982bbc35fc8116f9850814c14f Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 24 Apr 2017 13:14:13 -0700 Subject: [PATCH 35/49] Adding the ability to add/edit hosts to a group includes adding the ansible facts to that host form --- .../configuration/configuration.controller.js | 44 ++---- .../src/inventories/groups/groups.form.js | 15 +- awx/ui/client/src/inventories/groups/main.js | 4 +- .../inventories/groups/nested-hosts/main.js | 17 +++ .../nested-hosts-list-state.factory.js | 79 +++++++++++ .../groups/nested-hosts/nested-hosts.list.js | 131 ++++++++++++++++++ .../src/inventories/inventories.partial.html | 2 +- awx/ui/client/src/inventories/main.js | 2 + .../related-hosts/add/host-add.controller.js | 6 +- .../related-hosts/related-host.form.js | 3 +- .../src/shared/stateDefinitions.factory.js | 4 +- 11 files changed, 268 insertions(+), 39 deletions(-) create mode 100644 awx/ui/client/src/inventories/groups/nested-hosts/main.js create mode 100644 awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts-list-state.factory.js create mode 100644 awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.list.js 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/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index c8655fa4e6..308d3555d8 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -10,8 +10,9 @@ * @description This form is for adding/editing a Group on the inventory page */ -export default ['i18n', 'nestedGroupListState', -function(i18n, nestedGroupListState){ +export default ['i18n', 'nestedGroupListState', 'nestedHostsListState', + 'buildHostAddState', +function(i18n, nestedGroupListState, nestedHostsListState, buildHostAddState){ return { addTitle: 'CREATE GROUP', editTitle: '{{ name }}', @@ -91,6 +92,16 @@ function(i18n, nestedGroupListState){ // addState: buildGroupsAddState, // editState: buildGroupsEditState }, + 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/groups/main.js b/awx/ui/client/src/inventories/groups/main.js index b189cae4a2..d5a40f97ff 100644 --- a/awx/ui/client/src/inventories/groups/main.js +++ b/awx/ui/client/src/inventories/groups/main.js @@ -8,6 +8,7 @@ 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'; @@ -22,7 +23,8 @@ export default groupList.name, groupAdd.name, groupEdit.name, - nestedGroups.name + nestedGroups.name, + nestedHosts.name ]) .factory('GroupForm', groupFormDefinition) .value('GroupList', groupListDefinition) 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.js b/awx/ui/client/src/inventories/groups/nested-hosts/nested-hosts.list.js new file mode 100644 index 0000000000..7234327573 --- /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.", + dataTipWatch: "adhocCommandTooltip", + actionClass: 'btn List-buttonDefault', + buttonContent: 'RUN COMMANDS', + showTipWhenDisabled: true, + tooltipInnerClass: "Tooltip-wide", + // TODO: we don't always want to show this + ngShow: true + }, + system_tracking: { + buttonContent: 'System Tracking', + ngClick: 'systemTracking()', + 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/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index d8fc0b2c60..e9cbce9432 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,8 +1,8 @@
-
+
diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 5df33882a7..5d49269f82 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -256,6 +256,7 @@ angular.module('inventory', [ }; let relatedHostsAnsibleFacts = factsConfig('inventories.edit.hosts.edit.ansible_facts'); + let nestedHostsAnsibleFacts = factsConfig('inventories.edit.groups.edit.nested_hosts.edit.ansible_facts'); return Promise.all([ basicInventoryAdd, @@ -304,6 +305,7 @@ angular.module('inventory', [ stateExtender.buildDefinition(addSchedule), stateExtender.buildDefinition(editSchedule), stateExtender.buildDefinition(relatedHostsAnsibleFacts), + stateExtender.buildDefinition(nestedHostsAnsibleFacts), stateExtender.buildDefinition(copyMoveGroupRoute), stateExtender.buildDefinition(copyMoveHostRoute) ]) diff --git a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js index 7b9eb9ba3b..baeb5f80fb 100644 --- a/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/add/host-add.controller.js @@ -50,9 +50,9 @@ export default ['$state', '$stateParams', '$scope', 'RelatedHostsFormDefinition' }; HostManageService.post(params).then(function(res) { // assign the host to current group if not at the root level - if ($stateParams.group) { - HostManageService.associateGroup(res.data, _.last($stateParams.group)).then(function() { - $state.go('^.edit', { host_id: res.data.id }, { reload: true }); + if ($stateParams.group_id) { + HostManageService.associateGroup(res.data, $stateParams.group_id).then(function() { + $state.go('inventories.edit.groups.edit.nested_hosts', { group_id: $stateParams.group_id }, { reload: true }); }); } else { $state.go('^.edit', { host_id: res.data.id }, { reload: true }); diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.form.js b/awx/ui/client/src/inventories/related-hosts/related-host.form.js index 07f6798e11..00d9d284d9 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.form.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.form.js @@ -21,7 +21,8 @@ export default ['i18n', function(i18n) { formLabelSize: 'col-lg-3', formFieldSize: 'col-lg-9', iterator: 'host', - activeEditState: 'inventories.edit.hosts.edit', + // activeEditState: 'inventories.edit.hosts.edit', + // activeEditState: 'inventories.edit.groups.edit.nested_hosts.edit', stateTree: 'inventories.edit.hosts', headerFields:{ enabled: { diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index f1bc9ba79c..0ac639afe5 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -563,7 +563,7 @@ function($injector, $stateExtender, $log, i18n) { if(field && field.addState){ let formState = field.addState(field, formStateDefinition, params); states.push(formState); - // intent here is to add lookup states for any add-forms + // intent below is to add lookup states for any add-forms if(field.includeForm){ let form = field.includeForm ? $injector.get(field.includeForm) : field; states.push(that.generateLookupNodes(form, formState)); @@ -573,7 +573,7 @@ function($injector, $stateExtender, $log, i18n) { if(field && field.editState){ let formState = field.editState(field, formStateDefinition, params); states.push(formState); - // intent here is to add lookup states for any edit-forms + // intent below is to add lookup states for any edit-forms if(field.includeForm){ let form = field.includeForm ? $injector.get(field.includeForm) : field; states.push(that.generateLookupNodes(form, formState)); From fed34fff8b532c36e35688806e605ac0d436fa69 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 25 Apr 2017 21:12:10 -0700 Subject: [PATCH 36/49] fixing add/edit for a related group --- .../groups/add/groups-add.controller.js | 4 ++-- .../inventories/groups/nested-groups/main.js | 2 ++ .../nested-groups-list.controller.js | 2 +- .../nested-groups/nested-groups.form.js | 4 +--- .../nested-groups/nested-groups.list.js | 20 +++++++++---------- .../src/inventories/inventories.partial.html | 1 + 6 files changed, 17 insertions(+), 16 deletions(-) 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 index 2afb50be49..e8b8426c03 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -49,8 +49,8 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', }; GroupManageService.post(group).then(res => { - if ($stateParams.group) { - return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) + 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/nested-groups/main.js b/awx/ui/client/src/inventories/groups/nested-groups/main.js index 4d322b4270..0c02775e75 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/main.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/main.js @@ -5,6 +5,7 @@ *************************************************/ 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'; @@ -12,6 +13,7 @@ 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-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 31e55fa992..94b0d05a4f 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -64,7 +64,7 @@ } $scope.createGroup = function(){ - $state.go('inventories.edit.groups.add'); + $state.go('inventories.edit.groups.edit.nested_groups.add'); }; $scope.editGroup = function(id){ $state.go('inventories.edit.groups.edit', {group_id: id}); 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 index 9d4645018d..d39759bc25 100644 --- 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 @@ -88,9 +88,7 @@ function(i18n, nestedGroupListState){ includeForm: "RelatedGroupFormDefinition", title: i18n._('Groups'), iterator: 'related_group', - listState: nestedGroupListState, - // addState: buildGroupsAddState, - // editState: buildGroupsEditState + 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 index ba958b0175..8987130a59 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -22,10 +22,10 @@ export default { nosort: true, mode: 'all', iconOnly: true, - awToolTip: "{{ group.hosts_status_tip }}", + awToolTip: "{{ nested_group.hosts_status_tip }}", dataPlacement: "top", - ngClick: "showFailedHosts(group)", - icon: "{{ 'fa icon-job-' + group.hosts_status_class }}", + ngClick: "showFailedHosts(nested_group)", + icon: "{{ 'fa icon-job-' + nested_group.hosts_status_class }}", columnClass: 'status-column List-staticColumn--smallStatus' }, name: { @@ -33,7 +33,7 @@ export default { key: true, // ngClick: "groupSelect(group.id)", - ngClick: "editGroup(group.id)", + ngClick: "editGroup(nested_group.id)", columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', class: 'InventoryManage-breakWord', } @@ -104,7 +104,7 @@ export default { // }, copy: { mode: 'all', - ngClick: "copyMoveGroup(group.id)", + ngClick: "copyMoveGroup(nested_group.id)", awToolTip: 'Copy or move group', ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy", dataPlacement: "top" @@ -120,23 +120,23 @@ export default { edit: { //label: 'Edit', mode: 'all', - ngClick: "editGroup(group.id)", + ngClick: "editGroup(nested_group.id)", awToolTip: 'Edit group', dataPlacement: "top", - ngShow: "group.summary_fields.user_capabilities.edit" + ngShow: "nested_group.summary_fields.user_capabilities.edit" }, view: { //label: 'Edit', mode: 'all', - ngClick: "editGroup(group.id)", + ngClick: "editGroup(nested_group.id)", awToolTip: 'View group', dataPlacement: "top", - ngShow: "!group.summary_fields.user_capabilities.edit" + ngShow: "!nested_group.summary_fields.user_capabilities.edit" }, "delete": { //label: 'Delete', mode: 'all', - ngClick: "deleteGroup(group)", + ngClick: "deleteGroup(nested_group)", awToolTip: 'Delete group', dataPlacement: "top", ngShow: "group.summary_fields.user_capabilities.delete" diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index e9cbce9432..ae2f1df601 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -2,6 +2,7 @@
+
From 0b6a5b14a34996876b73ace0bb891b3204c10f18 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Tue, 25 Apr 2017 21:14:33 -0700 Subject: [PATCH 37/49] adding extra file for nested group adding --- .../nested-groups-add-state.factory.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 awx/ui/client/src/inventories/groups/nested-groups/nested-groups-add-state.factory.js 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; + } +]; From db8b36a0573e66ee339f936277b81c111dc0f9ba Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 26 Apr 2017 14:53:25 -0400 Subject: [PATCH 38/49] Removed old failed hosts logic. Added logic to hide panels where more than two are present --- awx/ui/client/legacy-styles/main-layout.less | 4 +++ .../src/inventories/groups/groups.list.js | 1 - .../groups/list/groups-list.controller.js | 4 +-- .../nested-groups-list.controller.js | 5 ++-- .../nested-groups/nested-groups.list.js | 1 - .../src/inventories/inventories.partial.html | 1 + .../limit-panels/limit-panels.directive.js | 30 +++++++++++++++++++ awx/ui/client/src/shared/limit-panels/main.js | 5 ++++ awx/ui/client/src/shared/main.js | 2 ++ 9 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 awx/ui/client/src/shared/limit-panels/limit-panels.directive.js create mode 100644 awx/ui/client/src/shared/limit-panels/main.js 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/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index bbf1280824..816a48d92b 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -24,7 +24,6 @@ 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' }, diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 9e66fa5c9d..68b7da4f35 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -183,15 +183,13 @@ $scope.cancelUpdate = function (id) { GroupsCancelUpdate({ scope: $scope, id: id }); }; + $scope.viewUpdateStatus = function (id) { ViewUpdateStatus({ scope: $scope, group_id: id }); }; - $scope.showFailedHosts = function() { - // TODO: implement - }; $scope.copyMoveGroup = function(id){ $state.go('inventories.edit.groups.copyMoveGroup', {group_id: id, groups: $stateParams.groups}); diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 94b0d05a4f..471c38cb59 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -167,15 +167,14 @@ $scope.cancelUpdate = function (id) { GroupsCancelUpdate({ scope: $scope, id: id }); }; + $scope.viewUpdateStatus = function (id) { ViewUpdateStatus({ scope: $scope, group_id: id }); }; - $scope.showFailedHosts = function() { - // TODO: implement - }; + // $scope.$parent governed by InventoryManageController, for unified multiSelect options $scope.$on('multiSelectList.selectionChanged', (event, selection) => { $scope.$parent.groupsSelected = selection.length > 0 ? true : false; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js index 8987130a59..f474e7d595 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -24,7 +24,6 @@ export default { iconOnly: true, awToolTip: "{{ nested_group.hosts_status_tip }}", dataPlacement: "top", - ngClick: "showFailedHosts(nested_group)", icon: "{{ 'fa icon-job-' + nested_group.hosts_status_class }}", columnClass: 'status-column List-staticColumn--smallStatus' }, diff --git a/awx/ui/client/src/inventories/inventories.partial.html b/awx/ui/client/src/inventories/inventories.partial.html index ae2f1df601..3cfe4b2711 100644 --- a/awx/ui/client/src/inventories/inventories.partial.html +++ b/awx/ui/client/src/inventories/inventories.partial.html @@ -1,4 +1,5 @@
+
diff --git a/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js b/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js new file mode 100644 index 0000000000..56ed21bf6f --- /dev/null +++ b/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js @@ -0,0 +1,30 @@ +export default ['$rootScope', function($rootScope) { + return { + restrict: 'E', + scope: { + maxPanels: '@', + panelContainer: '@' + }, + link: function(scope) { + + scope.maxPanels = parseInt(scope.maxPanels); + + $rootScope.$on('$stateChangeSuccess', function() { + let panels = angular.element('#' + scope.panelContainer).find('.Panel'); + + if(panels.length > scope.maxPanels) { + // hide the excess panels + $(panels).each(function( index ) { + if(index+1 > scope.maxPanels) { + $(this).addClass('Panel-hidden'); + } + }); + } + else { + // show all the panels + $(panels).removeClass('Panel-hidden'); + } + }); + } + }; +}]; diff --git a/awx/ui/client/src/shared/limit-panels/main.js b/awx/ui/client/src/shared/limit-panels/main.js new file mode 100644 index 0000000000..407cb09a95 --- /dev/null +++ b/awx/ui/client/src/shared/limit-panels/main.js @@ -0,0 +1,5 @@ +import directive from './limit-panels.directive'; + +export default + angular.module('LimitPanelsModule', []) + .directive('awLimitPanels', directive); diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 27b6bb1488..eccf67be4e 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -30,6 +30,7 @@ import PromptDialog from './prompt-dialog'; import directives from './directives'; import features from './features/main'; import orgAdminLookup from './org-admin-lookup/main'; +import limitPanels from './limit-panels/main'; import 'angular-duration-format'; export default @@ -57,6 +58,7 @@ angular.module('shared', [listGenerator.name, filters.name, features.name, orgAdminLookup.name, + limitPanels.name, require('angular-cookies'), 'angular-duration-format' ]) From 832e55f964aad654ff79fd235930463a3edc6301 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 26 Apr 2017 12:24:17 -0700 Subject: [PATCH 39/49] Adjustments to nested groups edit after feedback from Bill. --- .../groups/nested-groups/nested-groups-list.controller.js | 3 +++ .../inventories/groups/nested-groups/nested-groups.form.js | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 471c38cb59..3a13d3b64c 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -69,6 +69,9 @@ $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); 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 index d39759bc25..955d25e462 100644 --- 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 @@ -19,13 +19,13 @@ function(i18n, nestedGroupListState){ name: 'nested_group', iterator: "nested_group", basePath: 'groups', - parent: 'inventories.edit.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', - detailsClick: "$state.go('inventories.edit.groups.edit')", + activeEditState: 'inventories.edit.groups.edit.nested_groups.edit', + detailsClick: "$state.go('inventories.edit.groups.edit.nested_groups.edit')", well: false, tabs: true, fields: { From 5c79561b7df88dbb2df91576b91a373786d6c5f2 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 27 Apr 2017 13:47:28 -0400 Subject: [PATCH 40/49] Fixed inventory codemirrors and tweaked delete source warning --- .../edit/inventory-edit.controller.js | 17 ++++++----------- .../groups/add/groups-add.controller.js | 8 ++++---- awx/ui/client/src/inventories/inventory.form.js | 3 ++- .../sources/list/sources-list.controller.js | 2 +- 4 files changed, 13 insertions(+), 17 deletions(-) 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 69ac5ccec3..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) { 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 index e8b8426c03..6b5f849b40 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -20,10 +20,10 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', // 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; - }); + rbacUiControlService.canAdd(GetBasePath('inventory') + $stateParams.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); $scope.parseType = 'yaml'; $scope.envParseType = 'yaml'; ParseTypeChange({ diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js index c79be281f4..822f561163 100644 --- a/awx/ui/client/src/inventories/inventory.form.js +++ b/awx/ui/client/src/inventories/inventory.form.js @@ -75,7 +75,8 @@ function(i18n, buildGroupsListState, buildGroupsAddState, buildGroupsEditState, ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' }, - variables: { + inventory_variables: { + realName: 'variables', label: i18n._('Variables'), type: 'textarea', class: 'Form-formGroup--fullWidth', diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js index 78411433d1..e6cfe4a51b 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -82,7 +82,7 @@ $state.go('inventories.edit.inventory_sources.edit', {inventory_source_id: id}); }; $scope.deleteSource = function(inventory_source){ - var body = '
Are you sure you want to permanently delete the inventory source below from the inventory?
' + $filter('sanitize')(inventory_source.name) + '
'; + var body = '
Are you sure you want to permanently delete the inventory source below from the inventory? Hosts generated by this inventory source will also be deleted.
' + $filter('sanitize')(inventory_source.name) + '
'; var action = function(){ delete $rootScope.promptActionBtnClass; Wait('start'); From 11f3f057f6bf7594c569cf7563de45033235e520 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 27 Apr 2017 17:45:37 -0700 Subject: [PATCH 41/49] related groups for host-edit form --- .../src/inventories/groups/groups.form.js | 4 +--- .../hosts/edit/host-edit.controller.js | 4 ++++ .../client/src/inventories/hosts/host.form.js | 18 +++++++++++++++++- .../related-hosts/related-host.form.js | 17 ++++++++++++++++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/inventories/groups/groups.form.js b/awx/ui/client/src/inventories/groups/groups.form.js index 308d3555d8..4f6a178a71 100644 --- a/awx/ui/client/src/inventories/groups/groups.form.js +++ b/awx/ui/client/src/inventories/groups/groups.form.js @@ -88,9 +88,7 @@ function(i18n, nestedGroupListState, nestedHostsListState, buildHostAddState){ includeForm: "NestedGroupFormDefinition", title: i18n._('Groups'), iterator: 'nested_group', - listState: nestedGroupListState, - // addState: buildGroupsAddState, - // editState: buildGroupsEditState + listState: nestedGroupListState }, nested_hosts: { name: 'nested_hosts', 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 index 250c65e4d7..ab4ab233e4 100644 --- a/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js +++ b/awx/ui/client/src/inventories/hosts/edit/host-edit.controller.js @@ -20,6 +20,10 @@ $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, diff --git a/awx/ui/client/src/inventories/hosts/host.form.js b/awx/ui/client/src/inventories/hosts/host.form.js index 350e2c11cf..fdc8df1d56 100644 --- a/awx/ui/client/src/inventories/hosts/host.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'), @@ -107,6 +108,21 @@ export default ['i18n', function(i18n) { 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/related-hosts/related-host.form.js b/awx/ui/client/src/inventories/related-hosts/related-host.form.js index 00d9d284d9..9e7d30927b 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.form.js +++ b/awx/ui/client/src/inventories/related-hosts/related-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'), @@ -108,6 +109,20 @@ export default ['i18n', function(i18n) { 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 } } }; From fcc70f69fa17e5b015f8d2c45c972fd799107860 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 28 Apr 2017 14:49:17 -0400 Subject: [PATCH 42/49] Inventory sources socket work. Refactoring sources/factories. Small fixes to inventory sync stdout --- .../groups/add/groups-add.controller.js | 4 +- .../groups/edit/groups-edit.controller.js | 4 +- .../get-source-type-options.factory.js | 37 --------- .../factories/get-sync-status-msg.factory.js | 77 ------------------ .../factories/groups-cancel-update.factory.js | 81 ------------------- .../factories/view-update-status.factory.js | 46 ----------- .../groups/list/groups-list.controller.js | 53 ++---------- awx/ui/client/src/inventories/groups/main.js | 8 -- .../nested-groups-list.controller.js | 53 ++---------- .../factories/cancel-source-update.factory.js | 63 +++++++++++++++ .../factories/get-hosts-status-msg.factory.js | 33 -------- .../factories/get-sync-status-msg.factory.js | 20 +---- .../factories/groups-cancel-update.factory.js | 81 ------------------- .../factories/view-update-status.factory.js | 31 +++---- .../list/build-sources-list-state.factory.js | 7 ++ .../sources/list/sources-list.controller.js | 63 ++++++++++----- awx/ui/client/src/inventories/sources/main.js | 8 ++ .../standard-out-inventory-sync.partial.html | 9 --- .../lookup-name.factory.js | 7 +- 19 files changed, 146 insertions(+), 539 deletions(-) delete mode 100644 awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js delete mode 100644 awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js delete mode 100644 awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js delete mode 100644 awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js create mode 100644 awx/ui/client/src/inventories/sources/factories/cancel-source-update.factory.js delete mode 100644 awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js delete mode 100644 awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js 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 index 6b5f849b40..ae37692ba1 100644 --- a/awx/ui/client/src/inventories/groups/add/groups-add.controller.js +++ b/awx/ui/client/src/inventories/groups/add/groups-add.controller.js @@ -6,11 +6,11 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'GroupManageService', - 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', + 'GetChoices', 'GetBasePath', 'CreateSelect2', 'rbacUiControlService', 'ToJSON', function($state, $stateParams, $scope, GroupForm, ParseTypeChange, GenerateForm, inventoryData, GroupManageService, GetChoices, - GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, + GetBasePath, CreateSelect2, rbacUiControlService, ToJSON) { let form = GroupForm; 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 index 27f444d69a..1335b4bb80 100644 --- a/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/groups/edit/groups-edit.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', - 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'groupData', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON, - ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData) { + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, groupData) { init(); diff --git a/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js deleted file mode 100644 index befef8a499..0000000000 --- a/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js +++ /dev/null @@ -1,37 +0,0 @@ -export default - function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { - return function(params) { - var scope = params.scope, - variable = params.variable; - - if (scope[variable] === undefined) { - scope[variable] = []; - Rest.setUrl(GetBasePath('inventory_sources')); - Rest.options() - .success(function (data) { - var i, choices = data.actions.GET.source.choices; - for (i = 0; i < choices.length; i++) { - if (choices[i][0] !== 'file') { - scope[variable].push({ - label: choices[i][1], - value: choices[i][0] - }); - } - } - scope.cloudCredentialRequired = false; - scope.$emit('sourceTypeOptionsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status - }); - }); - } - }; - } - -GetSourceTypeOptions.$inject = - [ 'Rest', - 'ProcessErrors', - 'GetBasePath' - ]; diff --git a/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js deleted file mode 100644 index 2541abcc27..0000000000 --- a/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js +++ /dev/null @@ -1,77 +0,0 @@ -export default - function GetSyncStatusMsg(Empty) { - 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', - stat, stat_class, status_tip; - - stat = status; - stat_class = stat; - - switch (status) { - case 'never updated': - stat = 'never'; - stat_class = 'na'; - status_tip = 'Sync not performed. Click to start it now.'; - break; - case 'none': - case 'ok': - case '': - launch_class = 'btn-disabled'; - stat = 'n/a'; - stat_class = 'na'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - break; - case 'canceled': - status_tip = 'Sync canceled. Click to view log.'; - break; - case 'failed': - status_tip = 'Sync failed. Click to view log.'; - break; - case 'successful': - status_tip = 'Sync completed. Click to view log.'; - break; - case 'pending': - status_tip = 'Sync pending.'; - launch_class = "btn-disabled"; - launch_tip = "Sync pending"; - break; - case 'updating': - case 'running': - launch_class = "btn-disabled"; - launch_tip = "Sync running"; - status_tip = "Sync running. Click to view log."; - 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, - "status": stat, - "launch_class": launch_class, - "launch_tip": launch_tip, - "schedule_tip": schedule_tip - }; - }; - } - -GetSyncStatusMsg.$inject = - [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js deleted file mode 100644 index 1447d0aa1c..0000000000 --- a/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js +++ /dev/null @@ -1,81 +0,0 @@ -export default - function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { - return function(params) { - var scope = params.scope, - id = params.id, - group = params.group; - - if (scope.removeCancelUpdate) { - scope.removeCancelUpdate(); - } - scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { - // 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 - }); - }); - }); - - if (scope.removeCheckCancel) { - scope.removeCheckCancel(); - } - scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { - // Check that we have access to cancelling an update - var url = (current_update) ? current_update : last_update; - url += 'cancel/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - if (data.can_cancel) { - scope.$emit('CancelUpdate', url); - //} else { - // Wait('stop'); - // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + - // 'the latest status.', 'alert-info'); - } - else { - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET status: ' + status - }); - }); - }); - - // Cancel the update process - if (Empty(group)) { - group = Find({ list: scope.groups, key: 'id', val: id }); - scope.selected_group_id = group.id; - } - - if (group && (group.status === 'running' || group.status === 'pending')) { - // We found the group, and there is a running update - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status - }); - }); - } - }; - } - -GroupsCancelUpdate.$inject = - [ 'Empty', 'Rest', 'ProcessErrors', - 'Alert', 'Wait', 'Find' - ]; diff --git a/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js deleted file mode 100644 index 1f3280b51c..0000000000 --- a/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js +++ /dev/null @@ -1,46 +0,0 @@ -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 }); - - 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") { - Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + - 'clicking the button.
', 'alert-info', null, null, null, null, true); - } else { - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('SourceReady', data); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' GET returned status: ' + status }); - }); - } - } - }; - } - -ViewUpdateStatus.$inject = - [ '$state', 'Rest', 'ProcessErrors', - 'Alert', 'Wait', 'Empty', 'Find' - ]; diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 68b7da4f35..7cfd4cdb01 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -5,11 +5,11 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'GroupList', 'InventoryUpdate', - 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', - 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + 'GroupManageService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', + 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', function($scope, $rootScope, $state, $stateParams, GroupList, InventoryUpdate, - GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, - GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + GroupManageService, CancelSourceUpdate, rbacUiControlService, GetBasePath, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ let list = GroupList; @@ -144,51 +144,8 @@ })); }; - $scope.$on(`ws-jobs`, function(e, data){ - var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); - - if (group === undefined || group === null) { - group = {}; - } - - if(data.status === 'failed' || data.status === 'successful'){ - let path; - if($stateParams && $stateParams.group && $stateParams.group.length > 0) { - path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; - } - else { - //reaches here if the user is on the root level group - path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; - } - 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; - _.forEach($scope[list.name], buildStatusIndicators); - }); - } else { - var status = GetSyncStatusMsg({ - status: data.status, - has_inventory_sources: group.has_inventory_sources, - source: group.source - }); - group.status = data.status; - group.status_class = status.class; - group.status_tooltip = status.tooltip; - group.launch_tooltip = status.launch_tip; - group.launch_class = status.launch_class; - } - }); - $scope.cancelUpdate = function (id) { - GroupsCancelUpdate({ scope: $scope, id: id }); - }; - - $scope.viewUpdateStatus = function (id) { - ViewUpdateStatus({ - scope: $scope, - group_id: id - }); + CancelSourceUpdate({ scope: $scope, id: id }); }; $scope.copyMoveGroup = function(id){ diff --git a/awx/ui/client/src/inventories/groups/main.js b/awx/ui/client/src/inventories/groups/main.js index d5a40f97ff..ab66437ace 100644 --- a/awx/ui/client/src/inventories/groups/main.js +++ b/awx/ui/client/src/inventories/groups/main.js @@ -13,10 +13,6 @@ import groupFormDefinition from './groups.form'; import groupListDefinition from './groups.list'; import service from './groups.service'; 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'; export default angular.module('group', [ @@ -29,8 +25,4 @@ export default .factory('GroupForm', groupFormDefinition) .value('GroupList', groupListDefinition) .factory('GetHostsStatusMsg', GetHostsStatusMsg) - .factory('GetSourceTypeOptions', GetSourceTypeOptions) - .factory('GetSyncStatusMsg', GetSyncStatusMsg) - .factory('GroupsCancelUpdate', GroupsCancelUpdate) - .factory('ViewUpdateStatus', ViewUpdateStatus) .service('GroupManageService', service); diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index 3a13d3b64c..237757efa8 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -5,11 +5,11 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'NestedGroupListDefinition', 'InventoryUpdate', - 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', - 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', + 'GroupManageService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', + 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', function($scope, $rootScope, $state, $stateParams, NestedGroupListDefinition, InventoryUpdate, - GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, - GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ + GroupManageService, CancelSourceUpdate, rbacUiControlService, GetBasePath, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData){ let list = NestedGroupListDefinition; @@ -131,51 +131,8 @@ })); }; - $scope.$on(`ws-jobs`, function(e, data){ - var group = Find({ list: $scope.groups, key: 'id', val: data.group_id }); - - if (group === undefined || group === null) { - group = {}; - } - - if(data.status === 'failed' || data.status === 'successful'){ - let path; - if($stateParams && $stateParams.group && $stateParams.group.length > 0) { - path = GetBasePath('groups') + _.last($stateParams.group) + '/children'; - } - else { - //reaches here if the user is on the root level group - path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; - } - 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; - _.forEach($scope[list.name], buildStatusIndicators); - }); - } else { - var status = GetSyncStatusMsg({ - status: data.status, - has_inventory_sources: group.has_inventory_sources, - source: group.source - }); - group.status = data.status; - group.status_class = status.class; - group.status_tooltip = status.tooltip; - group.launch_tooltip = status.launch_tip; - group.launch_class = status.launch_class; - } - }); - $scope.cancelUpdate = function (id) { - GroupsCancelUpdate({ scope: $scope, id: id }); - }; - - $scope.viewUpdateStatus = function (id) { - ViewUpdateStatus({ - scope: $scope, - group_id: id - }); + CancelSourceUpdate({ scope: $scope, id: id }); }; // $scope.$parent governed by InventoryManageController, for unified multiSelect options 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/sources/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js deleted file mode 100644 index 19a846c414..0000000000 --- a/awx/ui/client/src/inventories/sources/factories/get-hosts-status-msg.factory.js +++ /dev/null @@ -1,33 +0,0 @@ -export default - function GetHostsStatusMsg() { - return function(params) { - var active_failures = params.active_failures, - total_hosts = params.total_hosts, - tip, failures, html_class; - - // Return values for use on host status indicator - - if (active_failures > 0) { - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; - html_class = 'error'; - failures = true; - } else { - failures = false; - if (total_hosts === 0) { - // no hosts - tip = "Contains 0 hosts."; - html_class = 'none'; - } else { - // many hosts with 0 failures - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; - html_class = 'success'; - } - } - - return { - tooltip: tip, - failures: failures, - 'class': html_class - }; - }; - } diff --git a/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/sources/factories/get-sync-status-msg.factory.js index 2541abcc27..efb393caf8 100644 --- a/awx/ui/client/src/inventories/sources/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/sources/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js deleted file mode 100644 index 1447d0aa1c..0000000000 --- a/awx/ui/client/src/inventories/sources/factories/groups-cancel-update.factory.js +++ /dev/null @@ -1,81 +0,0 @@ -export default - function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { - return function(params) { - var scope = params.scope, - id = params.id, - group = params.group; - - if (scope.removeCancelUpdate) { - scope.removeCancelUpdate(); - } - scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { - // 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 - }); - }); - }); - - if (scope.removeCheckCancel) { - scope.removeCheckCancel(); - } - scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { - // Check that we have access to cancelling an update - var url = (current_update) ? current_update : last_update; - url += 'cancel/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - if (data.can_cancel) { - scope.$emit('CancelUpdate', url); - //} else { - // Wait('stop'); - // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + - // 'the latest status.', 'alert-info'); - } - else { - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET status: ' + status - }); - }); - }); - - // Cancel the update process - if (Empty(group)) { - group = Find({ list: scope.groups, key: 'id', val: id }); - scope.selected_group_id = group.id; - } - - if (group && (group.status === 'running' || group.status === 'pending')) { - // We found the group, and there is a running update - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status - }); - }); - } - }; - } - -GroupsCancelUpdate.$inject = - [ 'Empty', 'Rest', 'ProcessErrors', - 'Alert', 'Wait', 'Find' - ]; diff --git a/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/sources/factories/view-update-status.factory.js index 1f3280b51c..5a746aa075 100644 --- a/awx/ui/client/src/inventories/sources/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', '
An inventory sync has not been performed for the selected group. Start the process by ' + 'clicking the button.
', 'alert-info', null, null, null, null, true); } else { Wait('start'); - Rest.setUrl(group.related.inventory_source); + Rest.setUrl(inventory_source.url); Rest.get() .success(function (data) { - scope.$emit('SourceReady', data); + // Get the ID from the correct summary field + var update_id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; + + $state.go('inventorySyncStdout', {id: update_id}); }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + msg: 'Failed to retrieve inventory source: ' + inventory_source.url + ' GET returned status: ' + status }); }); } diff --git a/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js index eb124f2842..71792e273d 100644 --- a/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js +++ b/awx/ui/client/src/inventories/sources/list/build-sources-list-state.factory.js @@ -14,6 +14,13 @@ export default ['SourcesListDefinition', '$stateExtender', 'templateUrl', '$inje searchPrefix: `${list.iterator}`, name: `${formStateDefinition.name}.${list.iterator}s`, url: `/${list.iterator}s`, + data: { + socket: { + "groups": { + "jobs": ["status_changed"] + } + } + }, ncyBreadcrumb: { parent: `${formStateDefinition.name}`, label: `${breadcrumbLabel}` diff --git a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js index e6cfe4a51b..8e260c0590 100644 --- a/awx/ui/client/src/inventories/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories/sources/list/sources-list.controller.js @@ -5,14 +5,14 @@ *************************************************/ export default ['$scope', '$rootScope', '$state', '$stateParams', 'SourcesListDefinition', - 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', + 'InventoryUpdate', 'GroupManageService', 'CancelSourceUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath', - 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', + 'GetSyncStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', '$filter', 'Prompt', 'Wait', 'SourcesService', function($scope, $rootScope, $state, $stateParams, SourcesListDefinition, - InventoryUpdate, GroupManageService, GroupsCancelUpdate, + InventoryUpdate, GroupManageService, CancelSourceUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg, - GetHostsStatusMsg, Dataset, Find, qs, inventoryData, $filter, Prompt, + Dataset, Find, qs, inventoryData, $filter, Prompt, Wait, SourcesService){ let list = SourcesListDefinition; @@ -20,14 +20,14 @@ init(); function init(){ - $scope.inventory_id = $stateParams.inventory_id; - $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; - $scope.canAdd = false; + $scope.inventory_id = $stateParams.inventory_id; + $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; + $scope.canAdd = false; - rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); // Search init $scope.list = list; @@ -43,6 +43,33 @@ $scope.inventory_id = $stateParams.inventory_id; _.forEach($scope[list.name], buildStatusIndicators); + $scope.$on(`ws-jobs`, function(e, data){ + var inventory_source = Find({ list: $scope.inventory_sources, key: 'id', val: data.inventory_source_id }); + + if (inventory_source === undefined || inventory_source === null) { + inventory_source = {}; + } + + if(data.status === 'failed' || data.status === 'successful'){ + let path = GetBasePath('inventory') + $stateParams.inventory_id + '/inventory_sources'; + + 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; + _.forEach($scope[list.name], buildStatusIndicators); + }); + } else { + var status = GetSyncStatusMsg({ + status: data.status + }); + inventory_source.status = data.status; + inventory_source.status_class = status.class; + inventory_source.status_tooltip = status.tooltip; + inventory_source.launch_tooltip = status.launch_tip; + inventory_source.launch_class = status.launch_class; + } + }); } function buildStatusIndicators(inventory_source){ @@ -50,27 +77,19 @@ inventory_source = {}; } - let inventory_source_status, hosts_status; + let inventory_source_status; inventory_source_status = GetSyncStatusMsg({ status: inventory_source.status, has_inventory_sources: inventory_source.has_inventory_sources, source: ( (inventory_source) ? inventory_source.source : null ) }); - hosts_status = GetHostsStatusMsg({ - active_failures: inventory_source.hosts_with_active_failures, - total_hosts: inventory_source.total_hosts, - inventory_id: $scope.inventory_id, - // group_id: group.id - }); _.assign(inventory_source, {status_class: inventory_source_status.class}, {status_tooltip: inventory_source_status.tooltip}, {launch_tooltip: inventory_source_status.launch_tip}, {launch_class: inventory_source_status.launch_class}, {group_schedule_tooltip: inventory_source_status.schedule_tip}, - {hosts_status_tip: hosts_status.tooltip}, - {hosts_status_class: hosts_status.class}, {source: inventory_source ? inventory_source.source : null}, {status: inventory_source ? inventory_source.status : null}); } @@ -114,12 +133,12 @@ }; $scope.cancelUpdate = function (id) { - GroupsCancelUpdate({ scope: $scope, id: id }); + CancelSourceUpdate({ scope: $scope, id: id }); }; $scope.viewUpdateStatus = function (id) { ViewUpdateStatus({ scope: $scope, - group_id: id + inventory_source_id: id }); }; $scope.scheduleSource = function(id) { diff --git a/awx/ui/client/src/inventories/sources/main.js b/awx/ui/client/src/inventories/sources/main.js index bb515628b7..e2ca31f6a0 100644 --- a/awx/ui/client/src/inventories/sources/main.js +++ b/awx/ui/client/src/inventories/sources/main.js @@ -10,6 +10,10 @@ import sourcesEdit from './edit/main'; import sourcesFormDefinition from './sources.form'; import sourcesListDefinition from './sources.list'; import service from './sources.service'; +import GetSyncStatusMsg from './factories/get-sync-status-msg.factory'; +import ViewUpdateStatus from './factories/view-update-status.factory'; +import CancelSourceUpdate from './factories/cancel-source-update.factory'; +import GetSourceTypeOptions from './factories/get-source-type-options.factory'; export default angular.module('sources', [ @@ -19,4 +23,8 @@ export default ]) .value('SourcesFormDefinition', sourcesFormDefinition) .value('SourcesListDefinition', sourcesListDefinition) + .factory('GetSyncStatusMsg', GetSyncStatusMsg) + .factory('ViewUpdateStatus', ViewUpdateStatus) + .factory('CancelSourceUpdate', CancelSourceUpdate) + .factory('GetSourceTypeOptions', GetSourceTypeOptions) .service('SourcesService', service); diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index 5df1bd84c7..e5b0b17baf 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -85,15 +85,6 @@
- -
SOURCE
diff --git a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js b/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js index 40d32b3f8d..319ff0916b 100644 --- a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js +++ b/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js @@ -14,16 +14,11 @@ Rest.get() .success(function(data) { if (scope_var === 'inventory_source') { - scope[scope_var + '_name'] = data.summary_fields.group.name; scope.inventory = data.inventory; } - else if (!Empty(data.name)) { + if (!Empty(data.name)) { scope[scope_var + '_name'] = data.name; } - if (!Empty(data.group)) { - // Used for inventory_source - scope.group = data.group; - } }) .error(function(data, status) { if (status === 403 && params.ignore_403) { From e3adc46e306e1836dfe0e6ad259d06737f829923 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 1 May 2017 11:24:54 -0400 Subject: [PATCH 43/49] Fixed bug adding a normal inventory --- .../src/inventories/add/inventory-add.controller.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 dd965bd362..fce7e26dc1 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) { From a20805d50c3056fdb3a44a5411bb7a7304aaf236 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 1 May 2017 11:42:35 -0400 Subject: [PATCH 44/49] Fixed redirects after adding an inventory and an inventory source --- .../add/inventory-add.controller.js | 2 +- .../sources/add/sources-add.controller.js | 41 ++----------------- 2 files changed, 5 insertions(+), 38 deletions(-) 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 fce7e26dc1..7fb71a9edb 100644 --- a/awx/ui/client/src/inventories/add/inventory-add.controller.js +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -75,7 +75,7 @@ function InventoriesAdd($scope, $location, .success(function(data) { var inventory_id = data.id; Wait('stop'); - $state.go('inventories.edit', {inventory_id: inventory_id}); + $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/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js index 15813fe765..ad98656eec 100644 --- a/awx/ui/client/src/inventories/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories/sources/add/sources-add.controller.js @@ -76,43 +76,10 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', } 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 || '': - // GroupManageService.post(group).then(res => { - // // associate - // if ($stateParams.group) { - // return GroupManageService.associateGroup(res.data, _.last($stateParams.group)) - // .then(() => $state.go('^', null, { reload: true })); - // } else { - // $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.post(group) - // // associate to group - // .then(res => { - // if ($stateParams.group) { - // GroupManageService.associateGroup(res.data, _.last($stateParams.group)); - // return res; - // } else { - // return res; } - // // pass the original POST response and not the association response - // }) - // .then(res => GroupManageService.putInventorySource( - // // put the received group ID into inventory source payload - // // and pass the related endpoint - // _.assign(params, { group: res.data.id }), res.data.related.inventory_source)) - // .then(res => $state.go('inventoryManage.editGroup', { group_id: res.data.group }, { reload: true })); - SourcesService.post(params).then(function(){ - $state.go('^.edit', null, {reload: true}); - }); - // break; - // } + 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; From 8f118835534fd41d4eb34302c91f60b814516915 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 27 Apr 2017 18:07:51 -0700 Subject: [PATCH 45/49] Adding insights routes and files --- .../inventories/insights/insights.block.less | 57 +++++++++++++++++++ .../insights/insights.controller.js | 16 ++++++ .../insights/insights.partial.html | 20 +++++++ .../client/src/inventories/insights/main.js | 11 ++++ awx/ui/client/src/inventories/main.js | 38 ++++++++++++- 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 awx/ui/client/src/inventories/insights/insights.block.less create mode 100644 awx/ui/client/src/inventories/insights/insights.controller.js create mode 100644 awx/ui/client/src/inventories/insights/insights.partial.html create mode 100644 awx/ui/client/src/inventories/insights/main.js 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..2fd0cfc4ea --- /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 ['$scope', 'Facts', 'ParseTypeChange', 'ParseVariableString', +function ($scope, Facts, ParseTypeChange, ParseVariableString) { + + 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 @@ +
+
+
Total Issues
+ 4 +
Critical
+ 1 +
High
+ 1 +
Medium
+ 1 +
Low
+ 1 +
+
+
Solvable With Playbook
+ 4 +
Not Solvable With Playbook
+ 1 +
+
diff --git a/awx/ui/client/src/inventories/insights/main.js b/awx/ui/client/src/inventories/insights/main.js new file mode 100644 index 0000000000..75e05fd1eb --- /dev/null +++ b/awx/ui/client/src/inventories/insights/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './insights.controller'; + +export default +angular.module('insightsDashboard', []) + .controller('InsightsController', controller); diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index 5d49269f82..8db43d4af6 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -20,6 +20,7 @@ import InventoryForm from './inventory.form'; import InventoryManageService from './inventory-manage.service'; import adHocRoute from './adhoc/adhoc.route'; import ansibleFacts from './ansible_facts/main'; +import insights from './insights/main'; import { copyMoveGroupRoute, copyMoveHostRoute } from './copy-move/copy-move.route'; import copyMove from './copy-move/main'; export default @@ -34,6 +35,7 @@ angular.module('inventory', [ inventoryEdit.name, inventoryList.name, ansibleFacts.name, + insights.name, copyMove.name ]) .factory('InventoryForm', InventoryForm) @@ -72,6 +74,34 @@ angular.module('inventory', [ }; } + function insightsConfig(stateName) { + return { + name: stateName, + url: '/insights', + ncyBreadcrumb: { + label: N_("INSIGHTS") + }, + views: { + 'related': { + controller: 'InsightsController', + templateUrl: templateUrl('inventories/insights/insights') + } + }, + resolve: { + Facts: ['$stateParams', 'GetBasePath', 'Rest', + function($stateParams, GetBasePath, Rest) { + let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; + Rest.setUrl(ansibleFactsUrl); + return Rest.get() + .success(function(data) { + return data; + }); + } + ] + } + }; + } + function generateInventoryStates() { let basicInventoryAdd = stateDefinitions.generateTree({ @@ -257,6 +287,8 @@ angular.module('inventory', [ let relatedHostsAnsibleFacts = factsConfig('inventories.edit.hosts.edit.ansible_facts'); let nestedHostsAnsibleFacts = factsConfig('inventories.edit.groups.edit.nested_hosts.edit.ansible_facts'); + let relatedHostsInsights = insightsConfig('inventories.edit.hosts.edit.insights'); + let nestedHostsInsights = insightsConfig('inventories.edit.groups.edit.nested_hosts.edit.insights'); return Promise.all([ basicInventoryAdd, @@ -306,6 +338,8 @@ angular.module('inventory', [ stateExtender.buildDefinition(editSchedule), stateExtender.buildDefinition(relatedHostsAnsibleFacts), stateExtender.buildDefinition(nestedHostsAnsibleFacts), + stateExtender.buildDefinition(relatedHostsInsights), + stateExtender.buildDefinition(nestedHostsInsights), stateExtender.buildDefinition(copyMoveGroupRoute), stateExtender.buildDefinition(copyMoveHostRoute) ]) @@ -359,6 +393,7 @@ angular.module('inventory', [ }); let hostAnsibleFacts = factsConfig('hosts.edit.ansible_facts'); + let hostInsights = insightsConfig('hosts.edit.insights'); return Promise.all([ hostTree @@ -367,7 +402,8 @@ angular.module('inventory', [ states: _.reduce(generated, (result, definition) => { return result.concat(definition.states); }, [ - stateExtender.buildDefinition(hostAnsibleFacts) + stateExtender.buildDefinition(hostAnsibleFacts), + stateExtender.buildDefinition(hostInsights) ]) }; }); From d0985f742cc323d72839a3ea5f2ec3d777552c92 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 1 May 2017 14:22:45 -0400 Subject: [PATCH 46/49] Adhoc command button tooltip fix --- .../src/inventories/groups/groups.list.js | 1 - .../groups/nested-groups/nested-groups.list.js | 2 +- .../groups/nested-hosts/nested-hosts.list.js | 2 +- .../related-hosts/related-host.list.js | 1 - .../src/inventories/sources/sources.list.js | 18 ------------------ 5 files changed, 2 insertions(+), 22 deletions(-) diff --git a/awx/ui/client/src/inventories/groups/groups.list.js b/awx/ui/client/src/inventories/groups/groups.list.js index 816a48d92b..412dd737ee 100644 --- a/awx/ui/client/src/inventories/groups/groups.list.js +++ b/awx/ui/client/src/inventories/groups/groups.list.js @@ -50,7 +50,6 @@ export default { 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.", - dataTipWatch: "adhocCommandTooltip", dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js index f474e7d595..082eb3bfad 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups.list.js @@ -52,7 +52,7 @@ export default { ngDisabled: '!groupsSelected', ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", - dataTipWatch: "adhocCommandTooltip", + dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: 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 index 7234327573..f713e091a9 100644 --- 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 @@ -88,7 +88,7 @@ export default { ngDisabled: '!hostsSelected', ngClick: 'setAdhocPattern()', awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.", - dataTipWatch: "adhocCommandTooltip", + dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', showTipWhenDisabled: true, diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js index 1b0a970086..3a83afc195 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -88,7 +88,6 @@ export default { 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.", - dataTipWatch: "adhocCommandTooltip", dataPlacement: 'top', actionClass: 'btn List-buttonDefault', buttonContent: 'RUN COMMANDS', diff --git a/awx/ui/client/src/inventories/sources/sources.list.js b/awx/ui/client/src/inventories/sources/sources.list.js index c8c44bf571..157477bdd1 100644 --- a/awx/ui/client/src/inventories/sources/sources.list.js +++ b/awx/ui/client/src/inventories/sources/sources.list.js @@ -47,24 +47,6 @@ export default { actionClass: 'btn List-buttonDefault', buttonContent: 'REFRESH' }, - // 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", - // 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: "createSource()", From b7f3b7703f0302921f5f3568087a209476ece32f Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 1 May 2017 15:13:09 -0400 Subject: [PATCH 47/49] fixing jshint --- awx/ui/client/src/inventories/insights/insights.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories/insights/insights.controller.js b/awx/ui/client/src/inventories/insights/insights.controller.js index 2fd0cfc4ea..e892b62387 100644 --- a/awx/ui/client/src/inventories/insights/insights.controller.js +++ b/awx/ui/client/src/inventories/insights/insights.controller.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default ['$scope', 'Facts', 'ParseTypeChange', 'ParseVariableString', -function ($scope, Facts, ParseTypeChange, ParseVariableString) { +export default [ +function () { function init() { // $scope.insights From 4820a3dca3447aeec842a56f3951a4ba389ec81a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Mon, 1 May 2017 15:49:56 -0400 Subject: [PATCH 48/49] Removed inventory sync status from inventory list --- awx/ui/client/legacy-styles/ansible-ui.less | 1 - awx/ui/client/src/inventories/inventory.list.js | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) 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/src/inventories/inventory.list.js b/awx/ui/client/src/inventories/inventory.list.js index da0c21351f..201b9b9138 100644 --- a/awx/ui/client/src/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories/inventory.list.js @@ -22,18 +22,12 @@ export default ['i18n', function(i18n) { fields: { status: { label: '', - columnClass: 'List-staticColumn--mediumStatus', + columnClass: 'col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', nosort: true, ngClick: "null", iconOnly: true, excludeModal: true, icons: [{ - icon: "{{ 'icon-cloud-' + inventory.syncStatus }}", - awToolTip: "{{ inventory.syncTip }}", - awTipPlacement: "right", - ngClick: "showGroupSummary($event, inventory.id)", - ngClass: "inventory.launch_class" - },{ icon: "{{ 'icon-job-' + inventory.hostsStatus }}", awToolTip: false, ngClick: "showHostSummary($event, inventory.id)", @@ -43,7 +37,7 @@ export default ['i18n', function(i18n) { name: { key: true, label: i18n._('Name'), - columnClass: 'col-md-5 col-sm-5 col-xs-8 List-staticColumnAdjacent', + columnClass: 'col-md-5 col-sm-4 col-xs-6 List-staticColumnAdjacent', modalColumnClass: 'col-md-11', awToolTip: "{{ inventory.description }}", awTipPlacement: "top", @@ -56,7 +50,7 @@ export default ['i18n', function(i18n) { sourceModel: 'organization', sourceField: 'name', excludeModal: true, - columnClass: 'col-md-5 col-sm-3 hidden-xs' + columnClass: 'col-md-4 col-sm-2 hidden-xs' } }, From 6de87b910ba011368b655ba4282693f5b57ffb02 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 1 May 2017 16:14:08 -0400 Subject: [PATCH 49/49] adding host resolve for nested groups route --- .../nested-groups/nested-groups-list-state.factory.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 index 7efd37c391..26763ee007 100644 --- 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 @@ -66,8 +66,15 @@ export default ['$stateExtender', 'templateUrl', '$injector', 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.data.summary_fields.inventory.id; + var id = ($stateParams.inventory_id) ? $stateParams.inventory_id : host.summary_fields.inventory.id; return InventoryManageService.getInventory(id).then(res => res.data); }] }