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",
+ 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 @@
-
-
+