diff --git a/awx/ui/static/js/controllers/Groups.js b/awx/ui/static/js/controllers/Groups.js index e6194b5622..41383b06a5 100644 --- a/awx/ui/static/js/controllers/Groups.js +++ b/awx/ui/static/js/controllers/Groups.js @@ -37,7 +37,8 @@ function InventoryGroups ($scope, $rootScope, $compile, $location, $log, $routeP scope: scope, inventory_id: id, emit_on_select: 'NodeSelect', - target_id: 'search-tree-container' + target_id: 'search-tree-container', + moveable: true }); if (!scope.$$phase) { scope.$digest(); diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index cc8ba5786b..a7fbb5e5be 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -459,7 +459,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' inventory_id: scope['inventory_id'], emit_on_select: 'NodeSelect', target_id: 'search-tree-container', - refresh: true + refresh: true, + moveable: true }); } @@ -514,9 +515,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' .factory('GroupsAdd', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'RefreshTree', 'ParseTypeChange', 'GroupsEdit', + 'Prompt', 'ProcessErrors', 'GetBasePath', 'ParseTypeChange', 'GroupsEdit', 'BuildTree', function($rootScope, $location, $log, $routeParams, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, RefreshTree, ParseTypeChange, GroupsEdit) { + GetBasePath, ParseTypeChange, GroupsEdit, BuildTree) { return function(params) { var inventory_id = params.inventory_id; @@ -578,29 +579,25 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' if (inventory_id) { data['inventory'] = inventory_id; } + + data.variables = json_data; Rest.setUrl(defaultUrl); Rest.post(data) .success( function(data, status, headers, config) { var id = data.id; scope.showGroupHelp = false; // get rid of the Hint - if (scope.variables) { - Rest.setUrl(data.related.variable_data); - Rest.put(json_data) - .success( function(data, status, headers, config) { - $('#form-modal').modal('hide'); - RefreshTree({ scope: scope, group_id: id }); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to add group varaibles. PUT returned status: ' + status }); - }); - } - else { - $('#form-modal').modal('hide'); - RefreshTree({ scope: scope, group_id: id }); - } - }) + $('#form-modal').modal('hide'); + BuildTree({ + scope: scope, + inventory_id: scope['inventory_id'], + emit_on_select: 'NodeSelect', + target_id: 'search-tree-container', + refresh: true, + moveable: true, + group_id: id + }); + }) .error( function(data, status, headers, config) { scope.formModalActionDisabled = false; ProcessErrors(scope, data, status, form, diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 81c9fa3f2f..56e1c1f9ef 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1012,11 +1012,15 @@ select.field-mini-height { } .activate:hover { - background-color: #ddd; + background-color: #ddd; + cursor: pointer; } .active { font-weight: bold; + box-shadow: 3px 3px 3px 0 @grey; + border-bottom: 1px solid @grey; + border-right: 1px solid @grey; } .expand-container, @@ -1028,14 +1032,19 @@ select.field-mini-height { .badge-container, .title-container { - border-bottom: 1px solid @well; + border-bottom: 2px solid @well; } - + .expand-container { width: 14px; text-align: center; } + #root-expand-container { + width: 20px; + text-align: left; + } + .expand { padding: 3px; } @@ -1060,14 +1069,6 @@ select.field-mini-height { padding: 10px; } -// drag-n-drop tree styles -.droppable-hover { - background-color: #C0C0C0; - color: @black; - border: 1px solid #808080; - font-weight: normal; -} - .disabled { color: @grey; } diff --git a/awx/ui/static/lib/ansible/TreeSelector.js b/awx/ui/static/lib/ansible/TreeSelector.js index 191a93b64d..4a7cf9fe8c 100644 --- a/awx/ui/static/lib/ansible/TreeSelector.js +++ b/awx/ui/static/lib/ansible/TreeSelector.js @@ -17,6 +17,8 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) var emit_on_select = params.emit_on_select; var target_id = params.target_id; var refresh_tree = (params.refresh == undefined || params.refresh == false) ? false : true; + var moveable = (params.moveable == undefined || params.moveable == false) ? false : true; + var group_id = params.group_id; var html = ''; var toolTip = 'Hosts have failed jobs?'; @@ -100,6 +102,119 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) } } + if (scope.moveNodeRemove) { + scope.moveNodeRemove(); + } + scope.moveNodeRemove = scope.$on('MoveNode', function(e, node, parent, target) { + var inv_id = scope['inventory_id']; + var variables; + + function cleanUp(state) { + if (state !== 'fail') { + // Visually move the element. Elment will be appended to the + // end of target element list + var elm = $('#' + node.attr('id')).detach(); + if (target.find('ul').length > 0) { + // parent has children + target.find('ul').first().append(elm); + } + else { + target.append(''); + target.find('ul').first().append(elm); + } + + // Remove any styling that might be left on the target + // and put the expander icon back the way it should be + target.find('div').each(function(idx) { + if (idx > 0 && idx < 3) { + $(this).css({ 'border-bottom': '2px solid #f5f5f5' }); + } + }); + + // Make sure the parent and target have the correct expander class/icon. + function setExpander(n) { + var c = n.find('.expand-container'); + var icon; + c.first().empty(); + if (n.attr('id') == 'inventory-root-node') { + c.first().html(''); + } + else if (c.length > 1) { + // not root and has children, put expander icon back + icon = (n.attr('data-state') == 'opened') ? 'icon-caret-down' : 'icon-caret-right'; + c.first().html(''); + c.first().find('a').first().bind('click', toggle); + } + } + setExpander(target); + setExpander(parent); + } + Wait('stop'); + } + + // disassociate the group from the original parent + if (scope.removeGroupRemove) { + scope.removeGroupRemove(); + } + scope.removeGroupRemove = scope.$on('removeGroup', function() { + var url = (parent.attr('data-group-id')) ? GetBasePath('base') + 'groups/' + parent.attr('data-group-id') + '/children/' : + GetBasePath('inventory') + inv_id + '/groups/'; + Rest.setUrl(url); + Rest.post({ id: node.attr('data-group-id'), disassociate: 1 }) + .success( function(data, status, headers, config) { + cleanUp('success'); + }) + .error( function(data, status, headers, config) { + cleanUp('fail'); + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to remove ' + node.attr('name') + ' from ' + + parent.attr('name') + '. POST returned status: ' + status }); + }); + }); + + if (scope['addToTargetRemove']) { + scope.addToTargetRemove(); + } + scope.addToTargetRemove = scope.$on('addToTarget', function() { + // add the new group to the target parent + var url = (target.attr('data-group-id')) ? GetBasePath('base') + 'groups/' + target.attr('data-group-id') + '/children/' : + GetBasePath('inventory') + inv_id + '/groups/'; + var group = { + id: node.attr('data-group-id'), + name: node.attr('data-name'), + description: node.attr('data-description'), + inventory: inv_id + } + Rest.setUrl(url); + Rest.post(group) + .success( function(data, status, headers, config) { + scope.$emit('removeGroup'); + }) + .error( function(data, status, headers, config) { + cleanUp('fail'); + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to add ' + node.attr('name') + ' to ' + + target.attr('name') + '. POST returned status: ' + status }); + }); + }); + + Wait('start'); + // Lookup the inventory. We already have what we need except for variables. + var url = GetBasePath('base') + 'groups/' + node.attr('data-group-id') + '/'; + Rest.setUrl(url); + Rest.get() + .success( function(data, status, headers, config) { + variables = (data.variables) ? JSON.parse(data.variables) : ""; + scope.$emit('addToTarget'); + }) + .error( function(data, status, headers, config) { + cleanUp('fail'); + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to lookup group ' + node.attr('name') + + '. GET returned status: ' + status }); + }); + }); + // The HTML is ready. Insert it into the view. if (scope.searchTreeReadyRemove) { scope.searchTreeReadyRemove(); @@ -121,6 +236,10 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) link.bind('click', activate); } } + + if (refresh_tree && group_id !== undefined) { + $('li[data-group-id="' + group_id + '"]').first().click(); + } // Attempt to stop the title from dropping to the next // line @@ -132,48 +251,63 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) }); // Make the tree drag-n-droppable - - $('#selector-tree .activate').draggable({ - cursor: "pointer", - cursorAt: { top: -16, left: -10 }, - revert: 'invalid', - helper: 'clone', - start: function (e, ui) { - var txt = '[ ' + ui.helper.text() + ' ]'; - ui.helper.css({ 'font-weight': 'normal', 'color': '#A9A9A9', 'background-color': '#f5f5f5' }).text(txt); - } - }) - .droppable({ - //hoverClass: 'droppable-hover', - tolerance: 'pointer', - over: function (e, ui) { - var p = $(this).parent().parent(); - p.find('div').each(function(idx) { - if (idx > 0 && idx < 3) { - $(this).css({ 'border-bottom': '1px dotted #808080' }); - } - }); - var c = p.find('.expand-container').first(); - c.empty().html(''); - }, - out: function (e, ui) { - var p = $(this).parent().parent(); - p.find('div').each(function(idx) { - if (idx > 0 && idx < 3) { - $(this).css({ 'border-bottom': '1px solid #f5f5f5' }); - } - }); - var c = p.find('.expand-container'); - var icon; - c.first().empty(); - if (c.length > 1) { - // has children, put expander icon back - icon = (p.attr('data-state') == 'opened') ? 'icon-caret-down' : 'icon-caret-right'; - c.first().html(''); - c.first().find('a').first().bind('click', toggle); - } + if (moveable) { + $('#selector-tree .activate').draggable({ + cursor: "pointer", + cursorAt: { top: -16, left: -10 }, + revert: 'invalid', + helper: 'clone', + start: function (e, ui) { + var txt = '[ ' + ui.helper.text() + ' ]'; + ui.helper.css({ 'font-weight': 'normal', 'color': '#A9A9A9', 'background-color': '#f5f5f5' }).text(txt); } }) + .droppable({ + //hoverClass: 'droppable-hover', + tolerance: 'pointer', + over: function (e, ui) { + var p = $(this).parent().parent(); + p.find('div').each(function(idx) { + if (idx > 0 && idx < 3) { + $(this).css({ 'border-bottom': '2px solid #A9A9A9' }); + } + }); + var c = p.find('.expand-container').first(); + c.empty().html(''); + }, + out: function (e, ui) { + var p = $(this).parent().parent(); + p.find('div').each(function(idx) { + if (idx > 0 && idx < 3) { + $(this).css({ 'border-bottom': '2px solid #f5f5f5' }); + } + }); + var c = p.find('.expand-container'); + var icon; + c.first().empty(); + if (c.length > 1) { + // has children, put expander icon back + icon = (p.attr('data-state') == 'opened') ? 'icon-caret-down' : 'icon-caret-right'; + c.first().html(''); + c.first().find('a').first().bind('click', toggle); + } + }, + drop: function (e,ui) { + var variables; + var node = ui.draggable.parent().parent(); // node being moved + var parent = node.parent().parent(); // node from + var target = $(this).parent().parent(); // node to + scope.$emit('MoveNode', node, parent, target); + + // Make sure angular picks up changes and jQuery doesn't + // leave us in limbo... + if (!scope.$$phase) { + scope.$digest(); + } + e.preventDefault(); + } + }); + } // if moveable Wait('stop'); }); @@ -199,7 +333,7 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) html += " " + "
" + - "
" + sorted[i].name + "
"; + "
" + sorted[i].name + "
"; idx++; if (sorted[i].children.length > 0) { buildHTML(sorted[i].children); @@ -268,14 +402,17 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) "data-failures=\"" + data.has_active_failures + "\" " + "data-groups=\"" + data.related.groups + "\" " + "data-name=\"" + data.name + "\" " + - ">" + - " " + - "" + data.name + ""; + ">"+ + "
" + + "
" + + "
" + data.name + "
"; scope.$emit('buildAllGroups', data.name, data.related.tree, data.related.groups); if (!refresh_tree) { - // if caller requests refresh, let caller handle next steps / node selection + // if caller requests with refresh true, let caller handle next steps / node selection + // otherwise, we're refreshing to summary page scope.$emit(emit_on_select, 'inventory-root-node', null, 'All Hosts'); } @@ -307,7 +444,6 @@ angular.module('TreeSelector', ['Utilities', 'RestServices']) if (inventory_id !== null) { $('#inventory-root-node').attr('data-name', name).attr('data-description', descr).find('.activate').first().text(name); } - } }])