Files
awx/awx/ui/client/src/helpers/Hosts.js
Leigh Johnson a49095bdbc Boolean / Smart Search (#3631)
* Part 1: building new search components

Directives: smart-search, column-sort, paginate
Service: QuerySet
Model: DjangoSearchModel

* Part 2: Implementing new search components, de-implementing old search
components

Remove old code:
	* tagSearch directive
	* old pagination strategy
	* old column sorting strategy
	* lookup

Add new directives to list/form generator:
	* smart-search,
	* paginate
	* column-sort

Connect $state + dataset resolution
	* upgrade ui-router lib to v1.0.0-beta3
	* Custom $urlMatcherFactory.type - queryset
	* Render lists, forms, related, lookups in named views
	* Provide html templates in list/form/lookup/related state definitions
	* Provide dataset through resolve block in state definitions

Update utilities
	* isEmpty filter
	* use async validation strategy in awlookup directive

* Part 3: State implementations (might split into per-module commits)

* Support optional state definition flag: squashSearchUrl. *_search params are only URI-encoded if squashSearchUrl is falsey.

* * Fix list badge counts
* Clear search input after search term(s) applied
* Chain of multiple search terms in one submission

* Hook up activity stream

* Hook up portal mode

* Fix pagination range calculations

* Hook up organization sub-list views

* Hook up listDefinition.search defaults

* Fix ng-disabled conditions reflecting RBAC access on form fields

* Fix actively-editing indicator in generated lists

* form generator - fix undefined span, remove dead event listeners

* wrap hosts/groups lists in a panel, fix groups list error

* Smart search directive: clear all search tags

* Search tags - ‘Clear All’ text - 12px
Search key - remove top padding/margin
Search key - reverse bolding of relationship fields / label, add commas
Search tags - remove padding-bottom
Lookup modal - “X” close button styled incorrectly
Lookup modal - List title not rendered
Lookup modal - 20px margin between buttons

* Portal Mode
Fix default column-sort on jobs list
Hide column-oort on job status column
Apply custom search bar sizes

* stateDefinition.factory

Return ES6 Promise instead of $q promise.
$q cannot be safely provided during module.config() phase
Some generated state trees (inventory / inventoryManage) need to be
reduced to one promise. Side-step issues caused by ui-router de-registering ALL registered states that match placeholder state name/url pattern.

e.g. inventories.lazyLoad() would de-register inventoryManage states if
a page refresh occured @ /#/inventories/**

* Combine generated state trees: inventories + inventoryManage
Hook up inventory sync schedule list/form add /form edit views

* Hook up system job schedule list/add/edit states

* Fix breadcrumb of generated states in /setup view
Fix typo in scheduler search prefix

* Remove old search system deritus from list definitions

* Fix breadcrumb definitions in states registered in app.js config block

* Transclude list action buttons in generated form lists

* Lookup Modal passes acceptance criterea:
Modal cancel/exit - don’t update form field’s ng-model
Modal save - do update form field's ng-model
Transclude generated list contents into <lookup-modal> directive
Lookup modal test spec

* Fix typo in merge conflict resolution

* Disable failing unit tests pending revision

* Integrate smart-search architechture into add-permissions modal

* use a semicolon delimiter instead of comma to avoid collision with django __in comparator

* Hook up Dashboard > Hosts states, update Dashboard Inventory/Project counts with new search filters

* Misc bug splat

Add 20px spacing around root ui-view
Fix missing closing div in related views
Remove dupe line in smart-search controller

* Remove defunct LookupHelper code

* Rebuild inventories list status tooltips on updates to dataset

Code cleanup - remove defunct modules
Remove LookupHelper / LookupInit code
Remove pre-RBAC permissions module

* Add mising stateTree / basePath properties to form definitions

* Resolve i18n conflicts in list and form generator
Freeze dependencies

* Integrate sockets

* Final bug splat:
fix jobs > job details and jobs > scheduled routing
fix mis-resolved merge conflicts
swap console.info for $log.debug
2016-10-28 14:28:06 -04:00

466 lines
17 KiB
JavaScript

/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint loopfunc: true */
/**
* @ngdoc function
* @name helpers.function:Hosts
* @description Routines that handle host add/edit/delete on the Inventory detail page.
*/
'use strict';
import listGenerator from '../shared/list-generator/main';
export default
angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'HostListDefinition',
listGenerator.name, 'HostsHelper',
'InventoryHelper', 'InventoryFormDefinition', 'SelectionHelper',
'HostGroupsFormDefinition', 'VariablesHelper', 'ModalDialog', 'StandardOutHelper',
'GroupListDefinition'
])
.factory('SetEnabledMsg', [ function() {
return function(host) {
if (host.has_inventory_sources) {
// Inventory sync managed, so not clickable
host.enabledToolTip = (host.enabled) ? 'Host is available' : 'Host is not available';
}
else {
// Clickable
host.enabledToolTip = (host.enabled) ? 'Host is available. Click to toggle.' : 'Host is not available. Click to toggle.';
}
};
}])
.factory('SetHostStatus', ['SetEnabledMsg', function(SetEnabledMsg) {
return function(host) {
// Set status related fields on a host object
host.activeFailuresLink = '/#/hosts/' + host.id + '/job_host_summaries/?inventory=' + host.inventory +
'&host_name=' + encodeURI(host.name);
if (host.has_active_failures === true) {
host.badgeToolTip = 'Most recent job failed. Click to view jobs.';
host.active_failures = 'failed';
}
else if (host.has_active_failures === false && host.last_job === null) {
host.has_active_failures = 'none';
host.badgeToolTip = "No job data available.";
host.active_failures = 'n/a';
}
else if (host.has_active_failures === false && host.last_job !== null) {
host.badgeToolTip = "Most recent job successful. Click to view jobs.";
host.active_failures = 'success';
}
host.enabled_flag = host.enabled;
SetEnabledMsg(host);
};
}])
.factory('SetStatus', ['$filter', 'SetEnabledMsg', 'Empty', function($filter, SetEnabledMsg, Empty) {
return function(params) {
var scope = params.scope,
host = params.host,
i, html, title;
function ellipsis(a) {
if (a.length > 25) {
return a.substr(0,25) + '...';
}
return a;
}
function noRecentJobs() {
title = 'No job data';
html = "<p>No recent job data available for this host.</p>\n";
}
function setMsg(host) {
var j, job, jobs;
if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) {
if (host.has_active_failures === true) {
host.badgeToolTip = 'Most recent job failed. Click to view jobs.';
host.active_failures = 'error';
}
else {
host.badgeToolTip = "Most recent job successful. Click to view jobs.";
host.active_failures = 'successful';
}
if (host.summary_fields.recent_jobs.length > 0) {
// build html table of job status info
jobs = host.summary_fields.recent_jobs.sort(
function(a,b) {
// reverse numerical order
return -1 * (a - b);
});
title = "Recent Jobs";
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>\n";
html += "<th>Status</th>\n";
html += "<th>Finished</th>\n";
html += "<th>Name</th>\n";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
for (j=0; j < jobs.length; j++) {
job = jobs[j];
html += "<tr>\n";
// SmartStatus-tooltips are named --success whereas icon-job uses successful
var iconStatus = (job.status === 'successful') ? 'success' : 'failed';
html += "<td><a href=\"#/jobs/" + job.id + "\"><i class=\"fa DashboardList-status SmartStatus-tooltip--" + iconStatus + " icon-job-" +
job.status + "\"></i></a></td>\n";
html += "<td>" + ($filter('longDate')(job.finished)).replace(/ /,'<br />') + "</td>\n";
html += "<td class=\"break\"><a href=\"#/jobs/" + job.id + "\" " +
"aw-tool-tip=\"" + job.status.charAt(0).toUpperCase() + job.status.slice(1) +
". Click for details\" data-placement=\"top\">" + ellipsis(job.name) + "</a></td>\n";
html += "</tr>\n";
}
html += "</tbody>\n";
html += "</table>\n";
}
else {
noRecentJobs();
}
}
else if (host.has_active_failures === false && host.last_job === null) {
host.badgeToolTip = "No job data available.";
host.active_failures = 'none';
noRecentJobs();
}
host.job_status_html = html;
host.job_status_title = title;
}
if (!Empty(host)) {
// update single host
setMsg(host);
SetEnabledMsg(host);
}
else {
// update all hosts
for (i=0; i < scope.hosts.length; i++) {
setMsg(scope.hosts[i]);
SetEnabledMsg(scope.hosts[i]);
}
}
};
}])
.factory('HostsReload', [ '$stateParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'Wait',
'SetHostStatus', 'SetStatus', 'ApplyEllipsis',
function($stateParams, Empty, InventoryHosts, GetBasePath, Wait, SetHostStatus, SetStatus,
ApplyEllipsis) {
return function(params) {
var scope = params.scope,
parent_scope = params.parent_scope;
// @issue: OLD SEARCH
// var list = InventoryHosts,
// group_id = params.group_id,
// inventory_id = params.inventory_id;
// pageSize = (params.pageSize) ? params.pageSize : 20,
//
// url = ( !Empty(group_id) ) ? GetBasePath('groups') + group_id + '/all_hosts/' :
// GetBasePath('inventory') + inventory_id + '/hosts/';
// @issue: OLD SEARCH
// scope.search_place_holder='Search ' + scope.selected_group_name;
if (scope.removeHostsReloadPostRefresh) {
scope.removeHostsReloadPostRefresh();
}
scope.removeHostsReloadPostRefresh = scope.$on('PostRefresh', function(e, set) {
if (set === 'hosts') {
for (var i=0; i < scope.hosts.length; i++) {
//Set tooltip for host enabled flag
scope.hosts[i].enabled_flag = scope.hosts[i].enabled;
}
SetStatus({ scope: scope });
setTimeout(function() { ApplyEllipsis('#hosts_table .host-name a'); }, 2500);
Wait('stop');
if (parent_scope) {
parent_scope.$emit('HostReloadComplete');
}
}
});
// @issue: OLD SEARCH
// SearchInit({ scope: scope, set: 'hosts', list: list, url: url });
// PaginateInit({ scope: scope, list: list, url: url, pageSize: pageSize });
//
// if ($stateParams.host_name) {
// scope[list.iterator + 'InputDisable'] = false;
// scope[list.iterator + 'SearchValue'] = $stateParams.host_name;
// scope[list.iterator + 'SearchField'] = 'name';
// scope[list.iterator + 'SearchFieldLabel'] = list.fields.name.label;
// scope[list.iterator + 'SearchSelectValue'] = null;
// }
//
// if (scope.show_failures) {
// scope[list.iterator + 'InputDisable'] = true;
// scope[list.iterator + 'SearchValue'] = 'true';
// scope[list.iterator + 'SearchField'] = 'has_active_failures';
// scope[list.iterator + 'SearchFieldLabel'] = list.fields.has_active_failures.label;
// scope[list.iterator + 'SearchSelectValue'] = { value: 1 };
// }
// scope.search(list.iterator, null, true);
};
}])
.factory('HostsCopy', ['$compile', 'Rest', 'ProcessErrors', 'CreateDialog', 'GetBasePath', 'Wait', 'generateList', 'GroupList',
function($compile, Rest, ProcessErrors, CreateDialog, GetBasePath, Wait, GenerateList, GroupList) {
return function(params) {
var host_id = params.host_id,
group_scope = params.group_scope,
parent_scope = params.host_scope,
parent_group = group_scope.selected_group_id,
scope = parent_scope.$new(),
buttonSet, url, host;
buttonSet = [{
label: "Cancel",
onClick: function() {
scope.cancel();
},
icon: "fa-times",
"class": "btn btn-default",
"id": "host-copy-cancel-button"
},{
label: "OK",
onClick: function() {
scope.performCopy();
},
icon: "fa-check",
"class": "btn btn-primary",
"id": "host-copy-ok-button"
}];
if (scope.removeHostCopyPostRefresh) {
scope.removeHostCopyPostRefresh();
}
scope.removeHostCopyPostRefresh = scope.$on('PostRefresh', function() {
scope.copy_groups.forEach(function(row, i) {
scope.copy_groups[i].checked = '0';
});
Wait('stop');
$('#host-copy-dialog').dialog('open');
$('#host-copy-ok-button').attr('disabled','disabled');
// prevent backspace from navigation when not in input or textarea field
$(document).on("keydown", function (e) {
if (e.which === 8 && !$(e.target).is('input[type="text"], textarea')) {
e.preventDefault();
}
});
});
if (scope.removeHostCopyDialogReady) {
scope.removeHostCopyDialogReady();
}
scope.removeCopyDialogReady = scope.$on('HostCopyDialogReady', function() {
// @issue: OLD SEARCH
// var url = GetBasePath('inventory') + group_scope.inventory.id + '/groups/';
GenerateList.inject(GroupList, {
mode: 'lookup',
id: 'copy-host-select-container',
scope: scope
//,
//instructions: instructions
});
// @issue: OLD SEARCH
// SearchInit({
// scope: scope,
// set: GroupList.name,
// list: GroupList,
// url: url
// });
// PaginateInit({
// scope: scope,
// list: GroupList,
// url: url,
// mode: 'lookup'
// });
// scope.search(GroupList.iterator, null, true, false);
});
if (scope.removeShowDialog) {
scope.removeShowDialog();
}
scope.removeShowDialog = scope.$on('ShowDialog', function() {
var d;
scope.name = host.name;
scope.copy_choice = "copy";
d = angular.element(document.getElementById('host-copy-dialog'));
$compile(d)(scope);
CreateDialog({
id: 'host-copy-dialog',
scope: scope,
buttons: buttonSet,
width: 650,
height: 650,
minWidth: 600,
title: 'Copy or Move Host',
callback: 'HostCopyDialogReady',
onClose: function() {
scope.cancel();
}
});
});
Wait('start');
url = GetBasePath('hosts') + host_id + '/';
Rest.setUrl(url);
Rest.get()
.success(function(data) {
host = data;
scope.$emit('ShowDialog');
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. GET returned: ' + status });
});
scope.cancel = function() {
$(document).off("keydown");
try {
$('#host-copy-dialog').dialog('close');
}
catch(e) {
// ignore
}
// @issue: OLD SEARCH
// scope.searchCleanup();
// @issue: OLD SEARCH
// group_scope.restoreSearch(); // Restore all parent search stuff and refresh hosts and groups lists
scope.$destroy();
};
scope['toggle_' + GroupList.iterator] = function (id) {
var count = 0,
list = GroupList;
scope[list.name].forEach( function(row, i) {
if (row.id === id) {
if (row.checked) {
scope[list.name][i].success_class = 'success';
}
else {
scope[list.name][i].success_class = '';
}
} else {
scope[list.name][i].checked = 0;
scope[list.name][i].success_class = '';
}
});
// Check if any rows are checked
scope[list.name].forEach(function(row) {
if (row.checked) {
count++;
}
});
if (count === 0) {
$('#host-copy-ok-button').attr('disabled','disabled');
}
else {
$('#host-copy-ok-button').removeAttr('disabled');
}
};
scope.performCopy = function() {
var list = GroupList,
target,
url;
Wait('start');
if (scope.use_root_group) {
target = null;
}
else {
scope[list.name].every(function(row) {
if (row.checked === 1) {
target = row;
return false;
}
return true;
});
}
if (scope.copy_choice === 'move') {
// Respond to move
// disassociate the host from the original parent
if (scope.removeHostRemove) {
scope.removeHostRemove();
}
scope.removeHostRemove = scope.$on('RemoveHost', function () {
if (parent_group > 0) {
// Only remove a host from a parent when the parent is a group and not the inventory root
url = GetBasePath('groups') + parent_group + '/hosts/';
Rest.setUrl(url);
Rest.post({ id: host.id, disassociate: 1 })
.success(function () {
scope.cancel();
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to remove ' + host.name + ' from group ' + parent_group + '. POST returned: ' + status });
});
} else {
scope.cancel();
}
});
// add the new host to the target
url = GetBasePath('groups') + target.id + '/hosts/';
Rest.setUrl(url);
Rest.post(host)
.success(function () {
scope.$emit('RemoveHost');
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status });
});
}
else {
// Respond to copy by adding the new host to the target
url = GetBasePath('groups') + target.id + '/hosts/';
Rest.setUrl(url);
Rest.post(host)
.success(function () {
scope.cancel();
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status
});
});
}
};
};
}]);