From 9d7018767dcb748597245b7ff4dbb064b8548c4d Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Thu, 15 Aug 2013 04:11:09 -0400 Subject: [PATCH] AC-331 Custom tree on Inventories detail Hosts tab is starting to work well. --- awx/ui/static/js/controllers/Inventories.js | 29 ++- awx/ui/static/js/helpers/Hosts.js | 37 +++- awx/ui/static/js/helpers/inventory.js | 2 +- awx/ui/static/js/helpers/refresh-related.js | 3 + awx/ui/static/less/ansible-ui.less | 213 +++++++++----------- awx/ui/static/lib/ansible/directives.js | 189 ++++++++++++++++- awx/ui/static/lib/ansible/form-generator.js | 8 + awx/ui/templates/ui/index.html | 4 +- 8 files changed, 347 insertions(+), 138 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 3221be607a..f18b0c69b2 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -185,7 +185,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, OrganizationList, TreeInit, GetBasePath, GroupsList, GroupsAdd, GroupsEdit, LoadInventory, GroupsDelete, HostsList, HostsAdd, HostsEdit, HostsDelete, RefreshGroupName, ParseTypeChange, - HostsReload, EditInventory, RefreshTree) + HostsReload, EditInventory, RefreshTree, LoadSearchTree) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -201,21 +201,26 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP $('#inventory-tabs a:first').tab('show'); //activate the groups tab - scope.inventoryParseType = 'yaml'; + scope['inventoryParseType'] = 'yaml'; scope['inventory_id'] = id; - + scope['inventoryFailureFilter'] = false; + // Retrieve each related set and any lookups if (scope.inventoryLoadedRemove) { scope.inventoryLoadedRemove(); } scope.inventoryLoadedRemove = scope.$on('inventoryLoaded', function() { - scope.groupTitle = '

All Hosts

'; - scope.createButtonShow = false; - scope.search(scope.relatedSets['hosts'].iterator); TreeInit(scope.TreeParams); }); LoadInventory({ scope: scope, doPostSteps: true }); + $('#inventory-tabs a[href="#inventory-hosts"]').on('show.bs.tab', function() { + LoadSearchTree({ scope: scope, inventory_id: scope['inventory_id'] }); + HostsReload({ scope: scope, inventory_id: scope['inventory_id'], group_id: scope['group_id'] }); + if (!scope.$$phase) { + scope.$digest(); + } + }); scope.filterInventory = function() { RefreshTree({ scope: scope }); @@ -365,7 +370,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP scope['selectedNodeName'] = node.attr('name'); scope['selectedNodeName'] += (node.attr('data-failures') == 'true') ? ' ' + - '' : ''; + '' : ''; $('#tree-view').jstree('open_node',node); @@ -399,7 +404,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP scope.$digest(); } - HostsReload({ scope: scope, inventory_id: scope['inventory_id'], group_id: scope['group_id'] }); + //HostsReload({ scope: scope, inventory_id: scope['inventory_id'], group_id: scope['group_id'] }); }); @@ -455,6 +460,12 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP }); } + scope.showHosts = function(e) { + console.log('here'); + var elm = angular.elment(e.srcElement); + console.log('Need to show hosts: ' + elm.attr('data-hosts')); + } + } InventoriesEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'InventoryForm', @@ -462,6 +473,6 @@ InventoriesEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$l 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'OrganizationList', 'TreeInit', 'GetBasePath', 'GroupsList', 'GroupsAdd', 'GroupsEdit', 'LoadInventory', 'GroupsDelete', 'HostsList', 'HostsAdd', 'HostsEdit', 'HostsDelete', 'RefreshGroupName', - 'ParseTypeChange', 'HostsReload', 'EditInventory', 'RefreshTree' + 'ParseTypeChange', 'HostsReload', 'EditInventory', 'RefreshTree', 'LoadSearchTree' ]; diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index 62e586d254..8d14bfd167 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -413,7 +413,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H scope = params.scope; scope['hosts'] = null; - var url = (scope.group_id !== null) ? GetBasePath('groups') + scope.group_id + '/all_hosts/' : + var url = (scope.group_id !== null && scope.group_id !== undefined) ? GetBasePath('groups') + scope.group_id + '/all_hosts/' : GetBasePath('inventory') + params.inventory_id + '/hosts/'; var relatedSets = { hosts: { url: url, iterator: 'host' } }; @@ -433,12 +433,45 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H } } - params.scope.search('host'); + scope.search('host'); + if (!params.scope.$$phase) { params.scope.$digest(); } } + }]) + + .factory('LoadSearchTree', ['Rest', 'GetBasePath', 'ProcessErrors', '$compile', + function(Rest, GetBasePath, ProcessErrors, $compile) { + return function(params) { + + var scope = params.scope; + var inventory_id = params.inventory_id; + var newTree = []; + scope.searchTree = []; + + // Load the root node + Rest.setUrl (GetBasePath('inventory') + inventory_id + '/'); + Rest.get() + .success( function(data, status, headers, config) { + scope.searchTree.push({ + name: data.name, + description: data.description, + hosts: data.related.hosts, + failures: data.has_active_failures, + groups: data.related.root_groups, + children: [] + }); + scope.$emit('hostTabInit'); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status }); + }); + } }]); + + diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index ad1bbacbe8..ab961e9bb9 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -57,7 +57,7 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi description: data.results[i].description, inventory: data.results[i].inventory, all: data.results[i].related.all_hosts, - children: data.results[i].related.children, + children: data.results[i].related.children + '?' + filter + 'order_by=name', hosts: data.results[i].related.hosts, variable: data.results[i].related.variable_data, "data-failures": data.results[i].has_active_failures diff --git a/awx/ui/static/js/helpers/refresh-related.js b/awx/ui/static/js/helpers/refresh-related.js index 48adb23908..6333e7c2b4 100644 --- a/awx/ui/static/js/helpers/refresh-related.js +++ b/awx/ui/static/js/helpers/refresh-related.js @@ -33,6 +33,9 @@ angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities']) scope[iterator + 'PageCount'] = Math.ceil((data.count / scope[iterator + 'PageSize'])); scope[iterator + 'SearchSpin'] = false; scope[iterator + 'Loading'] = false; + if (!params.scope.$$phase) { + params.scope.$digest(); + } }) .error ( function(data, status, headers, config) { scope[iterator + 'SearchSpin'] = true; diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 6b23100d9c..0c98e17475 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -510,137 +510,112 @@ input[type="text"].job-successful { /* End Jobs Page */ -/* Inventory detail */ +/* Inventory Detail Groups tab */ + .inventory-content { + padding: 15px; + border: 1px solid #ddd; + border-radius: 6px; + } -.inventory-content { - padding: 15px; - border: 1px solid #ddd; - border-radius: 6px; -} + .groups-menu { + min-height: 30px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + + .nav a { + color: @blue-link; + font-size: 12px; + i { + font-size: 14px; + } + } -.groups-menu { - min-height: 30px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 6px; - - .nav a { - color: @blue-link; - font-size: 12px; - i { + .navbar-form { + display: inline-block; + float: right; + margin-top: 13px; + margin-left: 20px; + margin-right: 10px; + + label { + font-size: 12px; + line-height: normal; + } + input[type="checkbox"] { + margin-top: 0; + } + } + + /* the brand is't really a link */ + .navbar-brand { + color: @black; + text-align: left; font-size: 14px; - } + max-width: 100%; + i { + color: @red; + } + } + .navbar-brand:hover { + color: @black; + cursor: default; + } + + /* neither is the status spinner */ + .nav .status { + color: @black; + } + .nav .status:hover { + color: @black; + cursor: default; + } + } - .navbar-form { - display: inline-block; - float: right; - margin-top: 13px; - margin-left: 20px; - margin-right: 10px; - - label { - font-size: 12px; - line-height: normal; - } - input[type="checkbox"] { - margin-top: 0; - } + .tree-badge { + color: @red; + font-size: 12px; } - /* the brand is't really a link */ - .navbar-brand { - color: @black; - text-align: left; - font-size: 14px; - max-width: 100%; - i { - color: @red; - } - } - .navbar-brand:hover { - color: @black; - cursor: default; - } - - /* neither is the status spinner */ - .nav .status { - color: @black; - } - .nav .status:hover { - color: @black; - cursor: default; +/* Inventory Detail Hosts tab */ + + .hosts-well { + padding-top: 5px; + + .search-widget { + margin-top: 10px; + } + + .list-actions { + padding-top: 10px; + } } -} + .hosts-title p { + font-size: 12px; + } -.tree-badge { - color: @red; - font-size: 12px; -} + .hosts-title h4 { + margin: 5px 0; + } -.inventory-title { - margin-top: 15px; - font-weight: bold; - color: @blue-link; -} + .search-tree { + ul { + list-style-type: none; + padding-left: 10px; + } + ul:first-child { + padding-left: 0; + } + } -.inventory-buttons { - text-align: right; - background-color: #f5f5f5; - border-top: 1px solid #e3e3e3; - border-right: 1px solid #e3e3e3; - border-left: 1px solid #e3e3e3; - -webkit-border-top-right-radius: 4px; - -moz-border-top-right-radius: 4px; - border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; - -moz-border-top-left-radius: 4px; - border-top-left-radius: 4px; -} - -.inventory-buttons button { - margin: 5px 5px 3px 0; -} - -.inventory-filter { - padding: 0 3px 3px 3px; - text-align: right; - background-color: #f5f5f5; - border-right: 1px solid #e3e3e3; - border-bottom: 1px solid #e3e3e3; - border-left: 1px solid #e3e3e3; - -webkit-border-bottom-right-radius: 4px; - -moz-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; - -webkit-border-bottom-left-radius: 4px; - -moz-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -.inventory-filter label { - margin-right: 10px; -} - -.hosts-well { - padding-top: 5px; -} - -.hosts-title p { - font-size: 12px; -} - -.hosts-title h4 { - margin: 5px 0; -} - -.hosts-well .search-widget { - margin-top: 10px; -} - -.hosts-well .list-actions { - padding-top: 10px; -} + .search-tree .active { + background-color: #ddd; + padding: 1px 1px 1px 0; + border: 1px solid #ddd; + border-radius: 4px; + } .parse-selection { display: inline-block; diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index 95f3510114..d3f1a5740d 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -8,7 +8,7 @@ var INTEGER_REGEXP = /^\-?\d*$/; -angular.module('AWDirectives', ['RestServices']) +angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'HostsHelper']) // awpassmatch: Add to password_confirm field. Will test if value // matches that of 'input[name="password"]' .directive('awpassmatch', function() { @@ -312,13 +312,191 @@ angular.module('AWDirectives', ['RestServices']) if (disabled) { opts['disabled'] = true; } - $(elm).spinner(opts); + } + } + }]) - /*$('#' + name + '-number').change( function() { - $('#' + name + '-slider').slider('value', parseInt( $(this).val() )); - });*/ + .directive('awTree', ['Rest', 'ProcessErrors', 'Authorization', '$compile', '$rootScope', 'HostsReload', + function(Rest, ProcessErrors, Authorization, $compile, $rootScope, HostsReload) { + return { + //require: 'ngModel', + + replace: true, + + transclude: true, + + scope: { + treeData: '=awTree' + }, + + replace: true, + + template: + "
\n" + + "\n" + + "
\n", + link: function(scope, elm , attrs) { + + var idx=1000; + + function toggle(e) { + var id = (e.target.tagName == 'I') ? e.target.parentNode.attributes.id.value : e.target.attributes.id.value; + var elm = angular.element(document.getElementById(id)); + + function activate() { + /* Set the clicked node as active */ + $('.search-tree .active').removeClass('active'); + elm.addClass('active'); + var group = (elm.attr('data-group-id')) ? elm.attr('data-group-id') : null; + var parentScope = angular.element(document.getElementById('htmlTemplate')).scope(); + console.log('calling for group: ' + group); + HostsReload({ scope: parentScope, inventory_id: parentScope['inventory_id'], group_id: group }); + } + + /* Open/close the node and expand */ + if (scope.childrenLoadedRemove) { + scope.childrenLoadedRemove(); + } + scope.childrenLoadedRemove = scope.$on('childrenLoaded', function() { + childlists = elm.parent().find('ul'); //look for children + if (childlists && childlists.length > 0) { + // bind toggle() to click event of each link in the group we clicked on + var parent = angular.element(elm.parent()[0]); + var links = parent.find('a'); + for (var i=0; i < links.length; i++) { + var link = angular.element(links[i]); + link.unbind('click', toggle); + link.bind('click', toggle); + } + toggle(e); + } + else { + var icon = angular.element(elm.children()[0]); + icon.removeClass('icon-caret-down').removeClass('icon-caret-right').addClass('icon-ellipsis-horizontal'); + } + }); + + if (elm.attr('data-state') == 'closed') { + // expand the elment + var childlists = elm.parent().find('ul'); + if (childlists && childlists.length > 0) { + // already has childen + for (var i=0; i < childlists.length; i++) { + var listChild = angular.element(childlists[i]); + var listParent = angular.element(listChild.parent().find('a')[0]); + if (listParent.attr('id') == elm.attr('id')) { + angular.element(childlists[i]).removeClass('hidden'); + } + // all the children should be in a closed state + var aList = listChild.find('a'); + for (var j=0; j < aList.length; j++) { + var thisList = angular.element(aList[j]); + thisList.attr('data-state', 'closed'); + var icon = angular.element(thisList.children()[0]); + icon.removeClass('icon-caret-down').removeClass('icon-ellipsis-horizontal').addClass('icon-caret-right'); + } + } + elm.attr('data-state','open'); + var icon = angular.element(elm.children()[0]); + icon.removeClass('icon-caret-right').removeClass('icon-ellipsis-horizontal').addClass('icon-caret-down'); + activate(); + } + else { + getChildren(elm); + } + } + else { + // close the element + elm.attr('data-state','closed'); + var icon = angular.element(elm.children()[0]); + icon.removeClass('icon-caret-down').removeClass('icon-ellipsis-horizontal').addClass('icon-caret-right'); + var childlists = elm.parent().find('ul'); + if (childlists && childlists.length > 0) { + // has childen + for (var i=0; i < childlists.length; i++) { + angular.element(childlists[i]).addClass('hidden'); + } + } + activate(); + } + } + + function getChildren(elm) { + var url = elm.attr('data-groups'); + var html = ''; + var token = Authorization.getToken(); + /* For reasons unknown calling Rest fails. It just dies with no errors + or any info */ + $.ajax({ + url: url, + headers: { 'Authorization': 'Token ' + token }, + dataType: 'json', + success: function(data) { + // build html and append to parent of clicked link + for (var i=0; i < data.results.length; i++) { + idx++; + html += "
  • \n"; + html += " " + data.results[i].name; + html += "
  • \n"; + } + html = (html !== '') ? "\n" : ""; + var parent = angular.element(elm.parent()[0]); + var compiled = $compile(html)(scope); + parent.append(compiled); //append the new list to the parent
  • + console.log('childrenLoaded'); + scope.$emit('childrenLoaded'); + }, + error: function(data, status) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to get child groups for ' + elm.attr('name') + + '. GET returned: ' + status }); + } + }); + } + + function initialize() { + var root = angular.element(document.getElementById('search-node-1000')); + root.bind('click', toggle); + } + + if ($rootScope.hostTabInitRemove) { + $rootScope.hostTabInitRemove(); + } + $rootScope.hostTabInitRemove = $rootScope.$on('hostTabInit', function(e) { + var container = angular.element(document.getElementById('search-tree-container')); + container.empty(); + var html = "\n"; + var compiled = $compile(html)(scope); + container.append(compiled); + initialize(); + //setTimeout(function() { $('.search-tree .active').click(); }, 1000); //click the root node, forcing level 1 nodes to appear + }); } } @@ -327,3 +505,4 @@ angular.module('AWDirectives', ['RestServices']) + diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 2df057bc32..c7f9e86b76 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -942,6 +942,11 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) // build the hosts tab itm = "hosts"; html += "
    \n"; + html += "
    \n"; + html += "
    \n"; + html += "
    \n"; + html += "
    \n"; + html += "
    \n"; html += "
    \n"; html += "
    \n"; html += SearchWidget({ iterator: form.related[itm].iterator, template: form.related[itm], mini: true, size: 'col-lg-6'}); @@ -1047,6 +1052,9 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) html += "
    \n"; // close well html += PaginateWidget({ set: itm, iterator: form.related[itm].iterator, mini: true }); + + html += "
    \n"; + html += "
    \n"; html += "
    \n"; html += "\n"; diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 6b62a06104..364ca4fee1 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -15,6 +15,7 @@ + @@ -338,8 +339,7 @@ - - +