diff --git a/awx/ui/static/js/controllers/JobEvents.js b/awx/ui/static/js/controllers/JobEvents.js
index 75d1d42758..82afcc2f2d 100644
--- a/awx/ui/static/js/controllers/JobEvents.js
+++ b/awx/ui/static/js/controllers/JobEvents.js
@@ -30,7 +30,7 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest,
scope.selected = [];
scope.expand = true; //on load, automatically expand all nodes
- scope.parentNode = 'parent-event'; // used in ngClass to dynamicall set row level class and control
+ scope.parentNode = 'parent-event'; // used in ngClass to dynamically set row level class and control
scope.childNode = 'child-event'; // link color and cursor
if (scope.removeSetHostLinks) {
diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less
index 8d39249232..81c9fa3f2f 100644
--- a/awx/ui/static/less/ansible-ui.less
+++ b/awx/ui/static/less/ansible-ui.less
@@ -15,6 +15,7 @@
@blue: #1778c3; /* logo blue */
@blue-link: #0088cc;
@grey: #A9A9A9;
+@well: #f5f5f5; /* well background color */
@green: #5bb75b;
@info: #d9edf7; /* alert info background color */
@info-border: #bce8f1; /* alert info border color */
@@ -995,11 +996,10 @@ select.field-mini-height {
ul {
list-style-type: none;
padding-left: 16px;
- padding-top: 5px;
}
-
- li {
- padding-bottom: 5px;
+
+ li {
+ padding-top: 3px;
}
.tree-root {
@@ -1019,11 +1019,18 @@ select.field-mini-height {
font-weight: bold;
}
- div {
+ .expand-container,
+ .badge-container,
+ .title-container {
display: inline-block;
vertical-align: top;
}
+ .badge-container,
+ .title-container {
+ border-bottom: 1px solid @well;
+ }
+
.expand-container {
width: 14px;
text-align: center;
@@ -1053,6 +1060,14 @@ 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;
}
@@ -1265,7 +1280,11 @@ tr td button i {
/* Large desktop */
@media (min-width: 1200px) {
-
+
+ .container {
+ max-width: 96%;
+ }
+
.delete-btn {
/* Used on job and project page to make cancel and delete buttons have an equal width */
width: 60px;
diff --git a/awx/ui/static/lib/ansible/TreeSelector.js b/awx/ui/static/lib/ansible/TreeSelector.js
index fbbeccce19..191a93b64d 100644
--- a/awx/ui/static/lib/ansible/TreeSelector.js
+++ b/awx/ui/static/lib/ansible/TreeSelector.js
@@ -22,31 +22,6 @@ angular.module('TreeSelector', ['Utilities', 'RestServices'])
var toolTip = 'Hosts have failed jobs?';
var idx = 0;
- function buildHTML(tree_data) {
- var sorted = SortNodes(tree_data);
- html += (sorted.length > 0) ? "
\n" : "";
- for(var i=0; i < sorted.length; i++) {
- html += "- " +
- "
" +
- "";
- idx++;
- if (sorted[i].children.length > 0) {
- buildHTML(sorted[i].children);
- }
- else {
- html += " \n";
- }
- }
- html += "
\n";
- }
-
function refresh(parent) {
var group, title;
var id = parent.attr('id');
@@ -155,10 +130,88 @@ angular.module('TreeSelector', ['Utilities', 'RestServices'])
$(this).css('width','80%');
}
});
+
+ // 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);
+ }
+ }
+ })
Wait('stop');
});
-
+
+ function buildHTML(tree_data) {
+ var sorted = SortNodes(tree_data);
+ html += (sorted.length > 0) ? "\n" : "";
+ for(var i=0; i < sorted.length; i++) {
+ html += "";
+
+ if (sorted[i].children.length > 0) {
+ html += "
";
+ }
+ else {
+ html += " ";
+ }
+ html += "
" +
+ "
" +
+ "";
+ idx++;
+ if (sorted[i].children.length > 0) {
+ buildHTML(sorted[i].children);
+ }
+ else {
+ //html += "\n";
+ html += " \n";
+ }
+ }
+ html += "
\n";
+ }
+
// Build the HTML for our tree
if (scope.buildAllGroupsRemove) {
scope.buildAllGroupsRemove();
@@ -168,7 +221,7 @@ angular.module('TreeSelector', ['Utilities', 'RestServices'])
Rest.get()
.success( function(data, status, headers, config) {
buildHTML(data);
- scope.$emit('searchTreeReady', html + "\n\n");
+ scope.$emit('searchTreeReady', html + "\n\n\n");
})
.error( function(data, status, headers, config) {
ProcessErrors(scope, data, status, null,
@@ -208,6 +261,7 @@ angular.module('TreeSelector', ['Utilities', 'RestServices'])
Rest.get()
.success( function(data, status, headers, config) {
html += "Group Selector:
\n" +
+ "\n" +
"
\n" +
"-
+ var parent = angular.element(e.target.parentNode.parentNode); //
-
+ $('.search-tree .active').removeClass('active');
+ elm.addClass('active');
+ refresh(parent);
+ }
+
+ function toggle(e) {
+ var id, parent, elm, icon;
+
+ if (e.target.tagName == 'I') {
+ id = e.target.parentNode.parentNode.parentNode.attributes.id.value;
+ parent = angular.element(e.target.parentNode.parentNode.parentNode); //
-
+ elm = angular.element(e.target.parentNode); //
+ }
+ else {
+ id = e.target.parentNode.parentNode.attributes.id.value;
+ parent = angular.element(e.target.parentNode.parentNode);
+ elm = angular.element(e.target);
+ }
+
+ var sibling = angular.element(parent.children()[2]); //
+ var state = parent.attr('data-state');
+ var icon = angular.element(elm.children()[0]);
+
+ if (state == 'closed') {
+ // expand the elment
+ var childlists = parent.find('ul');
+ if (childlists && childlists.length > 0) {
+ // has childen
+ for (var i=0; i < childlists.length; i++) {
+ var listChild = angular.element(childlists[i]);
+ var listParent = angular.element(listChild.parent());
+ if (listParent.attr('id') == id) {
+ angular.element(childlists[i]).removeClass('hidden');
+ }
+ }
+ }
+ parent.attr('data-state','open');
+ icon.removeClass('icon-caret-right').addClass('icon-caret-down');
+ }
+ else {
+ // close the element
+ parent.attr('data-state','closed');
+ icon.removeClass('icon-caret-down').addClass('icon-caret-right');
+ var childlists = parent.find('ul');
+ if (childlists && childlists.length > 0) {
+ // has childen
+ for (var i=0; i < childlists.length; i++) {
+ angular.element(childlists[i]).addClass('hidden');
+ }
+ }
+ /* When the active node's parent is closed, activate the parent */
+ if ($(parent).find('.active').length > 0) {
+ $(parent).find('.active').removeClass('active');
+ sibling.addClass('active');
+ refresh(parent);
+ }
+ }
+ }
+
+ // The HTML is ready. Insert it into the view.
+ if (scope.searchTreeReadyRemove) {
+ scope.searchTreeReadyRemove();
+ }
+ scope.searchTreeReadyRemove = scope.$on('searchTreeReady', function(e, html) {
+ var container = angular.element(document.getElementById(target_id));
+ container.empty();
+ var compiled = $compile(html)(scope);
+ container.append(compiled);
+ var links = container.find('a');
+ for (var i=0; i < links.length; i++) {
+ var link = angular.element(links[i]);
+ if (link.hasClass('expand')) {
+ link.unbind('click', toggle);
+ link.bind('click', toggle);
+ }
+ if (link.hasClass('activate')) {
+ link.unbind('click', activate);
+ link.bind('click', activate);
+ //link.parent().parent().draggable({ containment: target_id });
+ }
+ }
+
+ $('#inventory-tree').sortable();
+
+ // Attempt to stop the title from dropping to the next
+ // line
+ $(container).find('.title-container').each(function(idx) {
+ var parent = $(this).parent();
+ if ($(this).width() >= parent.width()) {
+ $(this).css('width','80%');
+ }
+ });
+
+ Wait('stop');
+ });
+
+ function buildHTML(data) {
+ //var sorted = SortNodes(tree_data);
+ var node;
+ for (var i=0; i < data.length; i++) {
+ node = data[i];
+ html += "
- " +
+ "
" +
+ "" +
+ " \n";
+ idx++;
+ if (node.children.length > 0) {
+ level++;
+ buildHTML(node.children);
+ }
+ else {
+ level=1;
+ }
+ }
+ }
+
+ // Build the HTML for our tree
+ if (scope.buildAllGroupsRemove) {
+ scope.buildAllGroupsRemove();
+ }
+ scope.buildAllGroupsRemove = scope.$on('buildAllGroups', function(e, inventory_name, inventory_tree) {
+ Rest.setUrl(inventory_tree);
+ Rest.get()
+ .success( function(data, status, headers, config) {
+ level=1;
+ buildHTML(data);
+ scope.$emit('searchTreeReady', html + "
\n");
+ })
+ .error( function(data, status, headers, config) {
+ ProcessErrors(scope, data, status, null,
+ { hdr: 'Error!', msg: 'Failed to get inventory tree for: ' + inventory_id + '. GET returned: ' + status });
+ });
+ });
+
+ // Builds scope.inventory_groups, used by the group picker on Hosts view to build the list of potential groups
+ // that can be added to a host. <<<< Should probably be moved to /helpers/Hosts.js
+ if (scope.buildGroupListRemove) {
+ scope.buildGroupListRemove();
+ }
+ scope.buildGroupListRemove = scope.$on('buildAllGroups', function(e, inventory_name, inventory_tree, groups_url) {
+ scope.inventory_groups = [];
+ Rest.setUrl(groups_url);
+ Rest.get()
+ .success( function(data, status, headers, config) {
+ var groups = [];
+ for (var i=0; i < data.results.length; i++) {
+ groups.push({
+ id: data.results[i].id,
+ description: data.results[i].description,
+ name: data.results[i].name });
+ }
+ scope.inventory_groups = SortNodes(groups);
+ })
+ .error( function(data, status, headers, config) {
+ ProcessErrors(scope, data, status, null,
+ { hdr: 'Error!', msg: 'Failed to get groups for inventory: ' + inventory_id + '. GET returned: ' + status });
+ });
+ });
+
+ Wait('start');
+
+ // Load the inventory root node
+ Rest.setUrl (GetBasePath('inventory') + inventory_id + '/');
+ Rest.get()
+ .success( function(data, status, headers, config) {
+ html += "
Group Selector:
\n" +
+ "
\n" +
+ "- " +
+ " " +
+ "" + data.name + "" +
+ "
\n";
+
+ 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
+ scope.$emit(emit_on_select, 'inventory-root-node', null, 'All Hosts');
+ }
+
+ })
+ .error( function(data, status, headers, config) {
+ ProcessErrors(scope, data, status, null,
+ { hdr: 'Error!', msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status });
+ });
+ }
+ }])
+
+ // Set node name and description after an update to Group properties.
+ .factory('SetNodeName', [ function() {
+ return function(params) {
+ var scope = params.scope;
+ var name = params.name;
+ var descr = params.description;
+ var group_id = (params.group_id !== undefined) ? params.group_id : null;
+ var inventory_id = (params.inventory_id != undefined) ? params.inventory_id : null;
+
+ if (group_id !== null) {
+ $('#inventory-tree').find('li [data-group-id="' + group_id + '"]').each(function(idx) {
+ $(this).attr('data-name',name);
+ $(this).attr('data-description',descr);
+ $(this).find('.activate').first().text(name);
+ });
+ }
+
+ if (inventory_id !== null) {
+ $('#inventory-root-node').attr('data-name', name).attr('data-description', descr).find('.activate').first().text(name);
+ }
+
+ }
+ }])
+
+ .factory('ClickNode', [ function() {
+ return function(params) {
+ var selector = params.selector; //jquery selector string to find the correct -
+ $(selector + ' .activate').first().click();
+ }
+ }]);
+
+
+
+
diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js
index 9d42960dde..31ae213ea1 100644
--- a/awx/ui/static/lib/ansible/form-generator.js
+++ b/awx/ui/static/lib/ansible/form-generator.js
@@ -1234,12 +1234,12 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
html += "
\n";
*/
html += "\n";
- html += "
\n" +
+ html += "
\n";
- html += "
\n";
+ html += "
\n";
html += "
\n
\n";
html += "
\n";
//html += "
\n";
@@ -1272,12 +1272,12 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies'])
*/
html += "
\n";
- html += "
\n";
+ html += "
\n";
html += "
\n";
html += "
\n
\n";
html += "
\n";
- html += "
\n";
- html += "
\n";
+ html += "
\n";
+ html += "