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 = '
" + + i18n._("Indicates if a host is available and should be included in running jobs.") + + "
" + + i18n._("For hosts that are part of an external" + + " inventory, this flag cannot be changed. It will be" + + " set by the inventory sync process.") + + "
", + dataTitle: i18n._('Host Enabled'), + ngDisabled: 'host.has_inventory_sources' + } + }, + fields: { + name: { + label: i18n._('Host Name'), + type: 'text', + required: true, + awPopOver: "" + + i18n._("Provide a host name, ip address, or ip address:port. Examples include:") + + "
" + + "myserver.domain.com", + dataTitle: i18n._('Host Name'), + dataPlacement: 'right', + dataContainer: 'body', + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + description: { + label: i18n._('Description'), + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)', + type: 'text' + }, + variables: { + label: i18n._('Variables'), + type: 'textarea', + rows: 6, + class: 'Form-formGroup--fullWidth', + "default": "---", + awPopOver: "
" + + "127.0.0.1
" + + "10.1.0.140:25
" + + "server.example.com:25" + + "
" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "
" + + "JSON:{\n" + + "YAML:
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
---\n" + + '
somevar: somevalue
password: magic
' + i18n.sprintf(i18n._('View JSON examples at %s'), 'www.json.org') + '
' + + '' + i18n.sprintf(i18n._('View YAML examples at %s'), 'docs.ansible.com') + '
', + dataTitle: i18n._('Host Variables'), + dataPlacement: 'right', + dataContainer: 'body' + }, + inventory: { + type: 'hidden', + includeOnEdit: true, + includeOnAdd: true + } + }, + + buttons: { + cancel: { + ngClick: 'formCancel()', + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + }, + close: { + ngClick: 'formCancel()', + ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' + }, + save: { + ngClick: 'formSave()', + ngDisabled: true, + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + } + }, + }; + }]; 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);