From 22af7cf6b057fb3bb8022a35897b5027dc320da7 Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Tue, 13 Aug 2013 17:57:28 -0400 Subject: [PATCH] AC-331 Inventories Groups tab just about completed --- awx/ui/static/js/controllers/Inventories.js | 101 +----- awx/ui/static/js/forms/Groups.js | 8 + awx/ui/static/js/forms/Inventories.js | 4 +- awx/ui/static/js/helpers/inventory.js | 292 +++++++++++++++--- awx/ui/static/less/ansible-ui.less | 36 ++- awx/ui/static/lib/ansible/form-generator.js | 10 +- .../lib/jstree/themes/ansible/style.css | 4 +- awx/ui/templates/ui/index.html | 36 +-- 8 files changed, 333 insertions(+), 158 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 8e2061ce78..3221be607a 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) + HostsReload, EditInventory, RefreshTree) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -225,80 +225,6 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP HostsReload({ scope: scope, inventory_id: scope['inventory_id'], group_id: scope['group_id'] }); } - function PostSave() { - // Make sure the inventory name in the tree is correct - RefreshGroupName($('#inventory-node'), scope['inventory_name'], scope['inventory_description']); - - // Reset the form to disable the form action buttons - scope[form.name + '_form'].$setPristine(); - - // Show the flash message for 5 seconds, letting the user know the save worked - scope['flashMessage'] = 'Your changes were successfully saved!'; - setTimeout(function() { - scope['flashMessage'] = null; - if (!scope.$$phase) { - scope.$digest(); - } - }, 5000); - } - - // Save - scope.formSave = function() { - try { - // Make sure we have valid variable data - if (scope.inventoryParseType == 'json') { - var json_data = JSON.parse(scope.inventory_variables); //make sure JSON parses - } - else { - var json_data = jsyaml.load(scope.inventory_variables); //parse yaml - } - - // Make sure our JSON is actually an object - if (typeof json_data !== 'object') { - throw "failed to return an object!"; - } - - var data = {} - for (var fld in form.fields) { - if (fld != 'inventory_variables') { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = scope[fld]; - } - else { - data[fld] = scope[fld]; - } - } - } - - Rest.setUrl(defaultUrl + id + '/'); - Rest.put(data) - .success( function(data, status, headers, config) { - if (scope.inventory_variables) { - Rest.setUrl(data.related.variable_data); - Rest.put(json_data) - .success( function(data, status, headers, config) { - PostSave(); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to update inventory varaibles. PUT returned status: ' + status }); - }); - } - else { - PostSave(); - } - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to update new inventory. Post returned status: ' + status }); - }); - } - catch(err) { - Alert("Error", "Error parsing inventory variables. Parser returned: " + err); - } - - }; - // Cancel scope.formReset = function() { generator.reset(); @@ -350,6 +276,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP }; scope.treeController = function($node) { + var nodeType = $($node).attr('type'); if (nodeType == 'inventory') { return { @@ -361,10 +288,11 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP scope.$digest(); } EditInventory({ scope: scope, "inventory_id": id }); - } + }, + separator_after: true }, addGroup: { - label: 'Create Group', + label: 'Create New Group', action: function(obj) { scope.group_id = null; if (!scope.$$phase) { @@ -386,11 +314,11 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP } GroupsEdit({ "inventory_id": id, group_id: $(obj).attr('group_id') }); }, - separator_before: true + separator_after: true }, addGroup: { - label: 'Add Existing', + label: 'Add Existing Group', action: function(obj) { scope.group_id = $(obj).attr('group_id'); if (!scope.$$phase) { @@ -401,7 +329,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP }, createGroup: { - label: 'Create New', + label: 'Create New Group', action: function(obj) { scope.group_id = $(obj).attr('group_id'); if (!scope.$$phase) { @@ -435,15 +363,18 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP scope['selectedNode'] = node; scope['selectedNodeName'] = node.attr('name'); - + scope['selectedNodeName'] += (node.attr('data-failures') == 'true') ? + ' ' + + '' : ''; + $('#tree-view').jstree('open_node',node); if (type == 'group') { url = node.attr('all'); scope.groupAddHide = false; scope.groupCreateHide = false; - scope.groupEditHide =false; - scope.inventoryEditHide=true; + scope.groupEditHide = false; + scope.inventoryEditHide = true; scope.groupDeleteHide = false; scope.createButtonShow = true; scope.group_id = node.attr('group_id'); @@ -473,7 +404,7 @@ function InventoriesEdit ($scope, $rootScope, $compile, $location, $log, $routeP }); scope.addGroup = function() { - GroupsList({ "inventory_id": id, group_id: scope.group_id }); + GroupsList({ "inventory_id": id, group_id: scope.group_id }); } scope.createGroup = function() { @@ -531,6 +462,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' + 'ParseTypeChange', 'HostsReload', 'EditInventory', 'RefreshTree' ]; diff --git a/awx/ui/static/js/forms/Groups.js b/awx/ui/static/js/forms/Groups.js index d660188d4c..4a2063c651 100644 --- a/awx/ui/static/js/forms/Groups.js +++ b/awx/ui/static/js/forms/Groups.js @@ -18,6 +18,14 @@ angular.module('GroupFormDefinition', []) formFieldSize: 'col-lg-9', fields: { + has_active_failures: { + label: 'Status', + control: '
' + + ' Contains hosts with failed jobs
', + type: 'custom', + ngShow: 'has_active_failures', + readonly: true + }, name: { label: 'Name', type: 'text', diff --git a/awx/ui/static/js/forms/Inventories.js b/awx/ui/static/js/forms/Inventories.js index fa35790af0..5892e94819 100644 --- a/awx/ui/static/js/forms/Inventories.js +++ b/awx/ui/static/js/forms/Inventories.js @@ -20,9 +20,9 @@ angular.module('InventoryFormDefinition', []) fields: { has_active_failures: { - label: 'Host Status', + label: 'Status', control: '
' + - ' Failed jobs
', + ' Contains hosts with failed jobs', type: 'custom', ngShow: 'has_active_failures', readonly: true diff --git a/awx/ui/static/js/helpers/inventory.js b/awx/ui/static/js/helpers/inventory.js index 2c3c15da94..ad1bbacbe8 100644 --- a/awx/ui/static/js/helpers/inventory.js +++ b/awx/ui/static/js/helpers/inventory.js @@ -31,19 +31,23 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi var treeData = []; // Ater inventory top-level hosts, load top-level groups - if (scope.HostLoadedRemove) { - scope.HostLoadedRemove(); + if (scope.inventoryLoadedRemove) { + scope.inventoryLoadedRemove(); } - scope.HostLoadedRemove = scope.$on('hostsLoaded', function() { - var filter = (scope.inventoryFailureFilter) ? "has_active_failures__int=1&" : ""; + scope.inventoryLoadedRemove = scope.$on('inventoryLoaded', function() { + var filter = (scope.inventoryFailureFilter) ? "has_active_failures=true&" : ""; var url = groups + '?' + filter + 'order_by=name'; + var title; Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { for (var i=0; i < data.results.length; i++) { + title = data.results[i].name; + title += (data.results[i].has_active_failures) ? ' ' + + '' : ''; treeData[0].children.push({ data: { - title: data.results[i].name + title: title }, attr: { id: idx, @@ -69,39 +73,35 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi }); }); - // Setup tree_data - Rest.setUrl(hosts + '?order_by=name'); - Rest.get() - .success ( function(data, status, headers, config) { - treeData = - [{ - data: { - title: inventory_name - }, - attr: { - type: 'inventory', - id: 'inventory-node', - url: inventory_url, - 'inventory_id': inventory_id, - hosts: hosts, - name: inventory_name, - description: inventory_descr, - "data-failures": inventory.has_active_failures - }, - state: 'open', - children:[] - }]; - scope.$emit('hostsLoaded'); - }) - .error ( function(data, status, headers, config) { - Alert('Error', 'Failed to laod tree data. Url: ' + hosts + ' GET status: ' + status); - }); + var title = inventory_name; + title += (has_active_failures) ? ' ' + + '' : ''; + treeData = + [{ + data: { + title: title + }, + attr: { + type: 'inventory', + id: 'inventory-node', + url: inventory_url, + 'inventory_id': inventory_id, + hosts: hosts, + name: inventory_name, + description: inventory_descr, + "data-failures": inventory.has_active_failures + }, + state: 'open', + children:[] + }]; + scope.$emit('inventoryLoaded'); + } }]) - .factory('TreeInit', ['Alert', 'Rest', 'Authorization', '$http', 'LoadTreeData', - function(Alert, Rest, Authorization, $http, LoadTreeData) { + .factory('TreeInit', ['Alert', 'Rest', 'Authorization', '$http', 'LoadTreeData', 'GetBasePath', 'ProcessErrors', + function(Alert, Rest, Authorization, $http, LoadTreeData, GetBasePath, ProcessErrors) { return function(params) { var scope = params.scope; @@ -121,7 +121,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi scope.buildTreeRemove = scope.$on('buildTree', function(e, treeData, index) { var idx = index; $(tree_id).jstree({ - "core": { "initially_open":['inventory_node'] }, + "core": { "initially_open":['inventory_node'], + "html_titles": true, + }, "plugins": ['themes', 'json_data', 'ui', 'contextmenu', 'dnd', 'crrm'], "themes": { "theme": "ansible", @@ -142,11 +144,15 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi headers: { 'Authorization': 'Token ' + Authorization.getToken() }, success: function(data) { var response = []; - var filter = (scope.inventoryFailureFilter) ? "has_active_failures__int=1&" : ""; + var title; + var filter = (scope.inventoryFailureFilter) ? "has_active_failures=true&" : ""; for (var i=0; i < data.results.length; i++) { + title = data.results[i].name; + title += (data.results[i].has_active_failures) ? ' ' + + '' : ''; response.push({ data: { - title: data.results[i].name + title: title }, attr: { id: idx, @@ -170,7 +176,18 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi } }, "dnd": { }, - "crrm": { }, + "crrm": { + "move": { + "check_move": function(m) { + if (m.op.attr('id') == m.np.attr('id')) { + // old parent and new parent cannot be the same + return false; + } + return true; + } + } + }, + "crrm" : { }, "contextmenu": { items: scope.treeController } @@ -182,20 +199,121 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi }); $(tree_id).bind('move_node.jstree', function(e, data) { - if (data.rslt.o[0].id !== 'inventory_id') { - console.log('group_id: ' + $('#tree-view li[id="' + data.rslt.o[0].id+ '"]').attr('group_id')); + // When user drags-n-drops a node, update the API + var node, target, url, parent, inv_id, variables; + node = $('#tree-view li[id="' + data.rslt.o[0].id + '"]'); // node being moved + parent = $('#tree-view li[id="' + data.args[0].op[0].id + '"]'); //node moving from + target = $('#tree-view li[id="' + data.rslt.np[0].id + '"]'); // node moving to + inv_id = inventory_id; + + if (scope.removeCopyVariables) { + scope.removeCopyVariables(); } - else { - console.log('id: ' + data.rslt.o[0].id); + scope.removeCopyVariables = scope.$on('copyVariables', function(e, id, url) { + + function showSuccessMsg() { + var parent_descr = (parent.attr('type') == 'inventory') ? 'the inventory root' : parent.attr('name'); + var target_descr = (target.attr('type') == 'inventory') ? 'the inventory root' : target.attr('name'); + Alert('Group Moved', 'Group ' + node.attr('name') + ' was successfully moved from ' + parent_descr + + ' to ' + target_descr + '.', 'alert-success'); + scope['treeLoading'] = false; + if (!scope.$$phase) { + scope.$digest(); + } + } + + if (variables) { + Rest.setUrl(url); + Rest.put(variables) + .success(function(data, status, headers, config) { + showSuccessMsg(); + }) + .error(function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to update variables. PUT returned status: ' + status }); + }); + } + else { + showSuccessMsg(); + } + }); + + if (scope['addToTargetRemove']) { + scope.addToTargetRemove(); } + scope.addToTargetRemove = scope.$on('addToTarget', function() { + // add the new group to the target parent + var url = (target.attr('type') == 'group') ? GetBasePath('base') + 'groups/' + target.attr('group_id') + '/children/' : + GetBasePath('inventory') + inv_id + '/groups/'; + var group = { + name: node.attr('name'), + description: node.attr('description'), + inventory: node.attr('inventory') + } + Rest.setUrl(url); + Rest.post(group) + .success( function(data, status, headers, config) { + //Update the node with new attributes + var filter = (scope.inventoryFailureFilter) ? "has_active_failures=true&" : ""; + node.attr('group_id', data.id); + node.attr('variable', data.related.variable_data); + node.attr('all', data.related.all_hosts); + node.attr('children', data.related.children + '?' + filter + 'order_by=name'); + node.attr('hosts', data.related.hosts); + node.attr('data-failures', data.has_active_failures); + scope.$emit('copyVariables', data.id, data.related.variable_data); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to add ' + node.attr('name') + ' to ' + + target.attr('name') + '. POST returned status: ' + status }); + }); + }); + + // disassociate the group from the original parent + if (scope.removeGroupRemove) { + scope.removeGroupRemove(); + } + scope.removeGroupRemove = scope.$on('removeGroup', function() { + var url = (parent.attr('type') == 'group') ? GetBasePath('base') + 'groups/' + parent.attr('group_id') + '/children/' : + GetBasePath('inventory') + inv_id + '/groups/'; + Rest.setUrl(url); + Rest.post({ id: node.attr('group_id'), disassociate: 1 }) + .success( function(data, status, headers, config) { + scope.$emit('addToTarget'); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to remove ' + node.attr('name') + ' from ' + + parent.attr('name') + '. POST returned status: ' + status }); + }); + }); + + // Lookup the inventory. We already have what we need except for variables. + Rest.setUrl(GetBasePath('base') + 'groups/' + node.attr('group_id') + '/'); + Rest.get() + .success( function(data, status, headers, config) { + variables = (data.variables) ? JSON.parse(data.variables) : ""; + scope.$emit('removeGroup'); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to lookup group ' + node.attr('name') + + '. GET returned status: ' + status }); + }); + + scope['treeLoading'] = true; + + if (!scope.$$phase) { + scope.$digest(); + } }); // When user clicks on a group, display the related hosts in the list view $(tree_id).bind("select_node.jstree", function(e, data){ - //selected node object: data.inst.get_json()[0]; - //selected node text: data.inst.get_json()[0].data scope.$emit('NodeSelect', data.inst.get_json()[0]); }); + }); scope['treeLoading'] = true; @@ -267,6 +385,10 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi // Call after GroupsEdit controller saves changes $('#tree-view').jstree('rename_node', node, name); node.attr('description', description); + scope = angular.element(getElementById('htmlTemplate')).scope(); + scope['selectedNodeName'] = name; + scope['selectedNodeName'] += (node.attr('data-failures') == 'true') ? + ' ' : ''; } }]) @@ -323,6 +445,8 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi $('#tree-view').jstree('destroy'); TreeInit(scope.TreeParams); }); + + scope.treeLoading = true; LoadInventory({ scope: scope, doPostSteps: true }); } @@ -330,9 +454,9 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi .factory('EditInventory', ['InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'LoadInventory', + 'GetBasePath', 'ParseTypeChange', 'LoadInventory', 'RefreshGroupName', function(InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, - LoadInventory) { + LoadInventory, RefreshGroupName) { return function(params) { var generator = GenerateForm; @@ -396,6 +520,82 @@ angular.module('InventoryHelper', [ 'RestServices', 'Utilities', 'OrganizationLi if (!scope.$$phase) { scope.$digest(); } + + function PostSave() { + $('#form-modal').modal('hide'); + + // Make sure the inventory name appears correctly in the tree and the navbar + RefreshGroupName($('#inventory-node'), scope['inventory_name'], scope['inventory_description']); + + // Reset the form to disable the form action buttons + //scope[form.name + '_form'].$setPristine(); + + // Show the flash message for 5 seconds, letting the user know the save worked + //scope['flashMessage'] = 'Your changes were successfully saved!'; + //setTimeout(function() { + // scope['flashMessage'] = null; + // if (!scope.$$phase) { + // scope.$digest(); + // } + // }, 5000); + } + + // Save + scope.formModalAction = function() { + try { + // Make sure we have valid variable data + if (scope.inventoryParseType == 'json') { + var json_data = JSON.parse(scope.inventory_variables); //make sure JSON parses + } + else { + var json_data = jsyaml.load(scope.inventory_variables); //parse yaml + } + + // Make sure our JSON is actually an object + if (typeof json_data !== 'object') { + throw "failed to return an object!"; + } + + var data = {} + for (var fld in form.fields) { + if (fld != 'inventory_variables') { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = scope[fld]; + } + else { + data[fld] = scope[fld]; + } + } + } + + Rest.setUrl(defaultUrl + scope['inventory_id'] + '/'); + Rest.put(data) + .success( function(data, status, headers, config) { + if (scope.inventory_variables) { + Rest.setUrl(data.related.variable_data); + Rest.put(json_data) + .success( function(data, status, headers, config) { + PostSave(); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update inventory varaibles. PUT returned status: ' + status }); + }); + } + else { + PostSave(); + } + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update inventory. POST returned status: ' + status }); + }); + } + catch(err) { + Alert("Error", "Error parsing inventory variables. Parser returned: " + err); + } + + }; } }]); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 5ffb75a90f..6b23100d9c 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -526,16 +526,43 @@ input[type="text"].job-successful { .nav a { color: @blue-link; + font-size: 12px; + i { + font-size: 14px; + } + } + + .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: @grey; + color: @black; + text-align: left; + font-size: 14px; + max-width: 100%; + i { + color: @red; + } } .navbar-brand:hover { - color: @grey; + color: @black; cursor: default; } + /* neither is the status spinner */ .nav .status { color: @black; @@ -547,6 +574,11 @@ input[type="text"].job-successful { } +.tree-badge { + color: @red; + font-size: 12px; +} + .inventory-title { margin-top: 15px; font-weight: bold; diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index ab00f01693..2df057bc32 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -914,20 +914,24 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) // build the groups tab html += "
\n"; - html += "\n"; + html += "\n"; html += "\n"; + html += "
\n"; + html += "\n"; + html += "
\n"; html += "
\n"; html += "
\n"; //html += "