mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 07:26:03 -03:30
Move code under lib/ansible to js/shared
This commit is contained in:
159
awx/ui/static/js/shared/AuthService.js
Normal file
159
awx/ui/static/js/shared/AuthService.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:AuthService
|
||||
* @description AuthService.js
|
||||
*
|
||||
* User authentication functions
|
||||
*
|
||||
*/
|
||||
|
||||
import Utilities from './Utilities';
|
||||
|
||||
export default
|
||||
angular.module('AuthService', ['ngCookies', Utilities.name])
|
||||
|
||||
.factory('Authorization', ['$http', '$rootScope', '$location', '$cookieStore', 'GetBasePath', 'Store',
|
||||
function ($http, $rootScope, $location, $cookieStore, GetBasePath, Store) {
|
||||
return {
|
||||
setToken: function (token, expires) {
|
||||
// set the session cookie
|
||||
$cookieStore.remove('token');
|
||||
$cookieStore.remove('token_expires');
|
||||
$cookieStore.remove('userLoggedIn');
|
||||
$cookieStore.put('token', token);
|
||||
$cookieStore.put('token_expires', expires);
|
||||
$cookieStore.put('userLoggedIn', true);
|
||||
$cookieStore.put('sessionExpired', false);
|
||||
$rootScope.token = token;
|
||||
$rootScope.userLoggedIn = true;
|
||||
$rootScope.token_expires = expires;
|
||||
$rootScope.sessionExpired = false;
|
||||
},
|
||||
|
||||
isUserLoggedIn: function () {
|
||||
if ($rootScope.userLoggedIn === undefined) {
|
||||
// Browser refresh may have occurred
|
||||
$rootScope.userLoggedIn = $cookieStore.get('userLoggedIn');
|
||||
$rootScope.sessionExpired = $cookieStore.get('sessionExpired');
|
||||
}
|
||||
return $rootScope.userLoggedIn;
|
||||
},
|
||||
|
||||
getToken: function () {
|
||||
return ($rootScope.token) ? $rootScope.token : $cookieStore.get('token');
|
||||
},
|
||||
|
||||
retrieveToken: function (username, password) {
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: GetBasePath('authtoken'),
|
||||
data: {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
// the following puts our primary scope up for garbage collection, which
|
||||
// should prevent content flash from the prior user.
|
||||
var scope = angular.element(document.getElementById('main-view')).scope();
|
||||
scope.$destroy();
|
||||
//$rootScope.$destroy();
|
||||
$cookieStore.remove('token_expires');
|
||||
$cookieStore.remove('current_user');
|
||||
|
||||
if($cookieStore.get('lastPath')==='/portal'){
|
||||
$cookieStore.put( 'lastPath', '/portal');
|
||||
$rootScope.lastPath = '/portal';
|
||||
}
|
||||
else {
|
||||
$cookieStore.remove('lastPath');
|
||||
$rootScope.lastPath = '/home';
|
||||
}
|
||||
|
||||
$cookieStore.remove('token');
|
||||
$cookieStore.put('userLoggedIn', false);
|
||||
$cookieStore.put('sessionExpired', false);
|
||||
$cookieStore.put('current_user', {});
|
||||
$rootScope.current_user = {};
|
||||
$rootScope.license_tested = undefined;
|
||||
$rootScope.userLoggedIn = false;
|
||||
$rootScope.sessionExpired = false;
|
||||
$rootScope.token = null;
|
||||
$rootScope.token_expires = null;
|
||||
$rootScope.login_username = null;
|
||||
$rootScope.login_password = null;
|
||||
},
|
||||
|
||||
getLicense: function () {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: GetBasePath('config'),
|
||||
headers: {
|
||||
'Authorization': 'Token ' + this.getToken()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setLicense: function (data) {
|
||||
var license = data.license_info;
|
||||
license.version = data.version;
|
||||
license.tested = false;
|
||||
Store('license', license);
|
||||
},
|
||||
|
||||
licenseTested: function () {
|
||||
var license, result;
|
||||
if ($rootScope.license_tested !== undefined) {
|
||||
result = $rootScope.license_tested;
|
||||
} else {
|
||||
// User may have hit browser refresh
|
||||
license = Store('license');
|
||||
$rootScope.version = license.version;
|
||||
if (license && license.tested !== undefined) {
|
||||
result = license.tested;
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
getUser: function () {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: '/api/v1/me/',
|
||||
headers: {
|
||||
'Authorization': 'Token ' + this.getToken()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setUserInfo: function (response) {
|
||||
// store the response values in $rootScope so we can get to them later
|
||||
$rootScope.current_user = response.results[0];
|
||||
$cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh
|
||||
$rootScope.$emit('OpenSocket');
|
||||
},
|
||||
|
||||
restoreUserInfo: function () {
|
||||
$rootScope.current_user = $cookieStore.get('current_user');
|
||||
},
|
||||
|
||||
getUserInfo: function (key) {
|
||||
// Access values returned from the Me API call
|
||||
var cu;
|
||||
if ($rootScope.current_user) {
|
||||
return $rootScope.current_user[key];
|
||||
}
|
||||
this.restoreUserInfo();
|
||||
cu = $cookieStore.get('current_user');
|
||||
return cu[key];
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
606
awx/ui/static/js/shared/InventoryTree.js
Normal file
606
awx/ui/static/js/shared/InventoryTree.js
Normal file
@@ -0,0 +1,606 @@
|
||||
/************************************
|
||||
*
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:inventoryTree
|
||||
* @description
|
||||
* InventoryTree.js
|
||||
*
|
||||
* Build data for the tree selector table used on inventory detail page.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'PromptDialog'])
|
||||
|
||||
.factory('SortNodes', [
|
||||
function () {
|
||||
return function (data) {
|
||||
//Sort nodes by name
|
||||
var i, j, names = [], newData = [];
|
||||
for (i = 0; i < data.length; i++) {
|
||||
names.push(data[i].name);
|
||||
}
|
||||
names.sort();
|
||||
for (j = 0; j < names.length; j++) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
if (data[i].name === names[j]) {
|
||||
newData.push(data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newData;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('BuildTree', ['$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'SortNodes', 'Wait', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Store',
|
||||
function ($location, Rest, GetBasePath, ProcessErrors, SortNodes, Wait, GetSyncStatusMsg, GetHostsStatusMsg, Store) {
|
||||
return function (params) {
|
||||
|
||||
var inventory_id = params.inventory_id,
|
||||
scope = params.scope,
|
||||
refresh = params.refresh,
|
||||
emit = params.emit,
|
||||
new_group_id = params.new_group_id,
|
||||
groups = [],
|
||||
id = 1,
|
||||
local_child_store,
|
||||
path = $location.path();
|
||||
|
||||
function buildAllHosts(tree_data) {
|
||||
// Start our tree object with All Hosts
|
||||
var children = [],
|
||||
sorted = SortNodes(tree_data),
|
||||
j, all_hosts;
|
||||
|
||||
for (j = 0; j < sorted.length; j++) {
|
||||
children.push(sorted[j].id);
|
||||
}
|
||||
|
||||
all_hosts = {
|
||||
name: 'All Hosts',
|
||||
id: 1,
|
||||
group_id: null,
|
||||
parent: 0,
|
||||
description: '',
|
||||
show: true,
|
||||
ngicon: null,
|
||||
has_children: false,
|
||||
related: {},
|
||||
selected_class: '',
|
||||
show_failures: false,
|
||||
isDraggable: false,
|
||||
isDroppable: true,
|
||||
children: children
|
||||
};
|
||||
groups.push(all_hosts);
|
||||
}
|
||||
|
||||
function getExpandState(key) {
|
||||
var result = true;
|
||||
local_child_store.every(function(child) {
|
||||
if (child.key === key) {
|
||||
result = child.expand;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getShowState(key) {
|
||||
var result = null;
|
||||
local_child_store.every(function(child) {
|
||||
if (child.key === key) {
|
||||
result = (child.show !== undefined) ? child.show : true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildGroups(tree_data, parent, level) {
|
||||
|
||||
var children, stat, hosts_status, group,
|
||||
sorted = SortNodes(tree_data),
|
||||
expand, show;
|
||||
|
||||
sorted.forEach( function(row, i) {
|
||||
id++;
|
||||
|
||||
stat = GetSyncStatusMsg({
|
||||
status: sorted[i].summary_fields.inventory_source.status,
|
||||
has_inventory_sources: sorted[i].has_inventory_sources,
|
||||
source: ( (sorted[i].summary_fields.inventory_source) ? sorted[i].summary_fields.inventory_source.source : null )
|
||||
}); // from helpers/Groups.js
|
||||
|
||||
hosts_status = GetHostsStatusMsg({
|
||||
active_failures: sorted[i].hosts_with_active_failures,
|
||||
total_hosts: sorted[i].total_hosts,
|
||||
inventory_id: inventory_id,
|
||||
group_id: sorted[i].id
|
||||
}); // from helpers/Groups.js
|
||||
|
||||
children = [];
|
||||
sorted[i].children.forEach( function(child, j) {
|
||||
children.push(sorted[i].children[j].id);
|
||||
});
|
||||
|
||||
expand = (sorted[i].children.length > 0) ? getExpandState(sorted[i].id) : false;
|
||||
show = getShowState(sorted[i].id);
|
||||
if (show === null) {
|
||||
// this is a node we haven't seen before, so check the parent expand/collapse state
|
||||
// If parent is not expanded, then child should be hidden.
|
||||
show = true;
|
||||
if (parent > 0) {
|
||||
groups.every(function(g) {
|
||||
if (g.id === parent) {
|
||||
show = getExpandState(g.key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
group = {
|
||||
name: sorted[i].name,
|
||||
has_active_failures: sorted[i].has_active_failures,
|
||||
total_hosts: sorted[i].total_hosts,
|
||||
hosts_with_active_failures: sorted[i].hosts_with_active_failures,
|
||||
total_groups: sorted[i].total_groups,
|
||||
groups_with_active_failures: sorted[i].groups_with_active_failures,
|
||||
parent: parent,
|
||||
has_children: (sorted[i].children.length > 0) ? true : false,
|
||||
has_inventory_sources: sorted[i].has_inventory_sources,
|
||||
id: id,
|
||||
source: sorted[i].summary_fields.inventory_source.source,
|
||||
key: sorted[i].id,
|
||||
group_id: sorted[i].id,
|
||||
event_level: level,
|
||||
children: children,
|
||||
show: show,
|
||||
related: sorted[i].related,
|
||||
status: sorted[i].summary_fields.inventory_source.status,
|
||||
status_class: stat['class'],
|
||||
status_tooltip: stat.tooltip,
|
||||
launch_tooltip: stat.launch_tip,
|
||||
launch_class: stat.launch_class,
|
||||
hosts_status_tip: hosts_status.tooltip,
|
||||
show_failures: hosts_status.failures,
|
||||
hosts_status_class: hosts_status['class'],
|
||||
inventory_id: inventory_id,
|
||||
selected_class: '',
|
||||
isDraggable: true,
|
||||
isDroppable: true
|
||||
};
|
||||
if (sorted[i].children.length > 0) {
|
||||
if (expand) {
|
||||
group.ngicon = 'fa fa-minus-square-o node-toggle';
|
||||
}
|
||||
else {
|
||||
group.ngicon = 'fa fa-plus-square-o node-toggle';
|
||||
}
|
||||
}
|
||||
else {
|
||||
group.ngicon = 'fa fa-square-o node-no-toggle';
|
||||
}
|
||||
if (new_group_id && group.group_id === new_group_id) {
|
||||
scope.selected_tree_id = id;
|
||||
scope.selected_group_id = group.group_id;
|
||||
}
|
||||
groups.push(group);
|
||||
if (sorted[i].children.length > 0) {
|
||||
buildGroups(sorted[i].children, id, 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) {
|
||||
buildAllHosts(data);
|
||||
buildGroups(data, 0, 0);
|
||||
scope.autoShowGroupHelp = (data.length === 0) ? true : false;
|
||||
if (refresh) {
|
||||
scope.groups = groups;
|
||||
scope.$emit('GroupTreeRefreshed', inventory_name, groups, emit);
|
||||
} else {
|
||||
scope.$emit('GroupTreeLoaded', inventory_name, groups, emit);
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to get inventory tree for: ' + inventory_id + '. GET returned: ' + status
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function loadTreeData() {
|
||||
// Load the inventory root node
|
||||
Wait('start');
|
||||
Rest.setUrl(GetBasePath('inventory') + inventory_id + '/');
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
scope.$emit('buildAllGroups', data.name, data.related.tree, data.related.groups);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status
|
||||
});
|
||||
});
|
||||
}
|
||||
local_child_store = Store(path + '_children');
|
||||
if (!local_child_store) {
|
||||
local_child_store = [];
|
||||
}
|
||||
loadTreeData();
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
// Update a group with a set of properties
|
||||
.factory('UpdateGroup', ['ApplyEllipsis', 'GetSyncStatusMsg', 'Empty',
|
||||
function (ApplyEllipsis, GetSyncStatusMsg, Empty) {
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope,
|
||||
group_id = params.group_id,
|
||||
properties = params.properties,
|
||||
i, p, grp, old_name, stat;
|
||||
|
||||
for (i = 0; i < scope.groups.length; i++) {
|
||||
if (scope.groups[i].id === group_id) {
|
||||
grp = scope.groups[i];
|
||||
for (p in properties) {
|
||||
if (p === 'name') {
|
||||
old_name = scope.groups[i].name;
|
||||
}
|
||||
if (p === 'source') {
|
||||
if (properties[p] !== scope.groups[i][p]) {
|
||||
// User changed source
|
||||
if (!Empty(properties[p]) && (scope.groups[i].status === 'none' || Empty(scope.groups[i].status))) {
|
||||
// We have a source but no status, seed the status with 'never' to enable sync button
|
||||
scope.groups[i].status = 'never updated';
|
||||
} else if (!properties[p]) {
|
||||
// User removed source
|
||||
scope.groups[i].status = 'none';
|
||||
}
|
||||
// Update date sync status links/icons
|
||||
stat = GetSyncStatusMsg({
|
||||
status: scope.groups[i].status,
|
||||
has_inventory_sources: properties.has_inventory_sources,
|
||||
source: properties.source
|
||||
});
|
||||
scope.groups[i].status_class = stat['class'];
|
||||
scope.groups[i].status_tooltip = stat.tooltip;
|
||||
scope.groups[i].launch_tooltip = stat.launch_tip;
|
||||
scope.groups[i].launch_class = stat.launch_class;
|
||||
}
|
||||
}
|
||||
scope.groups[i][p] = properties[p];
|
||||
}
|
||||
}
|
||||
/*if (scope.groups[i].id === scope.selected_tree_id) {
|
||||
//Make sure potential group name change gets reflected throughout the page
|
||||
scope.selected_group_name = scope.groups[i].name;
|
||||
scope.search_place_holder = 'Search ' + scope.groups[i].name;
|
||||
scope.hostSearchPlaceholder = 'Search ' + scope.groups[i].name;
|
||||
}*/
|
||||
}
|
||||
|
||||
// Update any titles attributes created by ApplyEllipsis
|
||||
if (old_name) {
|
||||
setTimeout(function () {
|
||||
$('#groups_table .group-name a[title="' + old_name + '"]').attr('title', properties.name);
|
||||
ApplyEllipsis('#groups_table .group-name a');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
// Set node name and description after an update to Group properties.
|
||||
.factory('SetNodeName', [
|
||||
function () {
|
||||
return function (params) {
|
||||
var name = params.name,
|
||||
descr = params.description,
|
||||
group_id = (params.group_id !== undefined) ? params.group_id : null,
|
||||
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 () {
|
||||
$(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);
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
// Copy or Move a group on the tree after drag-n-drop
|
||||
.factory('CopyMoveGroup', ['$compile', 'Alert', 'ProcessErrors', 'Find', 'Wait', 'Rest', 'Empty', 'GetBasePath',
|
||||
function ($compile, Alert, ProcessErrors, Find, Wait, Rest, Empty, GetBasePath) {
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope,
|
||||
target = Find({ list: scope.groups, key: 'id', val: params.target_tree_id }),
|
||||
inbound = Find({ list: scope.groups, key: 'id', val: params.inbound_tree_id }),
|
||||
e, html = '';
|
||||
|
||||
// Build the html for our prompt dialog
|
||||
html += "<div id=\"copy-prompt-modal\" class=\"modal fade\">\n";
|
||||
html += "<div class=\"modal-dialog\">\n";
|
||||
html += "<div class=\"modal-content\">\n";
|
||||
html += "<div class=\"modal-header\">\n";
|
||||
html += "<button type=\"button\" class=\"close\" data-target=\"#copy-prompt-modal\" " +
|
||||
"data-dismiss=\"modal\" aria-hidden=\"true\">×</button>\n";
|
||||
|
||||
if (target.id === 1 || inbound.parent === 0) {
|
||||
// We're moving the group to the top level, or we're moving a top level group down
|
||||
html += "<h3>Move Group</h3>\n";
|
||||
} else {
|
||||
html += "<h3>Copy or Move?</h3>\n";
|
||||
}
|
||||
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-body\">\n";
|
||||
|
||||
if (target.id === 1) {
|
||||
html += "<div class=\"alert alert-info\">Are you sure you want to move group " + inbound.name + " to the top level?</div>";
|
||||
} else if (inbound.parent === 0) {
|
||||
html += "<div class=\"alert alert-info\">Are you sure you want to move group " + inbound.name + " from the top level and make it a child of " +
|
||||
target.name + "?</div>";
|
||||
} else {
|
||||
html += "<div class=\"text-center\">\n";
|
||||
html += "<p>Would you like to copy or move group <em>" + inbound.name + "</em> to group <em>" + target.name + "</em>?</p>\n";
|
||||
html += "<div style=\"margin-top: 30px;\">\n";
|
||||
html += "<a href=\"\" ng-click=\"moveGroup()\" class=\"btn btn-primary\" style=\"margin-right: 15px;\"><i class=\"fa fa-cut\"></i> Move</a>\n";
|
||||
html += "<a href=\"\" ng-click=\"copyGroup()\" class=\"btn btn-primary\"><i class=\"fa fa-copy\"></i> Copy</a>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-footer\">\n";
|
||||
html += "<a href=\"#\" data-target=\"#prompt-modal\" data-dismiss=\"modal\" class=\"btn btn-default\">Cancel</a>\n";
|
||||
|
||||
if (target.id === 1 || inbound.parent === 0) {
|
||||
// We're moving the group to the top level, or we're moving a top level group down
|
||||
html += "<a href=\"\" data-target=\"#prompt-modal\" ng-click=\"moveGroup()\" class=\"btn btn-primary\">Yes</a>\n";
|
||||
}
|
||||
|
||||
html += "</div>\n";
|
||||
html += "</div><!-- modal-content -->\n";
|
||||
html += "</div><!-- modal-dialog -->\n";
|
||||
html += "</div><!-- modal -->\n";
|
||||
|
||||
// Inject our custom dialog
|
||||
e= angular.element(document.getElementById('inventory-modal-container'));
|
||||
e.empty().append(html);
|
||||
$compile(e)(scope);
|
||||
|
||||
// Display it
|
||||
$('#copy-prompt-modal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: true,
|
||||
show: true
|
||||
});
|
||||
|
||||
// Respond to move
|
||||
scope.moveGroup = function () {
|
||||
var url, group, parent;
|
||||
$('#copy-prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
|
||||
// disassociate the group from the original parent
|
||||
if (scope.removeGroupRemove) {
|
||||
scope.removeGroupRemove();
|
||||
}
|
||||
scope.removeGroupRemove = scope.$on('removeGroup', function () {
|
||||
if (inbound.parent > 0) {
|
||||
// Only remove a group from a parent when the parent is a group and not the inventory root
|
||||
parent = Find({ list: scope.groups, key: 'id', val: inbound.parent });
|
||||
url = GetBasePath('base') + 'groups/' + parent.group_id + '/children/';
|
||||
Rest.setUrl(url);
|
||||
Rest.post({ id: inbound.group_id, disassociate: 1 })
|
||||
.success(function () {
|
||||
//Triggers refresh of group list in inventory controller
|
||||
scope.$emit('GroupDeleteCompleted');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to remove ' + inbound.name +
|
||||
' from ' + parent.name + '. POST returned status: ' + status
|
||||
});
|
||||
});
|
||||
} else {
|
||||
//Triggers refresh of group list in inventory controller
|
||||
scope.$emit('GroupDeleteCompleted');
|
||||
}
|
||||
});
|
||||
|
||||
// add the new group to the target parent
|
||||
url = (!Empty(target.group_id)) ?
|
||||
GetBasePath('base') + 'groups/' + target.group_id + '/children/' :
|
||||
GetBasePath('inventory') + scope.inventory_id + '/groups/';
|
||||
group = {
|
||||
id: inbound.group_id,
|
||||
name: inbound.name,
|
||||
description: inbound.description,
|
||||
inventory: scope.inventory_id
|
||||
};
|
||||
Rest.setUrl(url);
|
||||
Rest.post(group)
|
||||
.success(function () {
|
||||
scope.$emit('removeGroup');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
var target_name = (Empty(target.group_id)) ? 'inventory' : target.name;
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to add ' + inbound.name + ' to ' + target_name + '. POST returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
scope.copyGroup = function () {
|
||||
$('#copy-prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
// add the new group to the target parent
|
||||
var url = (!Empty(target.group_id)) ?
|
||||
GetBasePath('base') + 'groups/' + target.group_id + '/children/' :
|
||||
GetBasePath('inventory') + scope.inventory_id + '/groups/',
|
||||
group = {
|
||||
id: inbound.group_id,
|
||||
name: inbound.name,
|
||||
description: inbound.description,
|
||||
inventory: scope.inventory_id
|
||||
};
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.post(group)
|
||||
.success(function () {
|
||||
//Triggers refresh of group list in inventory controller
|
||||
scope.$emit('GroupDeleteCompleted');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
var target_name = (Empty(target.group_id)) ? 'inventory' : target.name;
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to add ' + inbound.name + ' to ' + target_name + '. POST returned status: ' + status
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Copy a host after drag-n-drop
|
||||
.factory('CopyMoveHost', ['$compile', 'Alert', 'ProcessErrors', 'Find', 'Wait', 'Rest', 'Empty', 'GetBasePath',
|
||||
function ($compile, Alert, ProcessErrors, Find, Wait, Rest, Empty, GetBasePath) {
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope,
|
||||
target = Find({ list: scope.groups, key: 'id', val: params.target_tree_id }),
|
||||
host = Find({ list: scope.hosts, key: 'id', val: params.host_id }),
|
||||
found = false, e, i, html = '';
|
||||
|
||||
if (host.summary_fields.all_groups) {
|
||||
for (i = 0; i < host.summary_fields.all_groups.length; i++) {
|
||||
if (host.summary_fields.all_groups[i].id === target.group_id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
html += "<div id=\"copy-alert-modal\" class=\"modal fade\">\n";
|
||||
html += "<div class=\"modal-dialog\">\n";
|
||||
html += "<div class=\"modal-content\">\n";
|
||||
html += "<div class=\"modal-header\">\n";
|
||||
html += "<button type=\"button\" class=\"close\" ng-hide=\"disableButtons\" data-target=\"#copy-alert-modal\"\n";
|
||||
html += "data-dismiss=\"modal\" class=\"modal\" aria-hidden=\"true\">×</button>\n";
|
||||
html += "<h3>Already in Group</h3>\n";
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-body\">\n";
|
||||
html += "<div class=\"alert alert-info\"><p>Host " + host.name + " is already in group " + target.name + ".</p></div>\n";
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-footer\">\n";
|
||||
html += "<a href=\"#\" data-target=\"#copy-alert-modal\" data-dismiss=\"modal\" class=\"btn btn-primary\">OK</a>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
|
||||
// Inject our custom dialog
|
||||
e = angular.element(document.getElementById('inventory-modal-container'));
|
||||
e.empty().append(html);
|
||||
$compile(e)(scope);
|
||||
|
||||
// Display it
|
||||
$('#copy-alert-modal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: true,
|
||||
show: true
|
||||
});
|
||||
|
||||
} else {
|
||||
// Build the html for our prompt dialog
|
||||
html = '';
|
||||
html += "<div id=\"copy-prompt-modal\" class=\"modal fade\">\n";
|
||||
html += "<div class=\"modal-dialog\">\n";
|
||||
html += "<div class=\"modal-content\">\n";
|
||||
html += "<div class=\"modal-header\">\n";
|
||||
html += "<button type=\"button\" class=\"close\" data-target=\"#copy-prompt-modal\" " +
|
||||
"data-dismiss=\"modal\" aria-hidden=\"true\">×</button>\n";
|
||||
html += "<h3>Copy Host</h3>\n";
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-body\">\n";
|
||||
html += "<div class=\"alert alert-info\">Are you sure you want to copy host " + host.name + ' to group ' + target.name + '?</div>';
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"modal-footer\">\n";
|
||||
html += "<a href=\"#\" data-target=\"#prompt-modal\" data-dismiss=\"modal\" class=\"btn btn-default\">No</a>\n";
|
||||
html += "<a href=\"\" data-target=\"#prompt-modal\" ng-click=\"copyHost()\" class=\"btn btn-primary\">Yes</a>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div><!-- modal-content -->\n";
|
||||
html += "</div><!-- modal-dialog -->\n";
|
||||
html += "</div><!-- modal -->\n";
|
||||
|
||||
// Inject our custom dialog
|
||||
e = angular.element(document.getElementById('inventory-modal-container'));
|
||||
e.empty().append(html);
|
||||
$compile(e)(scope);
|
||||
|
||||
// Display it
|
||||
$('#copy-prompt-modal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: true,
|
||||
show: true
|
||||
});
|
||||
|
||||
scope.copyHost = function () {
|
||||
$('#copy-prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
Rest.setUrl(GetBasePath('groups') + target.group_id + '/hosts/');
|
||||
Rest.post(host)
|
||||
.success(function () {
|
||||
// Signal the controller to refresh the hosts view
|
||||
scope.$emit('GroupTreeRefreshed');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Wait('stop');
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to add ' + host.name + ' to ' +
|
||||
target.name + '. POST returned status: ' + status });
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
240
awx/ui/static/js/shared/Modal.js
Normal file
240
awx/ui/static/js/shared/Modal.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/************************************
|
||||
*
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:Modal
|
||||
* @description
|
||||
* Modal.js
|
||||
*
|
||||
* Create a draggable, resizable modal dialog using jQueryUI.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Modal#CreateDialog
|
||||
* @methodOf lib.ansible.function:Modal
|
||||
* @description
|
||||
* CreateDialog({
|
||||
* id: - id attribute value of the target DOM element
|
||||
* scope: - Required, $scope associated with the #id DOM element
|
||||
* buttons: - Required, Array of button objects. See example below.
|
||||
* width: - Desired width of modal dialog on open. Defaults to 500.
|
||||
* height: - Desired height of modal on open. Defaults to 600.
|
||||
* minWidth: - Minimum width that must be maintained regardless of reize attempts. Defaults to 400.
|
||||
* title: - Modal window title, optional
|
||||
* onResizeStop: - Function to call when user stops resizing the dialog, optional
|
||||
* onClose: - Function to call after window closes, optional
|
||||
* onOpen: - Function to call after window opens, optional
|
||||
* beforeDestroy: - Function to call during onClose and prior to destroying the window
|
||||
* callback: - String to pass to scope.$emit() after dialog is created, optional
|
||||
* })
|
||||
*
|
||||
* Note that the dialog will be created but not opened. It's up to the caller to open it. Use callback
|
||||
* option to respond to dialog created event.
|
||||
*/
|
||||
.factory('CreateDialog', ['Empty', function(Empty) {
|
||||
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
buttonSet = params.buttons,
|
||||
width = params.width || 500,
|
||||
height = params.height || 600,
|
||||
minWidth = params.minWidth || 300,
|
||||
title = params.title || '',
|
||||
onResizeStop = params.onResizeStop,
|
||||
onClose = params.onClose,
|
||||
onOpen = params.onOpen,
|
||||
callback = params.callback,
|
||||
beforeDestroy = params.beforeDestroy,
|
||||
closeOnEscape = (params.closeOnEscape === undefined) ? false : params.closeOnEscape,
|
||||
resizable = (params.resizable === undefined) ? true : params.resizable,
|
||||
forms = _.chain([params.form]).flatten().compact().value(),
|
||||
buttons,
|
||||
id = params.id,
|
||||
x, y, wh, ww;
|
||||
|
||||
function updateButtonStatus(isValid) {
|
||||
$('.ui-dialog[aria-describedby="' + id + '"]').find('.btn-primary').prop('disabled', !isValid);
|
||||
}
|
||||
|
||||
if (Empty(buttonSet)) {
|
||||
// Default button object
|
||||
buttonSet = [{
|
||||
label: "OK",
|
||||
onClick: function() {
|
||||
scope.modalOK();
|
||||
},
|
||||
icon: "",
|
||||
"class": "btn btn-primary",
|
||||
"id": "dialog-ok-button"
|
||||
}];
|
||||
}
|
||||
|
||||
buttons = {};
|
||||
buttonSet.forEach( function(btn) {
|
||||
buttons[btn.label] = btn.onClick;
|
||||
});
|
||||
|
||||
// Set modal dimensions based on viewport width
|
||||
ww = $(document).width();
|
||||
wh = $('body').height();
|
||||
x = (width > ww) ? ww - 10 : width;
|
||||
y = (height > wh) ? wh - 10 : height;
|
||||
|
||||
// Create the modal
|
||||
$('#' + id).dialog({
|
||||
buttons: buttons,
|
||||
modal: true,
|
||||
width: x,
|
||||
height: y,
|
||||
autoOpen: false,
|
||||
minWidth: minWidth,
|
||||
title: title,
|
||||
closeOnEscape: closeOnEscape,
|
||||
resizable: resizable,
|
||||
create: function () {
|
||||
// Fix the close button
|
||||
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x');
|
||||
|
||||
setTimeout(function() {
|
||||
// Make buttons bootstrapy
|
||||
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-buttonset button').each(function () {
|
||||
var txt = $(this).text(), self = $(this);
|
||||
buttonSet.forEach(function(btn) {
|
||||
if (txt === btn.label) {
|
||||
self.attr({ "class": btn['class'], "id": btn.id });
|
||||
if (btn.icon) {
|
||||
self.empty().html('<i class="fa ' + btn.icon + '"></i> ' + btn.label);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 300);
|
||||
|
||||
if (forms.length > 0) {
|
||||
forms.map(function(form_ctrl) {
|
||||
scope.$watch(form_ctrl.$name + '.$valid', updateButtonStatus);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
scope.$apply(function() {
|
||||
scope.$emit(callback);
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
resizeStop: function () {
|
||||
// for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100%
|
||||
var dialog = $('.ui-dialog[aria-describedby="' + id + '"]'),
|
||||
titleHeight = dialog.find('.ui-dialog-titlebar').outerHeight(),
|
||||
buttonHeight = dialog.find('.ui-dialog-buttonpane').outerHeight(),
|
||||
content = dialog.find('#' + id);
|
||||
content.width(dialog.width() - 28);
|
||||
content.css({ height: (dialog.height() - titleHeight - buttonHeight - 10) });
|
||||
if (onResizeStop) {
|
||||
onResizeStop();
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
// Destroy on close
|
||||
$('.tooltip').each(function () {
|
||||
// Remove any lingering tooltip <div> elements
|
||||
$(this).remove();
|
||||
});
|
||||
$('.popover').each(function () {
|
||||
// remove lingering popover <div> elements
|
||||
$(this).remove();
|
||||
});
|
||||
if (beforeDestroy) {
|
||||
beforeDestroy();
|
||||
}
|
||||
$('#' + id).dialog('destroy');
|
||||
$('#' + id).hide();
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
open: function () {
|
||||
$('.tooltip').each(function () {
|
||||
// Remove any lingering tooltip <div> elements
|
||||
$(this).remove();
|
||||
});
|
||||
$('.popover').each(function () {
|
||||
// remove lingering popover <div> elements
|
||||
$(this).remove();
|
||||
});
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}])
|
||||
|
||||
/**
|
||||
* TextareaResize({
|
||||
* scope: - $scope associated with the textarea element
|
||||
* textareaId: - id attribute value of the textarea
|
||||
* modalId: - id attribute of the <div> element used to create the modal
|
||||
* formId: - id attribute of the textarea's parent form
|
||||
* parse: - if true, call ParseTypeChange and replace textarea with codemirror editor
|
||||
* fld: - optional, form field name
|
||||
* bottom_margin: - optional, integer value for additional margin to leave below the textarea
|
||||
* onChange; - optional, function to call when the textarea value changes
|
||||
* })
|
||||
*
|
||||
* Use to resize a textarea field contained on a modal. Has only been tested where the
|
||||
* form contains 1 textarea and the the textarea is at the bottom of the form/modal.
|
||||
*
|
||||
**/
|
||||
.factory('TextareaResize', ['ParseTypeChange', 'Wait', function(ParseTypeChange, Wait){
|
||||
return function(params) {
|
||||
|
||||
var scope = params.scope,
|
||||
textareaId = params.textareaId,
|
||||
modalId = params.modalId,
|
||||
formId = params.formId,
|
||||
fld = params.fld,
|
||||
parse = (params.parse === undefined) ? true : params.parse,
|
||||
bottom_margin = (params.bottom_margin) ? params.bottom_margin : 0,
|
||||
onChange = params.onChange,
|
||||
textarea,
|
||||
formHeight, model, windowHeight, offset, rows;
|
||||
|
||||
function waitStop() {
|
||||
Wait('stop');
|
||||
}
|
||||
|
||||
// Attempt to create the largest textarea field that will fit on the window. Minimum
|
||||
// height is 6 rows, so on short windows you will see vertical scrolling
|
||||
textarea = $('#' + textareaId);
|
||||
if (scope.codeMirror) {
|
||||
model = textarea.attr('ng-model');
|
||||
scope[model] = scope.codeMirror.getValue();
|
||||
scope.codeMirror.destroy();
|
||||
}
|
||||
textarea.attr('rows', 1);
|
||||
formHeight = $('#' + formId).height();
|
||||
windowHeight = $('#' + modalId).height() - 20; //leave a margin of 20px
|
||||
offset = Math.floor(windowHeight - formHeight - bottom_margin);
|
||||
rows = Math.floor(offset / 20);
|
||||
rows = (rows < 6) ? 6 : rows;
|
||||
textarea.attr('rows', rows);
|
||||
while(rows > 6 && ($('#' + formId).height() > $('#' + modalId).height() + bottom_margin)) {
|
||||
rows--;
|
||||
textarea.attr('rows', rows);
|
||||
}
|
||||
if (parse) {
|
||||
ParseTypeChange({ scope: scope, field_id: textareaId, onReady: waitStop, variable: fld, onChange: onChange });
|
||||
}
|
||||
};
|
||||
}]);
|
||||
271
awx/ui/static/js/shared/RestServices.js
Normal file
271
awx/ui/static/js/shared/RestServices.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:RestServices
|
||||
* @description
|
||||
*
|
||||
* A wrapper for angular's $http service. Post user authentication API requests should go through Rest rather than directly using $http. The goal is to decouple
|
||||
* the application from $http, allowing Rest or anything that provides the same methods to handle setting authentication headers and performing tasks such as checking
|
||||
* for an expired authentication token. Having this buffer between the application and $http will prove useful should the authentication scheme change.
|
||||
*
|
||||
* #.setUrl(<url>)
|
||||
*
|
||||
* Before calling an action methods (i.e. get, put, post, destroy, options) to send the request, call setUrl. Pass a string containing the URL endpoint and any parameters.
|
||||
* Note that $http will automaticall encode the URL, replacing spaces and special characters with appropriate %hex codes. Example URL values might include:
|
||||
*
|
||||
* ```
|
||||
* /api/v1/inventories/9/
|
||||
* /api/v1/credentials/?name=SSH Key&kind=ssh
|
||||
* ```
|
||||
*
|
||||
* When constructing the URL be sure to use the GetBasePath() method found in lib/ansible/Utilities.js. GetBasePath uses the response objects from /api and
|
||||
* /api/<version>/to construct the base portion of the path. This way the API version number and base endpoints are not hard-coded within the application.
|
||||
*
|
||||
* #Action methods: .get(), put(<JSON data object>), .post(<JSON data object>), .destroy(<JSON data object>), options()
|
||||
*
|
||||
* Use the method matching the REST action to be performed. In the case of put, post and destroy a JSON object can be passed as a parameter. If included,
|
||||
* it will be sent as part of the request.
|
||||
*
|
||||
* In every case a call to an action method returns a promise object, allowing the caller to pass reponse functions to the promise methods. The functions can inspect
|
||||
* the respoinse details and initiate action. For example:
|
||||
*
|
||||
* ```
|
||||
* var url = GetBasePath('inventories') + $routeParams.id + '/';
|
||||
* Rest.setUrl(url);
|
||||
* Rest.get()
|
||||
* .success(function(data) {
|
||||
* // review the data object and take action
|
||||
* })
|
||||
* .error(function(status, data) {
|
||||
* // handle the error - typically a call to ProcessErrors() found in lib/ansible/Utitlties.js
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ##Options Reqeusts
|
||||
*
|
||||
* options() requests are used by the GetChoices() method found in lib/ansible/Utilities.js. Sending an Options request to an API endpoint returns an object that includes
|
||||
* possible values for fields that are typically presented in the UI as dropdowns or <select> elements. GetChoices will inspect the response object for the request
|
||||
* field and return an array of { label: 'Choice Label', value: 'choice 1' } objects.
|
||||
*
|
||||
*/
|
||||
|
||||
import AuthService from './AuthService';
|
||||
|
||||
export default
|
||||
angular.module('RestServices', ['ngCookies', AuthService.name])
|
||||
.factory('Rest', ['$http', '$rootScope', '$cookieStore', '$q', 'Authorization',
|
||||
function ($http, $rootScope, $cookieStore, $q, Authorization) {
|
||||
return {
|
||||
|
||||
headers: {},
|
||||
|
||||
setUrl: function (url) {
|
||||
this.url = url;
|
||||
},
|
||||
checkExpired: function () {
|
||||
return ($rootScope.sessionTimer) ? $rootScope.sessionTimer.isExpired() : false;
|
||||
},
|
||||
pReplace: function () {
|
||||
//in our url, replace :xx params with a value, assuming
|
||||
//we can find it in user supplied params.
|
||||
var key, rgx;
|
||||
for (key in this.params) {
|
||||
rgx = new RegExp("\\:" + key, 'gm');
|
||||
if (rgx.test(this.url)) {
|
||||
this.url = this.url.replace(rgx, this.params[key]);
|
||||
delete this.params[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
createResponse: function (data, status) {
|
||||
// Simulate an http response when a token error occurs
|
||||
// http://stackoverflow.com/questions/18243286/angularjs-promises-simulate-http-promises
|
||||
|
||||
var promise = $q.reject({
|
||||
data: data,
|
||||
status: status
|
||||
});
|
||||
promise.success = function (fn) {
|
||||
promise.then(function (response) {
|
||||
fn(response.data, response.status);
|
||||
}, null);
|
||||
return promise;
|
||||
};
|
||||
promise.error = function (fn) {
|
||||
promise.then(null, function (response) {
|
||||
fn(response.data, response.status);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
},
|
||||
|
||||
setHeader: function (hdr) {
|
||||
// Pass in { key: value } pairs to be added to the header
|
||||
for (var h in hdr) {
|
||||
this.headers[h] = hdr[h];
|
||||
}
|
||||
},
|
||||
get: function (args) {
|
||||
args = (args) ? args : {};
|
||||
this.params = (args.params) ? args.params : null;
|
||||
this.pReplace();
|
||||
var expired = this.checkExpired(),
|
||||
token = Authorization.getToken();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: this.url,
|
||||
headers: this.headers,
|
||||
params: this.params
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
},
|
||||
post: function (data) {
|
||||
var token = Authorization.getToken(),
|
||||
expired = this.checkExpired();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: this.url,
|
||||
headers: this.headers,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
},
|
||||
put: function (data) {
|
||||
var token = Authorization.getToken(),
|
||||
expired = this.checkExpired();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'PUT',
|
||||
url: this.url,
|
||||
headers: this.headers,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
},
|
||||
patch: function (data) {
|
||||
var token = Authorization.getToken(),
|
||||
expired = this.checkExpired();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'PATCH',
|
||||
url: this.url,
|
||||
headers: this.headers,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
},
|
||||
destroy: function (data) {
|
||||
var token = Authorization.getToken(),
|
||||
expired = this.checkExpired();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'DELETE',
|
||||
url: this.url,
|
||||
headers: this.headers,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
},
|
||||
options: function () {
|
||||
var token = Authorization.getToken(),
|
||||
expired = this.checkExpired();
|
||||
if (expired) {
|
||||
return this.createResponse({
|
||||
detail: 'Token is expired'
|
||||
}, 401);
|
||||
} else if (token) {
|
||||
this.setHeader({
|
||||
Authorization: 'Token ' + token
|
||||
});
|
||||
this.setHeader({
|
||||
"X-Auth-Token": 'Token ' + token
|
||||
});
|
||||
return $http({
|
||||
method: 'OPTIONS',
|
||||
url: this.url,
|
||||
headers: this.headers
|
||||
});
|
||||
} else {
|
||||
return this.createResponse({
|
||||
detail: 'Invalid token'
|
||||
}, 401);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
195
awx/ui/static/js/shared/Socket.js
Normal file
195
awx/ui/static/js/shared/Socket.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**************************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:Socket
|
||||
* @description
|
||||
* Socket.js
|
||||
*
|
||||
* Wrapper for lib/socket.io-client/dist/socket.io.js.
|
||||
*/
|
||||
|
||||
/* global io */
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Socket#SocketIO
|
||||
* @methodOf lib.ansible.function:Socket
|
||||
* @description
|
||||
*/
|
||||
export default
|
||||
angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
|
||||
.factory('Socket', ['$rootScope', '$location', '$log', 'Authorization', 'Store', function ($rootScope, $location, $log, Authorization, Store) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
host = $location.host(),
|
||||
endpoint = params.endpoint,
|
||||
protocol = $location.protocol(),
|
||||
config, socketPort,
|
||||
url;
|
||||
|
||||
// Since some pages are opened in a new tab, we might get here before AnsibleConfig is available.
|
||||
// In that case, load from local storage.
|
||||
if ($AnsibleConfig) {
|
||||
socketPort = $AnsibleConfig.websocket_port;
|
||||
}
|
||||
else {
|
||||
$log.debug('getting web socket port from local storage');
|
||||
config = Store('AnsibleConfig');
|
||||
socketPort = config.websocket_port;
|
||||
}
|
||||
url = protocol + '://' + host + ':' + socketPort + '/socket.io/' + endpoint;
|
||||
$log.debug('opening socket connection to: ' + url);
|
||||
|
||||
function getSocketTip(status) {
|
||||
var result = '';
|
||||
switch(status) {
|
||||
case 'error':
|
||||
result = "Live events: error connecting to the Tower server. Click for troubleshooting help.";
|
||||
break;
|
||||
case 'connecting':
|
||||
result = "Live events: attempting to connect to the Tower server. Click for troubleshooting help.";
|
||||
break;
|
||||
case "ok":
|
||||
result = "Live events: connected. Pages containing job status information will automatically update in real-time.";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: scope,
|
||||
url: url,
|
||||
socket: null,
|
||||
|
||||
init: function() {
|
||||
var self = this,
|
||||
token = Authorization.getToken();
|
||||
if (!$rootScope.sessionTimer || ($rootScope.sessionTimer && !$rootScope.sessionTimer.isExpired())) {
|
||||
// We have a valid session token, so attempt socket connection
|
||||
$log.debug('Socket connecting to: ' + url);
|
||||
self.scope.socket_url = url;
|
||||
self.socket = io.connect(url, {
|
||||
query: "Token="+token,
|
||||
headers:
|
||||
{
|
||||
'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt
|
||||
'X-Auth-Token': 'Token ' + token
|
||||
},
|
||||
'connect timeout': 3000,
|
||||
'try multiple transports': false,
|
||||
'max reconnection attempts': 3,
|
||||
'reconnection limit': 3000
|
||||
});
|
||||
|
||||
self.socket.on('connection', function() {
|
||||
$log.debug('Socket connecting...');
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('connect', function() {
|
||||
$log.debug('Socket connection established');
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'ok';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('connect_failed', function(reason) {
|
||||
var r = reason || 'connection refused by host';
|
||||
$log.error('Socket connection failed: ' + r);
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
|
||||
});
|
||||
self.socket.on('diconnect', function() {
|
||||
$log.debug('Socket disconnected');
|
||||
self.scope.$apply(function() {
|
||||
self.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('error', function(reason) {
|
||||
var r = reason || 'connection refused by host';
|
||||
console.error(reason)
|
||||
$log.debug('Socket error: ' + r);
|
||||
$log.error('Socket error: ' + r);
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnecting', function() {
|
||||
$log.debug('Socket attempting reconnect...');
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnect', function() {
|
||||
$log.debug('Socket reconnected');
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'ok';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnect_failed', function(reason) {
|
||||
$log.error('Socket reconnect failed: ' + reason);
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
// encountered expired token, redirect to login page
|
||||
$rootScope.sessionTimer.expireSession();
|
||||
$location.url('/login');
|
||||
}
|
||||
},
|
||||
checkStatus: function() {
|
||||
// Check connection status
|
||||
var self = this;
|
||||
if (self.socket.socket.connected) {
|
||||
self.scope.socketStatus = 'ok';
|
||||
}
|
||||
else if (self.socket.socket.connecting || self.socket.reconnecting) {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
}
|
||||
else {
|
||||
self.scope.socketStatus = 'error';
|
||||
}
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
return self.scope.socketStatus;
|
||||
},
|
||||
on: function (eventName, callback) {
|
||||
var self = this;
|
||||
self.socket.on(eventName, function () {
|
||||
var args = arguments;
|
||||
self.scope.$apply(function () {
|
||||
callback.apply(self.socket, args);
|
||||
});
|
||||
});
|
||||
},
|
||||
emit: function (eventName, data, callback) {
|
||||
var self = this;
|
||||
self.socket.emit(eventName, data, function () {
|
||||
var args = arguments;
|
||||
self.scope.$apply(function () {
|
||||
if (callback) {
|
||||
callback.apply(self.socket, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getUrl: function() {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
};
|
||||
}]);
|
||||
69
awx/ui/static/js/shared/Timer.js
Normal file
69
awx/ui/static/js/shared/Timer.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**************************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:Timer
|
||||
* @description
|
||||
* Timer.js
|
||||
*
|
||||
* Use to track user idle time and expire session. Timeout
|
||||
* duration set in config.js
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Timer#TimerService
|
||||
* @methodOf lib.ansible.function:Timer
|
||||
* @description
|
||||
*/
|
||||
export default
|
||||
angular.module('TimerService', ['ngCookies', 'Utilities'])
|
||||
.factory('Timer', ['$rootScope', '$cookieStore', '$location', 'GetBasePath', 'Empty',
|
||||
function ($rootScope, $cookieStore) {
|
||||
return {
|
||||
|
||||
sessionTime: null,
|
||||
timeout: null,
|
||||
|
||||
getSessionTime: function () {
|
||||
return (this.sessionTime) ? this.sessionTime : $cookieStore.get('sessionTime');
|
||||
},
|
||||
|
||||
isExpired: function () {
|
||||
var stime = this.getSessionTime(),
|
||||
now = new Date().getTime();
|
||||
if ((stime - now) <= 0) {
|
||||
//expired
|
||||
return true;
|
||||
} else {
|
||||
// not expired. move timer forward.
|
||||
this.moveForward();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
expireSession: function () {
|
||||
this.sessionTime = 0;
|
||||
$rootScope.sessionExpired = true;
|
||||
$cookieStore.put('sessionExpired', true);
|
||||
},
|
||||
|
||||
moveForward: function () {
|
||||
var tm, t;
|
||||
tm = ($AnsibleConfig) ? $AnsibleConfig.session_timeout : 1800;
|
||||
t = new Date().getTime() + (tm * 1000);
|
||||
this.sessionTime = t;
|
||||
$cookieStore.put('sessionTime', t);
|
||||
$rootScope.sessionExpired = false;
|
||||
$cookieStore.put('sessionExpired', false);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
this.moveForward();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
865
awx/ui/static/js/shared/Utilities.js
Normal file
865
awx/ui/static/js/shared/Utilities.js
Normal file
@@ -0,0 +1,865 @@
|
||||
/************************************
|
||||
*
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Utility functions
|
||||
*
|
||||
*/
|
||||
|
||||
/* jshint devel:true */
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('Utilities', ['RestServices', 'Utilities'])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#ClearScope
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Place to remove things that might be lingering from a prior tab or view.
|
||||
* This used to destroy the scope, but that causes issues in angular 1.2.x
|
||||
*/
|
||||
.factory('ClearScope', [ '$rootScope', function ($rootScope) {
|
||||
return function () {
|
||||
|
||||
$rootScope.flashMessage = null;
|
||||
|
||||
$('#form-modal .modal-body').empty();
|
||||
$('#form-modal2 .modal-body').empty();
|
||||
|
||||
$('.tooltip').each(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$('.popover').each(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$('.ui-dialog-content').each(function(){
|
||||
$(this).dialog('close');
|
||||
});
|
||||
|
||||
try {
|
||||
$('#help-modal').dialog('close');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#Empty
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description Empty()
|
||||
*
|
||||
* Test if a value is 'empty'. Returns true if val is null | '' | undefined.
|
||||
* Only works on non-Ojbect types.
|
||||
*
|
||||
*/
|
||||
.factory('Empty', [
|
||||
function () {
|
||||
return function (val) {
|
||||
return (val === null || val === undefined || val === '') ? true : false;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#ToggleClass
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
*/
|
||||
.factory('ToggleClass', function () {
|
||||
return function (selector, cssClass) {
|
||||
// Toggles the existance of a css class on a given element
|
||||
if ($(selector) && $(selector).hasClass(cssClass)) {
|
||||
$(selector).removeClass(cssClass);
|
||||
} else if ($(selector)) {
|
||||
$(selector).addClass(cssClass);
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#Alert
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description Pass in the header and message you want displayed on TB modal dialog found in index.html.
|
||||
* Assumes an #id of 'alert-modal'. Pass in an optional TB alert class (i.e. alert-danger, alert-success,
|
||||
* alert-info...). Pass an optional function(){}, if you want a specific action to occur when user
|
||||
* clicks 'OK' button. Set secondAlert to true, when a second dialog is needed.
|
||||
*/
|
||||
.factory('Alert', ['$rootScope', function ($rootScope) {
|
||||
return function (hdr, msg, cls, action, secondAlert, disableButtons, backdrop) {
|
||||
var scope = $rootScope.$new(), alertClass, local_backdrop;
|
||||
if (secondAlert) {
|
||||
|
||||
$('#alertHeader2').html(hdr);
|
||||
$('#alert2-modal-msg').html(msg);
|
||||
|
||||
alertClass = (cls) ? cls : 'alert-danger'; //default alert class is alert-danger
|
||||
local_backdrop = (backdrop === undefined) ? "static" : backdrop;
|
||||
|
||||
$('#alert2-modal-msg').attr({ "class": "alert " + alertClass });
|
||||
$('#alert-modal2').modal({
|
||||
show: true,
|
||||
keyboard: true,
|
||||
backdrop: local_backdrop
|
||||
});
|
||||
scope.disableButtons2 = (disableButtons) ? true : false;
|
||||
|
||||
$('#alert-modal2').on('hidden.bs.modal', function () {
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
});
|
||||
$('#alert-modal2').on('shown.bs.modal', function () {
|
||||
$('#alert2_ok_btn').focus();
|
||||
});
|
||||
$(document).bind('keydown', function (e) {
|
||||
if (e.keyCode === 27 || e.keyCode === 13) {
|
||||
$('#alert-modal2').modal('hide');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
$('#alertHeader').html(hdr);
|
||||
$('#alert-modal-msg').html(msg);
|
||||
alertClass = (cls) ? cls : 'alert-danger'; //default alert class is alert-danger
|
||||
local_backdrop = (backdrop === undefined) ? "static" : backdrop;
|
||||
|
||||
$('#alert-modal-msg').attr({ "class": "alert " + alertClass });
|
||||
$('#alert-modal').modal({
|
||||
show: true,
|
||||
keyboard: true,
|
||||
backdrop: local_backdrop
|
||||
});
|
||||
|
||||
$('#alert-modal').on('hidden.bs.modal', function () {
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
});
|
||||
$('#alert-modal').on('shown.bs.modal', function () {
|
||||
$('#alert_ok_btn').focus();
|
||||
});
|
||||
$(document).bind('keydown', function (e) {
|
||||
if (e.keyCode === 27 || e.keyCode === 13) {
|
||||
$('#alert-modal').modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
scope.disableButtons = (disableButtons) ? true : false;
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#ProcessErrors
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description For handling errors that are returned from the API
|
||||
*/
|
||||
.factory('ProcessErrors', ['$rootScope', '$cookieStore', '$log', '$location', 'Alert', 'Wait',
|
||||
function ($rootScope, $cookieStore, $log, $location, Alert, Wait) {
|
||||
return function (scope, data, status, form, defaultMsg) {
|
||||
var field, fieldErrors, msg, keys;
|
||||
Wait('stop');
|
||||
$log.debug('Debug status: ' + status);
|
||||
$log.debug('Debug data: ');
|
||||
$log.debug(data);
|
||||
if (defaultMsg.msg) {
|
||||
$log.debug('Debug: ' + defaultMsg.msg);
|
||||
}
|
||||
if (status === 403) {
|
||||
msg = 'The API responded with a 403 Access Denied error. ';
|
||||
if (data.detail) {
|
||||
msg += 'Detail: ' + data.detail;
|
||||
} else {
|
||||
msg += 'Please contact your system administrator.';
|
||||
}
|
||||
Alert(defaultMsg.hdr, msg);
|
||||
} else if (status === 410) {
|
||||
Alert('Deleted Object', 'The requested object was previously deleted and can no longer be accessed.');
|
||||
} else if ((status === 'Token is expired') || (status === 401 && data.detail && data.detail === 'Token is expired') ||
|
||||
(status === 401 && data.detail && data.detail === 'Invalid token')) {
|
||||
if ($rootScope.sessionTimer) {
|
||||
$rootScope.sessionTimer.expireSession();
|
||||
}
|
||||
$location.url('/login');
|
||||
} else if (data.non_field_errors) {
|
||||
Alert('Error!', data.non_field_errors);
|
||||
} else if (data.detail) {
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg + ' ' + data.detail);
|
||||
} else if (data.__all__) {
|
||||
if (typeof data.__all__ === 'object' && Array.isArray(data.__all__)) {
|
||||
Alert('Error!', data.__all__[0]);
|
||||
}
|
||||
else {
|
||||
Alert('Error!', data.__all__);
|
||||
}
|
||||
} else if (form) { //if no error code is detected it begins to loop through to see where the api threw an error
|
||||
fieldErrors = false;
|
||||
for (field in form.fields) {
|
||||
if (data[field] && form.fields[field].tab) {
|
||||
// If the form is part of a tab group, activate the tab
|
||||
$('#' + form.name + "_tabs a[href=\"#" + form.fields[field].tab + '"]').tab('show');
|
||||
}
|
||||
if (form.fields[field].realName) {
|
||||
if (data[form.fields[field].realName]) {
|
||||
scope[field + '_api_error'] = data[form.fields[field]][0];
|
||||
//scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false);
|
||||
$('[name="' + form.fields[field].realName + '"]').addClass('ng-invalid');
|
||||
$('[name="' + form.fields[field].realName + '"]').ScrollTo({ "onlyIfOutside": true, "offsetTop": 100 });
|
||||
fieldErrors = true;
|
||||
}
|
||||
}
|
||||
if (form.fields[field].sourceModel) {
|
||||
if (data[field]) {
|
||||
scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '_api_error'] =
|
||||
data[field][0];
|
||||
//scope[form.name + '_form'][form.fields[field].sourceModel + '_' + form.fields[field].sourceField].$setValidity('apiError', false);
|
||||
$('[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').addClass('ng-invalid');
|
||||
$('[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').ScrollTo({ "onlyIfOutside": true, "offsetTop": 100 });
|
||||
fieldErrors = true;
|
||||
}
|
||||
} else {
|
||||
if (data[field]) {
|
||||
scope[field + '_api_error'] = data[field][0];
|
||||
//scope[form.name + '_form'][field].$setValidity('apiError', false);
|
||||
$('[name="' + field + '"]').addClass('ng-invalid');
|
||||
$('[name="' + field + '"]').ScrollTo({ "onlyIfOutside": true, "offsetTop": 100 });
|
||||
fieldErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((!fieldErrors) && defaultMsg) {
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg);
|
||||
}
|
||||
} else if (typeof data === 'object' && Object.keys(data).length > 0) {
|
||||
keys = Object.keys(data);
|
||||
if (Array.isArray(data[keys[0]])) {
|
||||
msg = data[keys[0]][0];
|
||||
}
|
||||
else {
|
||||
msg = data[keys[0]];
|
||||
}
|
||||
Alert(defaultMsg.hdr, msg);
|
||||
} else {
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg);
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.factory('LoadBreadCrumbs', ['$rootScope', '$routeParams', '$location', 'Empty',
|
||||
function ($rootScope, $routeParams, $location, Empty) {
|
||||
return function (crumb) {
|
||||
|
||||
var title, found, j, i, paths, ppath, parent, child;
|
||||
|
||||
function toUppercase(a) {
|
||||
return a.toUpperCase();
|
||||
}
|
||||
|
||||
function singular(a) {
|
||||
return (a === 'ies') ? 'y' : '';
|
||||
}
|
||||
|
||||
//Keep a list of path/title mappings. When we see /organizations/XX in the path, for example,
|
||||
//we'll know the actual organization name it maps to.
|
||||
if (!Empty(crumb)) {
|
||||
found = false;
|
||||
//crumb.title = crumb.title.charAt(0).toUpperCase() + crumb.title.slice(1);
|
||||
for (i = 0; i < $rootScope.crumbCache.length; i++) {
|
||||
if ($rootScope.crumbCache[i].path === crumb.path) {
|
||||
found = true;
|
||||
$rootScope.crumbCache[i] = crumb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
$rootScope.crumbCache.push(crumb);
|
||||
}
|
||||
}
|
||||
paths = $location.path().replace(/^\//, '').split('/');
|
||||
ppath = '';
|
||||
$rootScope.breadcrumbs = [];
|
||||
if (paths.length > 1) {
|
||||
for (i = 0; i < paths.length - 1; i++) {
|
||||
if (i > 0 && paths[i].match(/\d+/)) {
|
||||
parent = paths[i - 1];
|
||||
child = parent.replace(/(ies$|s$)/, singular);
|
||||
child = child.charAt(0).toUpperCase() + child.slice(1);
|
||||
// find the correct title
|
||||
found = false;
|
||||
if ($rootScope.crumbCache) {
|
||||
for (j = 0; j < $rootScope.crumbCache.length; j++) {
|
||||
if ($rootScope.crumbCache[j].path === '/' + parent + '/' + paths[i]) {
|
||||
child = $rootScope.crumbCache[j].title;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found && $rootScope.crumbCache[j].altPath !== undefined) {
|
||||
// Use altPath to override default path construction
|
||||
$rootScope.breadcrumbs.push({
|
||||
title: child,
|
||||
path: $rootScope.crumbCache[j].altPath
|
||||
});
|
||||
} else {
|
||||
$rootScope.breadcrumbs.push({
|
||||
title: child,
|
||||
path: ppath + '/' + paths[i]
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
//if (/_/.test(paths[i])) {
|
||||
// replace '_' with space and uppercase each word
|
||||
|
||||
//}
|
||||
//title = paths[i].charAt(0).toUpperCase() + paths[i].slice(1);
|
||||
title = paths[i].replace(/(?:^|_)\S/g, toUppercase).replace(/_/g, ' ');
|
||||
$rootScope.breadcrumbs.push({
|
||||
title: title,
|
||||
path: ppath + '/' + paths[i]
|
||||
});
|
||||
}
|
||||
ppath += '/' + paths[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#HelpDialog
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description Display a help dialog
|
||||
*
|
||||
* HelpDialog({ defn: <HelpDefinition> })
|
||||
* discuss difference b/t this and other modal windows/dialogs
|
||||
*/
|
||||
.factory('HelpDialog', ['$rootScope', '$compile', '$location', 'Store',
|
||||
function ($rootScope, $compile, $location, Store) {
|
||||
return function (params) {
|
||||
|
||||
var defn = params.defn,
|
||||
current_step = params.step,
|
||||
autoShow = params.autoShow || false,
|
||||
scope = (params.scope) ? params.scope : $rootScope.$new();
|
||||
|
||||
function setButtonMargin() {
|
||||
var width = ($('.ui-dialog[aria-describedby="help-modal-dialog"] .ui-dialog-buttonpane').innerWidth() / 2) - $('#help-next-button').outerWidth() - 93;
|
||||
$('#help-next-button').css({'margin-right': width + 'px'});
|
||||
}
|
||||
|
||||
function showHelp(step) {
|
||||
|
||||
var e, btns, ww, width, height, isOpen = false;
|
||||
current_step = step;
|
||||
|
||||
function buildHtml(step) {
|
||||
var html = '';
|
||||
html += "<h4>" + step.intro + "</h4>\n";
|
||||
if (step.img) {
|
||||
html += "<div class=\"img-container\">\n";
|
||||
html += "<img src=\"" + $basePath + "img/help/" + step.img.src + "\" ";
|
||||
html += (step.img.maxWidth) ? "style=\"max-width:" + step.img.maxWidth + "px\" " : "";
|
||||
html += ">";
|
||||
html += "</div>\n";
|
||||
}
|
||||
if (step.icon) {
|
||||
html += "<div class=\"icon-container\"";
|
||||
html += (step.icon.containerHeight) ? "style=\"height:" + step.icon.containerHeight + "px;\">\n" : "";
|
||||
html += "<i class=\"" + step.icon['class'] + "\" ";
|
||||
html += (step.icon.style) ? "style=\"" + step.icon.style + "\" " : "";
|
||||
html += "></i>\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
html += "<div class=\"help-box\">" + step.box + "</div>";
|
||||
html += (autoShow && step.autoOffNotice) ? "<div class=\"help-auto-off\"><label><input type=\"checkbox\" " +
|
||||
"name=\"auto-off-checkbox\" id=\"auto-off-checkbox\"> Do not show this message in the future</label></div>\n" : "";
|
||||
return html;
|
||||
}
|
||||
|
||||
width = (defn.story.width) ? defn.story.width : 510;
|
||||
height = (defn.story.height) ? defn.story.height : 600;
|
||||
|
||||
// Limit modal width to width of viewport
|
||||
ww = $(document).width();
|
||||
width = (width > ww) ? ww : width;
|
||||
|
||||
try {
|
||||
isOpen = $('#help-modal-dialog').dialog('isOpen');
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
e = angular.element(document.getElementById('help-modal-dialog'));
|
||||
e.empty().html(buildHtml(defn.story.steps[current_step]));
|
||||
setTimeout(function() { scope.$apply(function() { $compile(e)(scope); }); });
|
||||
|
||||
if (!isOpen) {
|
||||
// Define buttons based on story length
|
||||
btns = [];
|
||||
if (defn.story.steps.length > 1) {
|
||||
btns.push({
|
||||
text: "Prev",
|
||||
click: function () {
|
||||
if (current_step - 1 === 0) {
|
||||
$('#help-prev-button').prop('disabled', true);
|
||||
}
|
||||
if (current_step - 1 < defn.story.steps.length - 1) {
|
||||
$('#help-next-button').prop('disabled', false);
|
||||
}
|
||||
showHelp(current_step - 1);
|
||||
},
|
||||
disabled: true
|
||||
});
|
||||
btns.push({
|
||||
text: "Next",
|
||||
click: function() {
|
||||
if (current_step + 1 > 0) {
|
||||
$('#help-prev-button').prop('disabled', false);
|
||||
}
|
||||
if (current_step + 1 >= defn.story.steps.length - 1) {
|
||||
$('#help-next-button').prop('disabled', true);
|
||||
}
|
||||
showHelp(current_step + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
btns.push({
|
||||
text: "Close",
|
||||
click: function () {
|
||||
$('#help-modal-dialog').dialog('close');
|
||||
}
|
||||
});
|
||||
|
||||
$('.overlay').css({
|
||||
width: $(document).width(),
|
||||
height: $(document).height()
|
||||
}).fadeIn();
|
||||
|
||||
// Show the dialog
|
||||
$('#help-modal-dialog').dialog({
|
||||
position: {
|
||||
my: "center top",
|
||||
at: "center top+150",
|
||||
of: 'body'
|
||||
},
|
||||
title: defn.story.hdr,
|
||||
width: width,
|
||||
height: height,
|
||||
buttons: btns,
|
||||
closeOnEscape: true,
|
||||
show: 500,
|
||||
hide: 500,
|
||||
resizable: false,
|
||||
close: function () {
|
||||
$('.overlay').hide();
|
||||
$('#help-modal-dialog').empty();
|
||||
}
|
||||
});
|
||||
|
||||
// Make the buttons look like TB and add FA icons
|
||||
$('.ui-dialog-buttonset button').each(function () {
|
||||
var c, h, i, l;
|
||||
l = $(this).text();
|
||||
if (l === 'Close') {
|
||||
h = "fa-times";
|
||||
c = "btn btn-default";
|
||||
i = "help-close-button";
|
||||
$(this).attr({
|
||||
'class': c,
|
||||
'id': i
|
||||
}).html("<i class=\"fa " + h + "\"></i> Close");
|
||||
} else if (l === 'Prev') {
|
||||
h = "fa-chevron-left";
|
||||
c = "btn btn-primary";
|
||||
i = "help-prev-button";
|
||||
$(this).attr({
|
||||
'class': c,
|
||||
'id': i
|
||||
}).html("<i class=\"fa " + h + "\"></i> Prev");
|
||||
} else {
|
||||
h = "fa-chevron-right";
|
||||
c = "btn btn-primary";
|
||||
i = "help-next-button";
|
||||
$(this).attr({
|
||||
'class': c,
|
||||
'id': i
|
||||
}).html("Next <i class=\"fa " + h + "\"></i>").css({
|
||||
'margin-right': '20px'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('.ui-dialog[aria-describedby="help-modal-dialog"]').find('.ui-dialog-titlebar button')
|
||||
.empty().attr({
|
||||
'class': 'close'
|
||||
}).text('x');
|
||||
|
||||
// If user clicks the checkbox, update local storage
|
||||
$('#auto-off-checkbox').click(function () {
|
||||
if ($('input[name="auto-off-checkbox"]:checked').length) {
|
||||
Store('inventoryAutoHelp', 'off');
|
||||
} else {
|
||||
Store('inventoryAutoHelp', 'on');
|
||||
}
|
||||
});
|
||||
|
||||
setButtonMargin();
|
||||
}
|
||||
}
|
||||
|
||||
showHelp(0);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#ReturnToCaller
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Split the current path by '/' and use the array elements from 0 up to and
|
||||
* including idx as the new path. If no idx value supplied, use 0 to length - 1.
|
||||
*
|
||||
*/
|
||||
.factory('ReturnToCaller', ['$location', 'Empty',
|
||||
function ($location, Empty) {
|
||||
return function (idx) {
|
||||
var paths = $location.path().replace(/^\//, '').split('/'),
|
||||
newpath = '',
|
||||
i;
|
||||
idx = (Empty(idx)) ? paths.length - 1 : idx + 1;
|
||||
for (i = 0; i < idx; i++) {
|
||||
newpath += '/' + paths[i];
|
||||
}
|
||||
$location.path(newpath);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#FormatDate
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Wrapper for data filter- an attempt to insure all dates display in
|
||||
* the same format. Pass in date object or string. See: http://docs.angularjs.org/api/ng.filter:date
|
||||
*/
|
||||
.factory('FormatDate', ['$filter',
|
||||
function ($filter) {
|
||||
return function (dt) {
|
||||
return $filter('date')(dt, 'MM/dd/yy HH:mm:ss');
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#Wait
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Display a spinning icon in the center of the screen to freeze the
|
||||
* UI while waiting on async things to complete (i.e. API calls).
|
||||
* Wait('start' | 'stop');
|
||||
*
|
||||
*/
|
||||
.factory('Wait', ['$rootScope',
|
||||
function ($rootScope) {
|
||||
return function (directive) {
|
||||
var docw, doch, spinnyw, spinnyh, x, y;
|
||||
if (directive === 'start' && !$rootScope.waiting) {
|
||||
$rootScope.waiting = true;
|
||||
docw = $(window).width();
|
||||
doch = $(window).height();
|
||||
spinnyw = $('.spinny').width();
|
||||
spinnyh = $('.spinny').height();
|
||||
x = (docw - spinnyw) / 2;
|
||||
y = (doch - spinnyh) / 2;
|
||||
$('.overlay').css({
|
||||
width: $(document).width(),
|
||||
height: $(document).height()
|
||||
}).fadeIn();
|
||||
$('.spinny').css({
|
||||
top: y,
|
||||
left: x
|
||||
}).fadeIn(400);
|
||||
} else if (directive === 'stop' && $rootScope.waiting) {
|
||||
$('.spinny, .overlay').fadeOut(400, function () {
|
||||
$rootScope.waiting = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('HideElement', [
|
||||
function () {
|
||||
return function (selector, action) {
|
||||
// Fade-in a cloack or vail or a specific element
|
||||
var target = $(selector),
|
||||
width = target.css('width'),
|
||||
height = target.css('height'),
|
||||
position = target.position(),
|
||||
parent = target.parent(),
|
||||
borderRadius = target.css('border-radius'),
|
||||
backgroundColor = target.css('background-color'),
|
||||
margin = target.css('margin'),
|
||||
padding = target.css('padding');
|
||||
|
||||
parent.append("<div id=\"curtain-div\" style=\"" +
|
||||
"position: absolute;" +
|
||||
"top: " + position.top + "px; " +
|
||||
"left: " + position.left + "px; " +
|
||||
"z-index: 1000; " +
|
||||
"width: " + width + "; " +
|
||||
"height: " + height + "; " +
|
||||
"background-color: " + backgroundColor + "; " +
|
||||
"margin: " + margin + "; " +
|
||||
"padding: " + padding + "; " +
|
||||
"border-radius: " + borderRadius + "; " +
|
||||
"opacity: .75; " +
|
||||
"display: none; " +
|
||||
"\"></div>");
|
||||
$('#curtain-div').show(0, action);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('ShowElement', [
|
||||
function () {
|
||||
return function () {
|
||||
// And Fade-out the cloack revealing the element
|
||||
$('#curtain-div').fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#GetChoices
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description Make an Options call to the API and retrieve dropdown options
|
||||
* GetChoices({
|
||||
* scope: Parent $scope
|
||||
* url: API resource to access
|
||||
* field: API element in the response object that contains the option list.
|
||||
* variable: Scope variable that will receive the list.
|
||||
* callback: Optional. Will issue scope.$emit(callback) on completion.
|
||||
* choice_name: Optional. Used when list is found in a variable other than 'choices'.
|
||||
* })
|
||||
*/
|
||||
.factory('GetChoices', ['Rest', 'ProcessErrors',
|
||||
function (Rest, ProcessErrors) {
|
||||
return function (params) {
|
||||
var scope = params.scope,
|
||||
url = params.url,
|
||||
field = params.field,
|
||||
variable = params.variable,
|
||||
callback = params.callback,
|
||||
choice_name = params.choice_name;
|
||||
|
||||
if (scope[variable]) {
|
||||
scope[variable].length = 0;
|
||||
} else {
|
||||
scope[variable] = [];
|
||||
}
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.success(function (data) {
|
||||
var choices;
|
||||
choices = (choice_name) ? data.actions.GET[field][choice_name] : data.actions.GET[field].choices;
|
||||
// including 'name' property so list can be used by search
|
||||
choices.forEach(function(choice) {
|
||||
scope[variable].push({
|
||||
label: choice[1],
|
||||
value: choice[0],
|
||||
name: choice[1]
|
||||
});
|
||||
});
|
||||
if (callback) {
|
||||
scope.$emit(callback);
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to get ' + url + '. GET status: ' + status });
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#Find
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* Search an array of objects, returning the matchting object or null
|
||||
*
|
||||
* Find({ list: [], key: "key", val: <key value> });
|
||||
*/
|
||||
.factory('Find', [
|
||||
function () {
|
||||
return function (params) {
|
||||
var list = params.list,
|
||||
key = params.key,
|
||||
val = params.val,
|
||||
found = false,
|
||||
i;
|
||||
if (typeof list === 'object' && Array.isArray(list)) {
|
||||
for (i = 0; i < params.list.length; i++) {
|
||||
if (list[i][key] === val) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (found) ? list[i] : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#DebugForm
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* DebugForm({ form: <form object>, scope: <current scope object> });
|
||||
*
|
||||
* Use to log the $pristine and $valid properties of each form element. Helpful when form
|
||||
* buttons fail to enable/disable properly.
|
||||
*
|
||||
*/
|
||||
.factory('DebugForm', [
|
||||
function () {
|
||||
return function (params) {
|
||||
var form = params.form,
|
||||
scope = params.scope,
|
||||
fld;
|
||||
for (fld in form.fields) {
|
||||
if (scope[form.name + '_form'][fld]) {
|
||||
console.log(fld + ' valid: ' + scope[form.name + '_form'][fld].$valid);
|
||||
}
|
||||
if (form.fields[fld].sourceModel) {
|
||||
if (scope[form.name + '_form'][form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]) {
|
||||
console.log(form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField + ' valid: ' +
|
||||
scope[form.name + '_form'][form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField].$valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('form pristine: ' + scope[form.name + '_form'].$pristine);
|
||||
console.log('form valid: ' + scope[form.name + '_form'].$valid);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#Store
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description Store
|
||||
*
|
||||
* Wrapper for local storage. All local storage requests flow through here so that we can
|
||||
* stringify/unstringify objects and respond to future issues in one place. For example,
|
||||
* we may at some point want to only use session storage rather than local storage. We might
|
||||
* want to add a test for whether or not local/session storage exists for the browser, etc.
|
||||
*
|
||||
* store(key,value) will store the value using the key
|
||||
*
|
||||
* store(key) retrieves the value of the key
|
||||
* discuss use case
|
||||
*/
|
||||
.factory('Store', ['Empty',
|
||||
function (Empty) {
|
||||
return function (key, value) {
|
||||
if (!Empty(value)) {
|
||||
// Store the value
|
||||
localStorage[key] = JSON.stringify(value);
|
||||
} else if (!Empty(key)) {
|
||||
// Return the value
|
||||
var val = localStorage[key];
|
||||
return (!Empty(val)) ? JSON.parse(val) : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:Utilities#ApplyEllipsis
|
||||
* @methodOf lib.ansible.function:Utilities
|
||||
* @description
|
||||
* ApplyEllipsis()
|
||||
* discuss significance
|
||||
*/
|
||||
.factory('ApplyEllipsis', [
|
||||
function () {
|
||||
return function (selector) {
|
||||
// Add a hidden element to the DOM. We'll use this to calc the px length of
|
||||
// our target text.
|
||||
var tmp = $('#string-test');
|
||||
if (!tmp.length) {
|
||||
$('body').append('<div style="display:none;" id="string-test"></div>');
|
||||
tmp = $('#string-test');
|
||||
}
|
||||
// Find and process the text.
|
||||
$(selector).each(function () {
|
||||
var setTitle = true,
|
||||
txt, w, pw, cw, df;
|
||||
if ($(this).attr('title')) {
|
||||
txt = $(this).attr('title');
|
||||
setTitle = false;
|
||||
} else {
|
||||
txt = $(this).text();
|
||||
}
|
||||
tmp.text(txt);
|
||||
w = tmp.width(); //text width
|
||||
pw = $(this).parent().width(); //parent width
|
||||
if (w > pw) {
|
||||
// text is wider than parent width
|
||||
if (setTitle) {
|
||||
// Save the original text in the title
|
||||
$(this).attr('title', txt);
|
||||
}
|
||||
cw = w / txt.length; // px width per character
|
||||
df = w - pw; // difference in px
|
||||
txt = txt.substr(0, txt.length - (Math.ceil(df / cw) + 3));
|
||||
$(this).text(txt + '...');
|
||||
}
|
||||
if (pw > w && !setTitle) {
|
||||
// the parent has expanded and we previously set the title text
|
||||
txt = $(this).attr('title');
|
||||
$(this).text(txt);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
]);
|
||||
76
awx/ui/static/js/shared/api-loader.js
Normal file
76
awx/ui/static/js/shared/api-loader.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name lib.ansible
|
||||
* @description lib files
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:api-loader
|
||||
* @description Read /api and /api/X to discover all the base paths needed
|
||||
* to access the primary model objects.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ApiLoader', ['Utilities'])
|
||||
|
||||
.factory('LoadBasePaths', ['$http', '$rootScope', 'Store', 'ProcessErrors',
|
||||
function ($http, $rootScope, Store, ProcessErrors) {
|
||||
return function () {
|
||||
|
||||
$http({ method: 'GET', url:'/api/', headers: { 'Authorization': "" } })
|
||||
.success(function (data) {
|
||||
var base = data.current_version;
|
||||
$http({ method: 'GET', url:base, headers: { 'Authorization': "" } })
|
||||
.success(function (data) {
|
||||
data.base = base;
|
||||
$rootScope.defaultUrls = data;
|
||||
Store('api', data);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
$rootScope.defaultUrls = {
|
||||
status: 'error'
|
||||
};
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error',
|
||||
msg: 'Failed to read ' + base + '. GET status: ' + status
|
||||
});
|
||||
});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
$rootScope.defaultUrls = {
|
||||
status: 'error'
|
||||
};
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error',
|
||||
msg: 'Failed to read /api. GET status: ' + status
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('GetBasePath', ['$rootScope', 'Store', 'LoadBasePaths', 'Empty',
|
||||
function ($rootScope, Store, LoadBasePaths, Empty) {
|
||||
return function (set) {
|
||||
// use /api/v1/ results to construct API URLs.
|
||||
if (Empty($rootScope.defaultUrls)) {
|
||||
// browser refresh must have occurred. load from local storage
|
||||
if (Store('api')) {
|
||||
$rootScope.defaultUrls = Store('api');
|
||||
return $rootScope.defaultUrls[set];
|
||||
}
|
||||
return ''; //we should never get here
|
||||
}
|
||||
return $rootScope.defaultUrls[set];
|
||||
};
|
||||
}
|
||||
]);
|
||||
1073
awx/ui/static/js/shared/directives.js
Normal file
1073
awx/ui/static/js/shared/directives.js
Normal file
File diff suppressed because it is too large
Load Diff
95
awx/ui/static/js/shared/filters.js
Normal file
95
awx/ui/static/js/shared/filters.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:filters
|
||||
* @description
|
||||
* Custom filters
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('AWFilters', [])
|
||||
|
||||
//
|
||||
// capitalize -capitalize the first letter of each word
|
||||
//
|
||||
.filter('capitalize', [ function () {
|
||||
return function (input) {
|
||||
var values, result, i;
|
||||
if (input) {
|
||||
values = input.replace(/\_/g, ' ').split(" ");
|
||||
result = "";
|
||||
for (i = 0; i < values.length; i++) {
|
||||
result += values[i].charAt(0).toUpperCase() + values[i].substr(1) + ' ';
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
//
|
||||
// Filter an object of objects by id using an array of id values
|
||||
// Created specifically for Filter Events on job detail page.
|
||||
//
|
||||
.filter('FilterById', [ function() {
|
||||
return function(input, list) {
|
||||
var results = [];
|
||||
if (input && list.length > 0) {
|
||||
list.forEach(function(itm) {
|
||||
input.forEach(function(row) {
|
||||
if (row.id === itm) {
|
||||
results.push(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
return results;
|
||||
}
|
||||
return input;
|
||||
};
|
||||
}])
|
||||
|
||||
.filter('FilterByField', [ function() {
|
||||
return function(input, list) {
|
||||
var fld, key, search = {}, results = {};
|
||||
for (fld in list) {
|
||||
if (list[fld]) {
|
||||
search[fld] = list[fld];
|
||||
}
|
||||
}
|
||||
if (Object.keys(search).length > 0) {
|
||||
for (fld in search) {
|
||||
for (key in input) {
|
||||
if (input[key][fld] === search[fld]) {
|
||||
results[key] = input[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
return input;
|
||||
};
|
||||
}])
|
||||
|
||||
.filter('FilterFailedEvents', [ function() {
|
||||
return function(input, liveEventProcessing, searchAllStatus) {
|
||||
var results = [];
|
||||
if (liveEventProcessing) {
|
||||
// while live events are happening, we don't want angular to filter out anything
|
||||
return input;
|
||||
}
|
||||
else if (searchAllStatus === 'failed') {
|
||||
// filter by failed
|
||||
input.forEach(function(row) {
|
||||
if (row.status === 'failed') {
|
||||
results.push(row);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
return input;
|
||||
};
|
||||
}]);
|
||||
1659
awx/ui/static/js/shared/form-generator.js
Normal file
1659
awx/ui/static/js/shared/form-generator.js
Normal file
File diff suppressed because it is too large
Load Diff
872
awx/ui/static/js/shared/generator-helpers.js
Normal file
872
awx/ui/static/js/shared/generator-helpers.js
Normal file
@@ -0,0 +1,872 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:generator-helpers
|
||||
* @description
|
||||
* GeneratorHelpers
|
||||
*
|
||||
* Functions shared between FormGenerator and ListGenerator
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('GeneratorHelpers', [])
|
||||
|
||||
.factory('Attr', function () {
|
||||
return function (obj, key, fld) {
|
||||
var i, s, result,
|
||||
value = (typeof obj[key] === "string") ? obj[key].replace(/[\'\"]/g, '"') : obj[key];
|
||||
|
||||
if (/^ng/.test(key)) {
|
||||
result = 'ng-' + key.replace(/^ng/, '').toLowerCase() + "=\"" + value + "\" ";
|
||||
} else if (/^data|^aw/.test(key) && key !== 'awPopOver') {
|
||||
s = '';
|
||||
for (i = 0; i < key.length; i++) {
|
||||
if (/[A-Z]/.test(key.charAt(i))) {
|
||||
s += '-' + key.charAt(i).toLowerCase();
|
||||
} else {
|
||||
s += key.charAt(i);
|
||||
}
|
||||
}
|
||||
result = s + "=\"" + value + "\" ";
|
||||
} else {
|
||||
switch (key) {
|
||||
case 'trueValue':
|
||||
result = "ng-true-value=\"" + value + "\" ";
|
||||
break;
|
||||
case 'falseValue':
|
||||
result = "ng-false-value=\"" + value + "\" ";
|
||||
break;
|
||||
case 'awPopOver':
|
||||
// construct the entire help link
|
||||
result = "<a id=\"awp-" + fld + "\" href=\"\" aw-pop-over=\'" + value + "\' ";
|
||||
result += (obj.dataPlacement) ? "data-placement=\"" + obj.dataPlacement + "\" " : "";
|
||||
result += (obj.dataContainer) ? "data-container=\"" + obj.dataContainer + "\" " : "";
|
||||
result += (obj.dataTitle) ? "data-title=\"" + obj.dataTitle + "\" " : "";
|
||||
result += (obj.dataTrigger) ? "data-trigger=\"" + obj.dataTrigger + "\" " : "";
|
||||
result += (obj.awPopOverWatch) ? "aw-pop-over-watch=\"" + obj.awPopOverWatch + "\" " : "";
|
||||
result += "class=\"help-link\" ";
|
||||
result += "><i class=\"fa fa-question-circle\"></i></a> ";
|
||||
break;
|
||||
case 'columnShow':
|
||||
result = "ng-show=\"" + value + "\" ";
|
||||
break;
|
||||
case 'icon':
|
||||
// new method of constructing <i> icon tag. Replaces Icon method.
|
||||
result = "<i class=\"fa fa-" + value;
|
||||
result += (obj.iconSize) ? " " + obj.iconSize : "";
|
||||
result += "\"></i>";
|
||||
break;
|
||||
case 'autocomplete':
|
||||
result = "autocomplete=\"";
|
||||
result += (value) ? 'true' : 'false';
|
||||
result += "\" ";
|
||||
break;
|
||||
default:
|
||||
result = key + "=\"" + value + "\" ";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
})
|
||||
|
||||
.factory('Icon', function () {
|
||||
return function (icon) {
|
||||
return "<i class=\"fa " + icon + "\"></i> ";
|
||||
};
|
||||
})
|
||||
|
||||
.factory('SelectIcon', ['Icon',
|
||||
function (Icon) {
|
||||
return function (params) {
|
||||
// Common point for matching any type of action to the appropriate
|
||||
// icon. The intention is to maintain consistent meaning and presentation
|
||||
// for every icon used in the application.
|
||||
var icon,
|
||||
action = params.action,
|
||||
size = params.size;
|
||||
switch (action) {
|
||||
case 'help':
|
||||
icon = "fa-question-circle";
|
||||
break;
|
||||
case 'add':
|
||||
case 'create':
|
||||
icon = "fa-plus";
|
||||
break;
|
||||
case 'edit':
|
||||
icon = "fa-pencil";
|
||||
break;
|
||||
case 'delete':
|
||||
icon = "fa-trash-o";
|
||||
break;
|
||||
case 'group_update':
|
||||
icon = 'fa-exchange';
|
||||
break;
|
||||
case 'scm_update':
|
||||
icon = 'fa-cloud-download';
|
||||
break;
|
||||
case 'cancel':
|
||||
icon = 'fa-minus-circle';
|
||||
break;
|
||||
case 'run':
|
||||
case 'rerun':
|
||||
case 'submit':
|
||||
icon = 'fa-rocket';
|
||||
break;
|
||||
case 'stream':
|
||||
icon = 'fa-clock-o';
|
||||
break;
|
||||
case 'socket':
|
||||
icon = 'fa-power-off';
|
||||
break;
|
||||
case 'refresh':
|
||||
icon = 'fa-refresh';
|
||||
break;
|
||||
case 'close':
|
||||
icon = 'fa-arrow-left';
|
||||
break;
|
||||
case 'save':
|
||||
case 'form_submit':
|
||||
icon = 'fa-check-square-o';
|
||||
break;
|
||||
case 'properties':
|
||||
icon = "fa-wrench";
|
||||
break;
|
||||
case 'reset':
|
||||
icon = "fa-undo";
|
||||
break;
|
||||
case 'view':
|
||||
icon = "fa-search-plus";
|
||||
break;
|
||||
case 'sync_status':
|
||||
icon = "fa-cloud";
|
||||
break;
|
||||
case 'schedule':
|
||||
icon = "fa-calendar";
|
||||
break;
|
||||
case 'stdout':
|
||||
icon = "fa-external-link";
|
||||
break;
|
||||
case 'question_cancel':
|
||||
icon = 'fa-times';
|
||||
break;
|
||||
case 'job_details':
|
||||
icon = 'fa-list-ul';
|
||||
break;
|
||||
case 'copy':
|
||||
icon = "fa-copy";
|
||||
break;
|
||||
}
|
||||
icon += (size) ? " " + size : "";
|
||||
return Icon(icon);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.factory('Button', ['Attr', 'SelectIcon',
|
||||
function (Attr, SelectIcon) {
|
||||
return function (params) {
|
||||
|
||||
// pass in button object, get back html
|
||||
|
||||
var btn = params.btn,
|
||||
action = params.action, // label used to select the icon
|
||||
toolbar = params.toolbar,
|
||||
html = '';
|
||||
|
||||
if (toolbar) {
|
||||
//if this is a toolbar button, set some defaults
|
||||
btn.class = 'btn-xs btn-primary';
|
||||
btn.iconSize = 'fa-lg';
|
||||
delete btn.label;
|
||||
}
|
||||
|
||||
html += "<button type=\"button\" ";
|
||||
html += "class=\"btn";
|
||||
|
||||
if (btn['class']) {
|
||||
html += ' ' + btn['class'];
|
||||
} else {
|
||||
html += " btn-sm";
|
||||
}
|
||||
|
||||
html += (btn.awPopOver) ? " help-link-white" : "";
|
||||
html += "\" ";
|
||||
html += (btn.ngClick) ? Attr(btn, 'ngClick') : "";
|
||||
|
||||
if (btn.id) {
|
||||
html += "id=\"" + btn.id + "\" ";
|
||||
} else {
|
||||
if (action) {
|
||||
html += "id=\"" + action + "_btn\" ";
|
||||
}
|
||||
}
|
||||
|
||||
html += (btn.ngHide) ? Attr(btn, 'ngHide') : "";
|
||||
html += (btn.awToolTip) ? Attr(btn, 'awToolTip') : "";
|
||||
html += (btn.awToolTip && btn.dataPlacement === undefined) ? "data-placement=\"top\" " : "";
|
||||
html += (btn.dataTipWatch) ? "data-tip-watch=\"" + btn.dataTipWatch + "\" " : "";
|
||||
html += (btn.awPopOver) ? "aw-pop-over=\"" +
|
||||
btn.awPopOver.replace(/[\'\"]/g, '"') + "\" " : "";
|
||||
html += (btn.dataPlacement) ? Attr(btn, 'dataPlacement') : "";
|
||||
html += (btn.dataContainer) ? Attr(btn, 'dataContainer') : "";
|
||||
html += (btn.dataTitle) ? Attr(btn, 'dataTitle') : "";
|
||||
html += (btn.ngShow) ? Attr(btn, 'ngShow') : "";
|
||||
html += (btn.ngHide) ? Attr(btn, 'ngHide') : "";
|
||||
html += (btn.ngDisabled) ? Attr(btn, 'ngDisabled') : "";
|
||||
html += (btn.ngClass) ? Attr(btn, 'ngClass') : "";
|
||||
html += (btn.awTipPlacement) ? Attr(btn, 'awTipPlacement') : "";
|
||||
html += " >";
|
||||
html += (btn.img) ? "<img src=\"" + $basePath + "img/" + btn.img + "\" style=\"width: 12px; height: 12px;\" >" : "";
|
||||
|
||||
if (btn.iconClass) {
|
||||
html += "<i class=\"" + btn.iconClass + "\"></i>";
|
||||
}
|
||||
else {
|
||||
html += SelectIcon({
|
||||
action: action,
|
||||
size: btn.iconSize
|
||||
});
|
||||
}
|
||||
|
||||
html += (btn.label) ? " " + btn.label : "";
|
||||
html += "</button> ";
|
||||
|
||||
return html;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.factory('NavigationLink', ['Attr', 'Icon',
|
||||
function (Attr, Icon) {
|
||||
return function (link) {
|
||||
var html = "<a ";
|
||||
html += (link.href) ? Attr(link, 'href') : '';
|
||||
html += (link.ngClick) ? Attr(link, 'ngClick') : '';
|
||||
html += (link.ngShow) ? Attr(link, 'ngShow') : '';
|
||||
html += '>';
|
||||
html += (link.icon) ? Icon(link.icon) : '';
|
||||
html += (link.label) ? link.label : '';
|
||||
html += "</a>\n";
|
||||
return html;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.factory('DropDown', ['Attr', 'Icon',
|
||||
function (Attr, Icon) {
|
||||
return function (params) {
|
||||
|
||||
var list = params.list,
|
||||
fld = params.fld,
|
||||
i, html, field, name;
|
||||
|
||||
if (params.field) {
|
||||
field = params.field;
|
||||
} else {
|
||||
if (params.type) {
|
||||
field = list[params.type][fld];
|
||||
} else {
|
||||
field = list.fields[fld];
|
||||
}
|
||||
}
|
||||
|
||||
name = field.label.replace(/ /g, '_');
|
||||
|
||||
if (params.td === undefined || params.td !== false) {
|
||||
html = "<td class=\"" + fld + "-column\">\n";
|
||||
} else {
|
||||
html = '';
|
||||
}
|
||||
|
||||
html += "<div class=\"dropdown\">\n";
|
||||
html += "<a href=\"\" class=\"toggle";
|
||||
html += "\" ";
|
||||
html += (field.ngDisabled) ? "ng-disabled=\"" + field.ngDisabled + "\" " : "";
|
||||
html += (field.ngShow) ? "ng-show=\"" + field.ngShow + "\" " : "";
|
||||
html += "data-toggle=\"dropdown\" ";
|
||||
html += ">";
|
||||
html += (field.icon) ? Icon(field.icon) : "";
|
||||
html += field.label;
|
||||
html += " <span class=\"caret\"></span></a>\n";
|
||||
html += "<ul class=\"dropdown-menu pull-right\" role=\"menu\" aria-labelledby=\"dropdownMenu1\">\n";
|
||||
for (i = 0; i < field.options.length; i++) {
|
||||
html += "<li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" ";
|
||||
html += (field.options[i].ngClick) ? "ng-click=\"" + field.options[i].ngClick + "\" " : "";
|
||||
html += (field.options[i].ngHref) ? "ng-href=\"" + field.options[i].ngHref + "\" " : "";
|
||||
html += (field.options[i].ngShow) ? "ng-show=\"" + field.options[i].ngShow + "\" " : "";
|
||||
html += (field.options[i].ngHide) ? "ng-hide=\"" + field.options[i].ngHide + "\" " : "";
|
||||
html += "href=\"\">" + field.options[i].label + "</a></li>\n";
|
||||
}
|
||||
|
||||
html += "</ul>\n";
|
||||
html += "</div>\n";
|
||||
html += (params.td === undefined || params.td !== false) ? "</td>\n" : "";
|
||||
|
||||
return html;
|
||||
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('BadgeCount', [
|
||||
function () {
|
||||
return function (params) {
|
||||
|
||||
// Adds a badge count with optional tooltip
|
||||
|
||||
var list = params.list,
|
||||
fld = params.fld,
|
||||
field = list.fields[fld],
|
||||
html;
|
||||
|
||||
html = "<td class=\"" + fld + "-column";
|
||||
html += (field.columnClass) ? " " + field.columnClass : "";
|
||||
html += "\">\n";
|
||||
html += "<a ng-href=\"" + field.ngHref + "\" aw-tool-tip=\"" + field.awToolTip + "\"";
|
||||
html += (field.dataPlacement) ? " data-placement=\"" + field.dataPlacement + "\"" : "";
|
||||
html += ">";
|
||||
html += "<span class=\"badge";
|
||||
html += (field['class']) ? " " + field['class'] : "";
|
||||
html += "\">";
|
||||
html += "{{ " + list.iterator + '.' + fld + " }}";
|
||||
html += "</span>";
|
||||
html += (field.badgeLabel) ? " " + field.badgeLabel : "";
|
||||
html += "</a>\n";
|
||||
html += "</td>\n";
|
||||
return html;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Badge', [
|
||||
function () {
|
||||
return function (field) {
|
||||
|
||||
// Adds an icon(s) with optional tooltip
|
||||
|
||||
var i, html = '';
|
||||
|
||||
if (field.badges) {
|
||||
for (i = 0; i < field.badges.length; i++) {
|
||||
if (field.badges[i].toolTip) {
|
||||
html += "<a href=\"\" aw-tool-tip=\"" + field.badges[i].toolTip + "\"";
|
||||
html += (field.badges[i].toolTipPlacement) ? " data-placement=\"" + field.badges[i].toolTipPlacement + "\"" : "";
|
||||
html += (field.badges[i].badgeShow) ? " ng-show=\"" + field.badges[i].badgeShow + "\"" : "";
|
||||
html += ">";
|
||||
}
|
||||
html += "<i ";
|
||||
html += (field.badges[i].badgeShow) ? "ng-show=\"" + field.badges[i].badgeShow + "\" " : "";
|
||||
html += " class=\"field-badge " + field.badges[i].icon;
|
||||
html += (field.badges[i].badgeClass) ? " " + field.badges[i].badgeClass : "";
|
||||
html += "\"></i>";
|
||||
if (field.badges[i].toolTip) {
|
||||
html += "</a>";
|
||||
}
|
||||
html += "\n";
|
||||
}
|
||||
} else {
|
||||
if (field.badgeToolTip) {
|
||||
html += "<a ";
|
||||
html += (field.badgeNgHref) ? "ng-href=\"" + field.badgeNgHref + "\" " : "href=\"\"";
|
||||
html += (field.ngClick) ? "ng-click=\"" + field.ngClick + "\" " : "";
|
||||
html += " aw-tool-tip=\"" + field.badgeToolTip + "\"";
|
||||
html += (field.badgeTipPlacement) ? " data-placement=\"" + field.badgeTipPlacement + "\"" : "";
|
||||
html += (field.badgeTipWatch) ? " data-tip-watch=\"" + field.badgeTipWatch + "\"" : "";
|
||||
html += (field.badgeShow) ? " ng-show=\"" + field.badgeShow + "\"" : "";
|
||||
html += ">";
|
||||
}
|
||||
html += "<i ";
|
||||
html += (field.badgeShow) ? "ng-show=\"" + field.badgeShow + "\" " : "";
|
||||
html += " class=\"fa " + field.badgeIcon;
|
||||
html += (field.badgeClass) ? " " + field.badgeClass : "";
|
||||
html += "\"></i>";
|
||||
if (field.badgeToolTip) {
|
||||
html += "</a>";
|
||||
}
|
||||
html += "\n";
|
||||
}
|
||||
return html;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Breadcrumbs', ['$rootScope', 'Attr',
|
||||
function ($rootScope, Attr) {
|
||||
return function (params) {
|
||||
|
||||
// Generate breadcrumbs using the list-generator.js method.
|
||||
|
||||
var list = params.list,
|
||||
mode = params.mode,
|
||||
html = '', itm, navigation;
|
||||
|
||||
html += "<ul class=\"ansible-breadcrumb\" id=\"breadcrumb-list\">\n";
|
||||
html += "<li ng-repeat=\"crumb in breadcrumbs\"><a href=\"{{ '#' + crumb.path }}\">{{ crumb.title }}</a></li>\n";
|
||||
|
||||
if (list.navigationLinks) {
|
||||
navigation = list.navigationLinks;
|
||||
if (navigation.ngHide) {
|
||||
html += "<li class=\"active\" ng-show=\"" + navigation.ngHide + "\">";
|
||||
html += list.editTitle;
|
||||
html += "</li>\n";
|
||||
html += "<li class=\"active\" ng-hide=\"" + navigation.ngHide + "\"> </li>\n";
|
||||
} else {
|
||||
html += "<li class=\"active\"> </li>\n";
|
||||
html += "</ul>\n";
|
||||
}
|
||||
html += "<div class=\"dropdown\" ";
|
||||
html += (navigation.ngHide) ? Attr(navigation, 'ngHide') : '';
|
||||
html += ">\n";
|
||||
for (itm in navigation) {
|
||||
if (typeof navigation[itm] === 'object' && navigation[itm].active) {
|
||||
html += "<a href=\"\" class=\"toggle\" ";
|
||||
html += "data-toggle=\"dropdown\" ";
|
||||
html += ">" + navigation[itm].label + " <i class=\"fa fa-chevron-circle-down crumb-icon\"></i></a>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
html += "<ul class=\"dropdown-menu\" role=\"menu\">\n";
|
||||
for (itm in navigation) {
|
||||
if (typeof navigation[itm] === 'object') {
|
||||
html += "<li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" href=\"" +
|
||||
navigation[itm].href + "\" ";
|
||||
// html += (navigation[itm].active) ? "class=\"active\" " : "";
|
||||
html += ">";
|
||||
html += "<i class=\"fa fa-check\" style=\"visibility: ";
|
||||
html += (navigation[itm].active) ? "visible" : "hidden";
|
||||
html += "\"></i> ";
|
||||
html += navigation[itm].label;
|
||||
html += "</a></li>\n";
|
||||
}
|
||||
}
|
||||
html += "</ul>\n";
|
||||
html += "</div><!-- dropdown -->\n";
|
||||
} else {
|
||||
html += "<li class=\"active\"><a href=\"\">";
|
||||
if (mode === 'select') {
|
||||
html += list.selectTitle;
|
||||
} else {
|
||||
html += list.editTitle;
|
||||
}
|
||||
html += "</a></li>\n</ul>\n";
|
||||
}
|
||||
|
||||
return html;
|
||||
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// List field with multiple icons
|
||||
.factory('BuildLink', ['Attr', 'Icon', function(Attr, Icon){
|
||||
return function(params) {
|
||||
var html = '',
|
||||
field = params.field,
|
||||
list = params.list,
|
||||
base = params.base,
|
||||
fld = params.fld;
|
||||
|
||||
if (field.linkTo) {
|
||||
html += "<a href=\"" + field.linkTo + "\" ";
|
||||
} else if (field.ngClick) {
|
||||
html += "<a href=\"\" " + Attr(field, 'ngClick') + " ";
|
||||
} else if (field.ngHref) {
|
||||
html += "<a ng-href=\"" + field.ngHref + "\" ";
|
||||
} else if (field.link || (field.key && (field.link === undefined || field.link))) {
|
||||
html += "<a href=\"#/" + base + "/{{" + list.iterator + ".id }}\" ";
|
||||
} else {
|
||||
html += "<a href=\"\">";
|
||||
}
|
||||
if (field.awDroppable) {
|
||||
html += Attr(field, 'awDroppable');
|
||||
html += (field.dataAccept) ? Attr(field, 'dataAccept') : '';
|
||||
}
|
||||
if (field.awDraggable) {
|
||||
html += Attr(field, 'awDraggable');
|
||||
html += (field.dataContainment) ? Attr(field, 'dataContainment') : '';
|
||||
html += (field.dataTreeId) ? Attr(field, 'dataTreeId') : '';
|
||||
html += (field.dataGroupId) ? Attr(field, 'dataGroupId') : '';
|
||||
html += (field.dataHostId) ? Attr(field, 'dataHostId') : '';
|
||||
html += (field.dataType) ? Attr(field, 'dataType') : '';
|
||||
}
|
||||
if (field.awToolTip) {
|
||||
html += Attr(field, 'awToolTip');
|
||||
html += (field.dataPlacement && !field.awPopOver) ? Attr(field, 'dataPlacement') : "";
|
||||
html += (field.dataTipWatch) ? Attr(field, 'dataTipWatch') : "";
|
||||
html += (field.awTipPlacement) ? Attr(field, 'awTipPlacement') : "";
|
||||
}
|
||||
if (field.awToolTipEllipses) {
|
||||
html += Attr(field, 'awToolTipEllipses');
|
||||
html += (field.dataPlacement && !field.awPopOver) ? Attr(field, 'dataPlacement') : "";
|
||||
html += (field.dataTipWatch) ? Attr(field, 'dataTipWatch') : "";
|
||||
html += (field.awTipPlacement) ? Attr(field, 'awTipPlacement') : "";
|
||||
}
|
||||
if (field.awPopOver) {
|
||||
html += "aw-pop-over=\"" + field.awPopOver + "\" ";
|
||||
html += (field.dataPlacement) ? "data-placement=\"" + field.dataPlacement + "\" " : "";
|
||||
html += (field.dataTitle) ? "data-title=\"" + field.dataTitle + "\" " : "";
|
||||
}
|
||||
html += (field.ngClass) ? Attr(field, 'ngClass') : '';
|
||||
html += (field.ngEllipsis) ? "data-ng-bind=\"" + list.iterator + "." + fld + "\" data-ellipsis " : "";
|
||||
html += ">";
|
||||
|
||||
// Add icon:
|
||||
if (field.ngShowIcon) {
|
||||
html += "<i ng-show=\"" + field.ngShowIcon + "\" class=\"" + field.icon + "\"></i> ";
|
||||
} else if (field.icon) {
|
||||
html += Icon(field.icon) + " ";
|
||||
}
|
||||
|
||||
// Add data binds
|
||||
if (!field.ngBindHtml && !field.iconOnly && !field.ngEllipsis && (field.showValue === undefined || field.showValue === true)) {
|
||||
if (field.ngBind) {
|
||||
html += "{{ " + field.ngBind;
|
||||
} else {
|
||||
html += "{{" + list.iterator + "." + fld;
|
||||
}
|
||||
if (field.filter) {
|
||||
html += " | " + field.filter + " }}";
|
||||
}
|
||||
else {
|
||||
html += " }}";
|
||||
}
|
||||
}
|
||||
|
||||
// Add additional text:
|
||||
if (field.text) {
|
||||
html += field.text;
|
||||
}
|
||||
html += "</a>";
|
||||
return html;
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('Column', ['Attr', 'Icon', 'DropDown', 'Badge', 'BadgeCount', 'BuildLink',
|
||||
function (Attr, Icon, DropDown, Badge, BadgeCount, BuildLink) {
|
||||
return function (params) {
|
||||
var list = params.list,
|
||||
fld = params.fld,
|
||||
options = params.options,
|
||||
base = params.base,
|
||||
field = list.fields[fld],
|
||||
html = '';
|
||||
|
||||
if (field.type !== undefined && field.type === 'DropDown') {
|
||||
html = DropDown(params);
|
||||
} else if (field.type === 'badgeCount') {
|
||||
html = BadgeCount(params);
|
||||
} else if (field.type === 'badgeOnly') {
|
||||
html = Badge(field);
|
||||
} else {
|
||||
html += "<td class=\"" + fld + "-column";
|
||||
html += (field['class']) ? " " + field['class'] : "";
|
||||
if (options.mode === 'lookup' && field.modalColumnClass) {
|
||||
html += " " + field.modalColumnClass;
|
||||
}
|
||||
else if (field.columnClass) {
|
||||
html += " " + field.columnClass;
|
||||
}
|
||||
html += "\" ";
|
||||
html += (field.ngClass) ? Attr(field, 'ngClass') : "";
|
||||
html += (options.mode === 'lookup' || options.mode === 'select') ? " ng-click=\"toggle_" + list.iterator +
|
||||
"(" + list.iterator + ".id)\"" : "";
|
||||
html += (field.columnShow) ? Attr(field, 'columnShow') : "";
|
||||
html += (field.ngBindHtml) ? "ng-bind-html=\"" + field.ngBindHtml + "\" " : "";
|
||||
html += (field.columnClick) ? "ng-click=\"" + field.columnClick + "\" " : "";
|
||||
html += (field.awEllipsis) ? "aw-ellipsis " : "";
|
||||
html += ">\n";
|
||||
|
||||
// Add ngShow
|
||||
html += (field.ngShow) ? "<span " + Attr(field, 'ngShow') + ">" : "";
|
||||
|
||||
//Add ngHide
|
||||
//html += (field.ngHide) ? "<span " + Attr(field, 'ngHide') + ">" : "";
|
||||
|
||||
// Badge
|
||||
if (options.mode !== 'lookup' && (field.badges || (field.badgeIcon && field.badgePlacement && field.badgePlacement === 'left'))) {
|
||||
html += Badge(field);
|
||||
}
|
||||
|
||||
// Add collapse/expand icon --used on job_events page
|
||||
if (list.hasChildren && field.hasChildren) {
|
||||
html += "<div class=\"level level-{{ " + list.iterator + ".event_level }}\"><a href=\"\" ng-click=\"toggle(" +
|
||||
list.iterator + ".id)\"> " +
|
||||
"<i class=\"{{ " + list.iterator + ".ngicon }}\"></i></a></div>";
|
||||
}
|
||||
|
||||
if (list.name === 'groups') {
|
||||
html += "<div class=\"group-name\">";
|
||||
}
|
||||
if (list.name === 'hosts') {
|
||||
html += "<div class=\"host-name\">";
|
||||
}
|
||||
|
||||
// Start the Link
|
||||
if ((field.key || field.link || field.linkTo || field.ngClick || field.ngHref || field.awToolTip || field.awPopOver) &&
|
||||
options.mode !== 'lookup' && options.mode !== 'select' && !field.noLink && !field.ngBindHtml) {
|
||||
if(field.noLink === true){
|
||||
// provide an override here in case we want key=true for sorting purposes but don't want links -- see: portal mode,
|
||||
}
|
||||
else if (field.icons) {
|
||||
field.icons.forEach(function(icon, idx) {
|
||||
var key, i = field.icons[idx];
|
||||
for (key in i) {
|
||||
field[key] = i[key];
|
||||
}
|
||||
html += BuildLink({
|
||||
list: list,
|
||||
field: field,
|
||||
fld: fld,
|
||||
base: base
|
||||
}) + ' ';
|
||||
});
|
||||
}
|
||||
else {
|
||||
html += BuildLink({
|
||||
list: list,
|
||||
field: field,
|
||||
fld: fld,
|
||||
base: base
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Add icon:
|
||||
if (field.ngShowIcon) {
|
||||
html += "<i ng-show=\"" + field.ngShowIcon + "\" class=\"" + field.icon + "\"></i> ";
|
||||
} else if (field.icon) {
|
||||
html += Icon(field.icon) + " ";
|
||||
}
|
||||
// Add data binds
|
||||
if (!field.ngBindHtml && !field.iconOnly && (field.showValue === undefined || field.showValue === true)) {
|
||||
if (field.ngBind) {
|
||||
html += "{{ " + field.ngBind;
|
||||
} else {
|
||||
html += "{{ " + list.iterator + "." + fld;
|
||||
}
|
||||
if (field.filter) {
|
||||
html += " | " + field.filter + " }}";
|
||||
}
|
||||
else {
|
||||
html += " }}";
|
||||
}
|
||||
}
|
||||
// Add additional text:
|
||||
if (field.text) {
|
||||
html += field.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.name === 'hosts' || list.name === 'groups') {
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
// close ngShow
|
||||
html += (field.ngShow) ? "</span>" : "";
|
||||
|
||||
//close ngHide
|
||||
//html += (field.ngHide) ? "</span>" : "";
|
||||
|
||||
// Specific to Job Events page -showing event detail/results
|
||||
html += (field.appendHTML) ? "<div ng-show=\"" + field.appendHTML + " !== null\" " +
|
||||
"ng-bind-html=\"" + field.appendHTML + "\" " +
|
||||
"class=\"level-{{ " + list.iterator + ".event_level }}-detail\" " +
|
||||
"></div>\n" : "";
|
||||
|
||||
// Badge
|
||||
if (options.mode !== 'lookup' && field.badgeIcon && field.badgePlacement && field.badgePlacement !== 'left') {
|
||||
html += Badge(field);
|
||||
}
|
||||
}
|
||||
return html += "</td>\n";
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('HelpCollapse', function () {
|
||||
return function (params) {
|
||||
|
||||
var hdr = params.hdr,
|
||||
content = params.content,
|
||||
show = params.show,
|
||||
idx = params.idx,
|
||||
bind = params.bind,
|
||||
html = '';
|
||||
|
||||
html += "<div class=\"panel-group collapsible-help\" ";
|
||||
html += (show) ? "ng-show=\"" + show + "\"" : "";
|
||||
html += ">\n";
|
||||
html += "<div class=\"panel panel-default\">\n";
|
||||
html += "<div class=\"panel-heading\" ng-click=\"accordionToggle('#accordion" + idx + "')\">\n";
|
||||
html += "<h4 class=\"panel-title\">\n";
|
||||
//html += "<i class=\"fa fa-question-circle help-collapse\"></i> " + hdr;
|
||||
html += hdr;
|
||||
html += "<i class=\"fa fa-minus pull-right collapse-help-icon\" id=\"accordion" + idx + "-icon\"></i>";
|
||||
html += "</h4>\n";
|
||||
html += "</div>\n";
|
||||
html += "<div id=\"accordion" + idx + "\" class=\"panel-collapse collapse in\">\n";
|
||||
html += "<div class=\"panel-body\" ";
|
||||
html += (bind) ? "ng-bind-html=\"" + bind + "\" " : "";
|
||||
html += ">\n";
|
||||
html += (!bind) ? content : "";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
return html;
|
||||
};
|
||||
})
|
||||
|
||||
.factory('SearchWidget', function () {
|
||||
return function (params) {
|
||||
//
|
||||
// Generate search widget
|
||||
//
|
||||
var iterator = params.iterator,
|
||||
form = params.template,
|
||||
size = params.size,
|
||||
includeSize = (params.includeSize === undefined) ? true : params.includeSize,
|
||||
i, html = '',
|
||||
modifier,
|
||||
searchWidgets = (params.searchWidgets) ? params.searchWidgets : 1,
|
||||
sortedKeys;
|
||||
|
||||
function addSearchFields(idx) {
|
||||
var html = '';
|
||||
sortedKeys = Object.keys(form.fields).sort();
|
||||
sortedKeys.forEach(function(fld) {
|
||||
if ((form.fields[fld].searchable === undefined || form.fields[fld].searchable === true) &&
|
||||
(((form.fields[fld].searchWidget === undefined || form.fields[fld].searchWidget === 1) && idx === 1) ||
|
||||
(form.fields[fld].searchWidget === idx))) {
|
||||
html += "<li><a href=\"\" ng-click=\"setSearchField('" + iterator + "','";
|
||||
html += fld + "','";
|
||||
if (form.fields[fld].searchLabel) {
|
||||
html += form.fields[fld].searchLabel + "', " + idx + ")\">" +
|
||||
form.fields[fld].searchLabel + "</a></li>\n";
|
||||
} else {
|
||||
html += form.fields[fld].label.replace(/<br \/>/g, ' ') + "', " + idx + ")\">" +
|
||||
form.fields[fld].label.replace(/<br \/>/g, ' ') + "</a></li>\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
for (i = 1; i <= searchWidgets; i++) {
|
||||
modifier = (i === 1) ? '' : i;
|
||||
|
||||
if (includeSize) {
|
||||
html += "<div class=\"";
|
||||
html += (size) ? size : "col-lg-4 col-md-6 col-sm-8 col-xs-9";
|
||||
html += "\" id=\"search-widget-container" + modifier + "\">\n";
|
||||
}
|
||||
|
||||
html += "<div class=\"input-group input-group-sm";
|
||||
html += "\">\n";
|
||||
html += "<div class=\"input-group-btn dropdown\">\n";
|
||||
html += "<button type=\"button\" ";
|
||||
html += "id=\"search_field_ddown\" ";
|
||||
html += "class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\"";
|
||||
html += ">\n";
|
||||
html += "<span ng-bind=\"" + iterator + "SearchFieldLabel" + modifier + "\"></span>\n";
|
||||
html += "<span class=\"caret\"></span>\n";
|
||||
html += "</button>\n";
|
||||
html += "<ul class=\"dropdown-menu\" id=\"" + iterator + "SearchDropdown" + modifier + "\">\n";
|
||||
html += addSearchFields(i);
|
||||
html += "</ul>\n";
|
||||
html += "</div><!-- input-group-btn -->\n";
|
||||
|
||||
html += "<select id=\"search_value_select\" ng-show=\"" + iterator + "SelectShow" + modifier + "\" " +
|
||||
"ng-model=\"" + iterator + "SearchSelectValue" + modifier + "\" ng-change=\"search('" + iterator + "')\" ";
|
||||
html += "ng-options=\"c.name for c in " + iterator + "SearchSelectOpts track by c.value" + modifier + "\" class=\"form-control search-select";
|
||||
html += "\"></select>\n";
|
||||
|
||||
|
||||
html += "<input id=\"search_value_input\" type=\"text\" ng-hide=\"" + iterator + "SelectShow" + modifier + " || " +
|
||||
iterator + "InputHide" + modifier + "\" " +
|
||||
"class=\"form-control\" ng-model=\"" + iterator + "SearchValue" + modifier + "\" " +
|
||||
"aw-placeholder=\"" + iterator + "SearchPlaceholder" + modifier + "\" type=\"text\" ng-disabled=\"" + iterator +
|
||||
"InputDisable" + modifier + " || " + iterator + "HoldInput" + modifier + "\" ng-keypress=\"startSearch($event,'" +
|
||||
iterator + "')\">\n";
|
||||
|
||||
// Reset button for drop-down
|
||||
html += "<div class=\"input-group-btn\" ng-show=\"" + iterator + "SelectShow" + modifier + "\" >\n";
|
||||
html += "<button type=\"button\" class=\"btn btn-default btn-small\" id=\"search-reset-button\" ng-click=\"resetSearch('" + iterator + "')\" " +
|
||||
"aw-tool-tip=\"Clear the search\" data-placement=\"top\"><i class=\"fa fa-times\"></i></button>\n";
|
||||
html += "</div><!-- input-group-btn -->\n";
|
||||
|
||||
html += "</div><!-- input-group -->\n";
|
||||
|
||||
html += "<a class=\"search-reset-start\" id=\"search-reset-button\" ng-click=\"resetSearch('" + iterator + "')\"" +
|
||||
"ng-hide=\"" + iterator + "SelectShow" + modifier + " || " + iterator + "InputHide" + modifier + " || " +
|
||||
iterator + "ShowStartBtn" + modifier + " || " +
|
||||
iterator + "HideAllStartBtn" + modifier + "\"" +
|
||||
"><i class=\"fa fa-times\"></i></a>\n";
|
||||
|
||||
html += "<a class=\"search-reset-start\" id=\"search-submit-button\" ng-click=\"search('" + iterator + "')\"" +
|
||||
"ng-hide=\"" + iterator + "SelectShow" + modifier + " || " + iterator + "InputHide" + modifier + " || " +
|
||||
"!" + iterator + "ShowStartBtn" + modifier + " || " +
|
||||
iterator + "HideAllStartBtn" + modifier + "\"" +
|
||||
"><i class=\"fa fa-search\"></i></a>\n";
|
||||
|
||||
html += "<div id=\"search-widget-spacer\" ng-show=\"" + iterator + "SelectShow" + modifier + "\"></div>\n";
|
||||
|
||||
if (includeSize) {
|
||||
html += "</div>\n";
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
|
||||
};
|
||||
})
|
||||
|
||||
.factory('PaginateWidget', [
|
||||
function () {
|
||||
return function (params) {
|
||||
var iterator = params.iterator,
|
||||
set = params.set,
|
||||
html = '';
|
||||
html += "<!-- Paginate Widget -->\n";
|
||||
html += "<div id=\"" + iterator + "-pagination\" class=\"row page-row\">\n";
|
||||
html += "<div class=\"col-lg-8 col-md-8\">\n";
|
||||
html += "<ul id=\"pagination-links\" class=\"pagination\" ng-hide=\"" + iterator + "HidePaginator || " + iterator + "_num_pages <= 1\">\n";
|
||||
html += "<li ng-hide=\"" + iterator + "_page -5 <= 1 \"><a href id=\"first-page-set\" ng-click=\"getPage(1,'" + set + "','" + iterator + "')\">" +
|
||||
"<i class=\"fa fa-angle-double-left\"></i></a></li>\n";
|
||||
|
||||
html += "<li ng-hide=\"" + iterator + "_page -1 <= 0\"><a href " +
|
||||
"id=\"previous-page\" ng-click=\"getPage(" + iterator + "_page - 1,'" + set + "','" + iterator + "')\">" +
|
||||
"<i class=\"fa fa-angle-left\"></i></a></li>\n";
|
||||
|
||||
// html += "<li ng-repeat=\"page in " + iterator + "_page_range\" ng-class=\"pageIsActive(page,'" + iterator + "')\">" +
|
||||
// "<a href id=\"{{ 'link-to-page-' + page }}\" ng-click=\"getPage(page,'" + set + "','" + iterator + "')\">{{ page }}</a></li>\n";
|
||||
html += "<li ng-repeat=\"page in " + iterator + "_page_range\" ng-class=\"pageIsActive(page,'" + iterator + "')\">" +
|
||||
"<a href id=\"{{ 'page-' + page }}\" ng-click=\"getPage(page,'" + set + "','" + iterator + "')\">{{ page }}</a></li>\n";
|
||||
|
||||
html += "<li ng-hide=\"" + iterator + "_page + 1 > " + iterator + "_num_pages\"><a href id=\"next-page\" ng-click=\"" +
|
||||
"getPage(" + iterator + "_page + 1,'" + set + "','" + iterator + "')\"><i class=\"fa fa-angle-right\"></i></a></li>\n";
|
||||
|
||||
html += "<li ng-hide=\"" + iterator + "_page +4 >= " + iterator + "_num_pages\"><a href id=\"last-page-set\" ng-click=\"" +
|
||||
"getPage(" + iterator + "_num_pages,'" + set + "','" + iterator + "')\"><i class=\"fa fa-angle-double-right\"></i></a></li>\n";
|
||||
html += "</ul>\n";
|
||||
html += "</div>\n";
|
||||
html += "<div class=\"col-lg-4 col-md-4\" ng-hide=\"" + iterator + "_mode == 'lookup'\">\n";
|
||||
html += "<div id=\"pagination-labels\" class=\"page-label\">\n";
|
||||
html += "Page <span id=\"current-page\">{{ " + iterator + "_page }}</span> of <span id=\"total-pages\">{{ " + iterator + "_num_pages }}</span> (<span id=\"total-items\">{{ " + iterator + "_total_rows | number:0 }}</span> items)";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
|
||||
return html;
|
||||
};
|
||||
}
|
||||
]);
|
||||
585
awx/ui/static/js/shared/list-generator.js
Normal file
585
awx/ui/static/js/shared/list-generator.js
Normal file
@@ -0,0 +1,585 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:list-generator
|
||||
* @description
|
||||
* #ListGenerator
|
||||
*
|
||||
* Use GenerateList.inject(list_object, { key:value }) to generate HTML from a list object and inject it into the DOM. Returns the $scope of the new list.
|
||||
*
|
||||
* Pass in a list object and a JSON object of key:value parameters. List objects are found in lists/*.js. Possible parameters include:
|
||||
*
|
||||
* | Parameter | Required | Description |
|
||||
* | --------- | -------- | ----------- |
|
||||
* | activityStream | | Used in widgets/stream.js to create the list contained within the activity stream widget. |
|
||||
* | breadCrumbs | | true or false. Set to false, if breadcrumbs should not be included in the generated HTML. |
|
||||
* | hdr | | Deprecated. Was used when list generator created the lookup dialog. This was moved to helpers/Lookup.js. |
|
||||
* | id | | DOM element ID attribute value. Use to inject the list into a custom DOM element. Otherwise, the HTML for a list will be injected into the DOM element with an ID attribute of 'htmlTemplate'. |
|
||||
* | listSize | | Bootstrap size class to apply to the grid column containing the action buttons, which generally appears to the right of the search widget. Defaults to 'col-lg-8 col-md-6 col-sm-4 col-xs-3'. |
|
||||
* | mode | Yes | One of 'edit', 'lookup', 'select', or 'summary'. Generally this will be 'edit'. helpers/Lookup.js uses 'lookup' to generate the lookup dialog. The 'select' option is used in certain controllers when multiple objects are being added to a parent object. For example, building a select list of Users that can be added to an Oranization. 'summary' is no longer used. |
|
||||
* | scope | | If the HTML will be injected into the DOM by list generator, pass in an optional $scope to be used in conjuction with $compile. The list will be associated with the scope value. Otherwise, the scope of the DOM element will be fetched passed to $compile. |
|
||||
* | showSearch | | true or false. Set to false, if the search widget should not be included in the generated HTML. |
|
||||
* | searchSize | | Bootstrap size class (e.g. col-lg-3, col-md-4, col-sm-5, etc.) to apply to the search widget. Defaults to 'col-lg-4 col-md-6 col-sm-8 col-xs-9'. |
|
||||
*
|
||||
* #HTML only
|
||||
*
|
||||
* Use the buildHTML() method to get a string containing the generated HTML for a list object. buldHTML() expects the same parameters as the inject method. For example:
|
||||
* ```
|
||||
* var html = GenerateList.buildHTML({
|
||||
* mode: 'edit',
|
||||
* breadCrumbs: false,
|
||||
* showSearch: false
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* #List Objects
|
||||
*
|
||||
* List objects are found in lists/*.js. Any API endpoint that returns a collection or array is represented with a list object. Examples inlcude Organizations, Credentials, Inventories, etc.
|
||||
* A list can have the following attributes:
|
||||
*
|
||||
* | Attribute | Description |
|
||||
* | --------- | ----------- |
|
||||
* | hover | true or false. If true, 'table-hover' class will be added to the <table> element. |
|
||||
* | index | true or false. If false, the index column, which adds a sequential number to each table row starting with 1, will not be added to the table. |
|
||||
* | iterator | String containing a descriptive name of a single row in the collection - inventory, organization, credential, etc. Used to generate name and ID attributes in the list HTML. |
|
||||
* | name | Name of the collection. Generally matches the endpoint name - inventories, organizations, etc. Will match the $scope variable containing the array of rows comprising the collection. |
|
||||
* | selectTitle | Descriptive title used when mode is 'select'. |
|
||||
* | selectInstructions | Text and HTML used to create popover for help button when mode is 'select'. |
|
||||
* | editTitle | Descriptive title used when mode is 'edit'. |
|
||||
*
|
||||
* ##Fields
|
||||
*
|
||||
* A list contains a fields object. Each column in the list is defined as a separate object within the fields object. A field definition may contain the following attributes:
|
||||
*
|
||||
* | Attribute | Description |
|
||||
* | --------- | ----------- |
|
||||
* | columnClass | String of CSS class names to add to the <td> elemnts of the table column. |
|
||||
* | columnClick | Adds an ng-click directive to the <td> element. |
|
||||
* | excludeModal | true or false. If false, the field will not be included in the generated HTML when the mode is 'lookup' |
|
||||
* | key | true or false. If set to true, helpers/search.js will use the field name as the default sort order when generating the API request. |
|
||||
* | noLink | true or false. If set to true this will override any 'key', 'linkTo', 'ngClick', or other option that would cause the field to be a link. Used in portal mode and custom inv. script. |
|
||||
* | label | Text string used as the column header text. |
|
||||
* | linkTo | Wraps the field value with an <a> element. Set to the value of the href attribute. |
|
||||
* | ngClick | Wraps the field value with an <a> and adds the ng-click directive. Set to the JS expression that ng-click will evaluate. |
|
||||
* | nosort | true or false. Setting to false removes the ability to sort the table by the column. |
|
||||
* | searchable | true or fasel. Set to false if the field should not be included as in option in the search widget. |
|
||||
* | searchOnly | true or false. Set to true if the field should be included in the search widget but not included as a column in the generated HTML <table>. |
|
||||
* | searchOptions | Array of { name: 'Descriptive Name', value: 'api_value' } objects used to generate <options> for the <select> when searchType is 'select'. |
|
||||
* | searchType | One of the available search types defined in helpers/search.js. |
|
||||
* | sourceField | Name of the attribute within summary_fields.<source_model> that the field maps to in the API response object. Used in conjunction with sourceModel. |
|
||||
* | sourceModel | Name of the summary_fields object that the field maps to in the API response object. |
|
||||
*
|
||||
* ##Field Actions
|
||||
*
|
||||
* A list contains a fieldActions object. Each icon found in the Actions column is defined as an object within the feildActions object. fieldActions can have a columnClass attribute,
|
||||
* which may contain a string of CSS class names to add to the action <td> element. It may also contain a label attribute, which can be set to false to suppress the Actions column header.
|
||||
*
|
||||
* Feld action items can have the following attributes:
|
||||
*
|
||||
* | Attribute | Description |
|
||||
* | --------- | ----------- |
|
||||
* | awToolTip | Adds the aw-tool-tip directive. Set to the value of the HTML or text to dislay in the tooltip. |
|
||||
* | 'class' | Set to a string containing any CSS classes to add to the <a> element. |
|
||||
* | dataPlacement | Set to the Bootstrip tooltip placement - right, left, top, bottom, etc. |
|
||||
* | dataTipWatch | Set to the $scope variable that contains the text and HTML to display in the tooltip. A $scope.$watch will be added to the variable so that anytime its value changes the tooltip will change. |
|
||||
* | iconClass | By default the CSS icon class is set by the SelectIcon() method in lib/ansible/generator-helpers.js. The icon is based on the action name. Use iconClass to override the default value. |
|
||||
* | mode | One of 'all' or 'edit'. Will generally be 'all'. Note that field actions are not displayed when the list is in 'lookup' mode. |
|
||||
* | ngClass | Adds the ng-class directive. Set to the JS expressino that ng-class will evaluate. |
|
||||
* | ngShow | Adds the ng-show directive. Set to the JS expression that ng-show will evaluate. |
|
||||
*
|
||||
* ##Actions
|
||||
*
|
||||
* A list can contain an actions object. The actions object contains an object for each action button displayed in the top-right corner of the list container. An action can have the same
|
||||
* attributes as an action defined in fieldAction. Both are actions. Clicking on an action evaluates the JS found in the ngClick attribute. In both cases icon is generated automatically by the SelectIcon() method in lib/ansible/generator-helpers.js.
|
||||
* The real difference is that an <a> element is used to generate fieldAction items while a <button> element is used for action items.
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ListGenerator', ['GeneratorHelpers'])
|
||||
.factory('GenerateList', ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon',
|
||||
'Column', 'DropDown', 'NavigationLink', 'Button', 'SelectIcon', 'Breadcrumbs',
|
||||
function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, Column, DropDown, NavigationLink,
|
||||
Button, SelectIcon, Breadcrumbs) {
|
||||
return {
|
||||
|
||||
setList: function (list) {
|
||||
this.list = list;
|
||||
},
|
||||
|
||||
setOptions: function(options) {
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
attr: Attr,
|
||||
|
||||
icon: Icon,
|
||||
|
||||
has: function (key) {
|
||||
return (this.form[key] && this.form[key] !== null && this.form[key] !== undefined) ? true : false;
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
$('#lookup-modal').modal('hide');
|
||||
},
|
||||
|
||||
button: Button,
|
||||
|
||||
buildHTML: function(list, options) {
|
||||
this.setList(list);
|
||||
return this.build(options);
|
||||
},
|
||||
|
||||
inject: function (list, options) {
|
||||
// options.mode = one of edit, select or lookup
|
||||
//
|
||||
// Modes edit and select will inject the list as html into element #htmlTemplate.
|
||||
// 'lookup' mode injects the list html into #lookup-modal-body.
|
||||
//
|
||||
// For options.mode == 'lookup', include the following:
|
||||
//
|
||||
// hdr: <lookup dialog header>
|
||||
//
|
||||
// Inject into a custom element using options.id: <element id attribute value>
|
||||
// Control breadcrumb creation with options.breadCrumbs: <true | false>
|
||||
//
|
||||
var element;
|
||||
|
||||
if (options.id) {
|
||||
element = angular.element(document.getElementById(options.id));
|
||||
} else {
|
||||
element = angular.element(document.getElementById('htmlTemplate'));
|
||||
}
|
||||
|
||||
this.setOptions(options);
|
||||
this.setList(list);
|
||||
element.html(this.build(options)); // Inject the html
|
||||
|
||||
if (options.prepend) {
|
||||
element.prepend(options.prepend);
|
||||
}
|
||||
|
||||
if (options.append) {
|
||||
element.append(options.append);
|
||||
}
|
||||
|
||||
if (options.scope) {
|
||||
this.scope = options.scope;
|
||||
} else {
|
||||
this.scope = element.scope();
|
||||
}
|
||||
|
||||
$compile(element)(this.scope);
|
||||
|
||||
// Reset the scope to prevent displaying old data from our last visit to this list
|
||||
//this.scope[list.name] = null;
|
||||
this.scope[list.iterator] = [];
|
||||
|
||||
// Remove any lingering tooltip and popover <div> elements
|
||||
$('.tooltip').each(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$('.popover').each(function() {
|
||||
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
try {
|
||||
$('#help-modal').empty().dialog('destroy');
|
||||
} catch (e) {
|
||||
//ignore any errors should the dialog not be initialized
|
||||
}
|
||||
|
||||
/*if (options.mode === 'lookup') {
|
||||
// options should include {hdr: <dialog header>, action: <function...> }
|
||||
this.scope.formModalActionDisabled = false;
|
||||
this.scope.lookupHeader = options.hdr;
|
||||
$('#lookup-modal').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: true
|
||||
});
|
||||
$('#lookup-modal').unbind('hidden.bs.modal');
|
||||
$(document).bind('keydown', function (e) {
|
||||
if (e.keyCode === 27) {
|
||||
$('#lookup-modal').modal('hide');
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
return this.scope;
|
||||
},
|
||||
|
||||
build: function (options) {
|
||||
//
|
||||
// Generate HTML. Do NOT call this function directly. Called by inject(). Returns an HTML
|
||||
// string to be injected into the current view.
|
||||
//
|
||||
var html = '',
|
||||
list = this.list,
|
||||
base, size, action, btn, fld, cnt, field_action, fAction, itm;
|
||||
|
||||
if (options.activityStream) {
|
||||
// Breadcrumbs for activity stream widget
|
||||
// Make the links clickable using ng-click function so we can first remove the stream widget
|
||||
// before navigation
|
||||
html += "<div>\n";
|
||||
html += "<ul class=\"ansible-breadcrumb\">\n";
|
||||
html += "<li ng-repeat=\"crumb in breadcrumbs\"><a href=\"\" " + "ng-click=\"closeStream(crumb.path)\">" +
|
||||
"{{ crumb.title }}</a></li>\n";
|
||||
html += "<li class=\"active\"><a href=\"\">";
|
||||
html += list.editTitle;
|
||||
html += "</a></li>\n</ul>\n</div>\n";
|
||||
}
|
||||
//else if (options.mode !== 'lookup' && (options.breadCrumbs === undefined || options.breadCrumbs)) {
|
||||
else if (options.breadCrumbs) {
|
||||
html += Breadcrumbs({
|
||||
list: list,
|
||||
mode: options.mode
|
||||
});
|
||||
}
|
||||
|
||||
if (options.mode === 'edit' && list.editInstructions) {
|
||||
html += "<div class=\"alert alert-info alert-block\">\n";
|
||||
html += "<button type=\"button\" class=\"close\" data-dismiss=\"alert\">×</button>\n";
|
||||
html += "<strong>Hint: </strong>" + list.editInstructions + "\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
if (options.instructions) {
|
||||
html += "<div class=\"instructions alert alert-info\">" + options.instructions + "</div>\n";
|
||||
}
|
||||
else if (list.instructions) {
|
||||
html += "<div class=\"instructions alert alert-info\">" + list.instructions + "</div>\n";
|
||||
}
|
||||
|
||||
if (options.mode !== 'lookup' && (list.well === undefined || list.well)) {
|
||||
html += "<div class=\"list-well\">\n";
|
||||
}
|
||||
|
||||
if (options.activityStream) {
|
||||
// Add a title row
|
||||
html += "<div class=\"row\">\n";
|
||||
html += "<div class=\"col-lg-12\">\n";
|
||||
html += "<h5>{{ streamTitle }}</h5>\n";
|
||||
html += "</div>\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
if (options.showSearch=== undefined || options.showSearch === true) {
|
||||
html += "<div class=\"row\">\n";
|
||||
if (options.searchSize) {
|
||||
html += SearchWidget({
|
||||
iterator: list.iterator,
|
||||
template: list,
|
||||
mini: true,
|
||||
size: options.searchSize,
|
||||
searchWidgets: list.searchWidgets
|
||||
});
|
||||
} else if (options.mode === 'summary') {
|
||||
html += SearchWidget({
|
||||
iterator: list.iterator,
|
||||
template: list,
|
||||
mini: true,
|
||||
size: 'col-lg-6'
|
||||
});
|
||||
} else if (options.mode === 'lookup' || options.id !== undefined) {
|
||||
html += SearchWidget({
|
||||
iterator: list.iterator,
|
||||
template: list,
|
||||
mini: true,
|
||||
size: 'col-lg-8'
|
||||
});
|
||||
} else {
|
||||
html += SearchWidget({
|
||||
iterator: list.iterator,
|
||||
template: list,
|
||||
mini: true
|
||||
});
|
||||
}
|
||||
|
||||
if (options.mode !== 'lookup') {
|
||||
//actions
|
||||
base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
html += "<div class=\"";
|
||||
if (options.searchSize && !options.listSize) {
|
||||
// User supplied searchSize, calc the remaining
|
||||
size = parseInt(options.searchSize.replace(/([A-Z]|[a-z]|\-)/g, ''));
|
||||
size = (list.searchWidgets) ? list.searchWidgets * size : size;
|
||||
html += 'col-lg-' + (12 - size);
|
||||
} else if (options.listSize) {
|
||||
html += options.listSize;
|
||||
} else if (options.mode === 'summary') {
|
||||
html += 'col-lg-6';
|
||||
} else if (options.id !== undefined) {
|
||||
html += "col-lg-4";
|
||||
} else {
|
||||
html += "col-lg-8 col-md-6 col-sm-4 col-xs-3";
|
||||
}
|
||||
html += "\">\n";
|
||||
|
||||
html += "<div class=\"list-actions\">\n";
|
||||
|
||||
// Add toolbar buttons or 'actions'
|
||||
for (action in list.actions) {
|
||||
if (list.actions[action].mode === 'all' || list.actions[action].mode === options.mode) {
|
||||
if ((list.actions[action].basePaths === undefined) ||
|
||||
(list.actions[action].basePaths && list.actions[action].basePaths.indexOf(base) > -1)) {
|
||||
html += this.button({
|
||||
btn: list.actions[action],
|
||||
action: action,
|
||||
toolbar: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//select instructions
|
||||
if (options.mode === 'select' && list.selectInstructions) {
|
||||
btn = {
|
||||
awPopOver: list.selectInstructions,
|
||||
dataPlacement: 'left',
|
||||
dataContainer: 'body',
|
||||
'class': 'btn-xs btn-help',
|
||||
awToolTip: 'Click for help',
|
||||
dataTitle: 'Help',
|
||||
iconSize: 'fa-lg'
|
||||
};
|
||||
//html += this.button(btn, 'select');
|
||||
html += this.button({
|
||||
btn: btn,
|
||||
action: 'help',
|
||||
toolbar: true
|
||||
});
|
||||
}
|
||||
|
||||
html += "</div><!-- list-acitons -->\n";
|
||||
html += "</div><!-- list-actions-column -->\n";
|
||||
} else {
|
||||
//lookup
|
||||
html += "<div class=\"col-lg-7\"></div>\n";
|
||||
}
|
||||
html += "</div><!-- row -->\n";
|
||||
}
|
||||
|
||||
// Add a title and optionally a close button (used on Inventory->Groups)
|
||||
if (options.mode !== 'lookup' && list.showTitle) {
|
||||
html += "<div class=\"form-title\">";
|
||||
html += (options.mode === 'edit' || options.mode === 'summary') ? list.editTitle : list.addTitle;
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
// table header row
|
||||
html += "<div class=\"list-table-container\"";
|
||||
html += (list.awCustomScroll) ? " aw-custom-scroll " : "";
|
||||
html += ">\n";
|
||||
html += "<table id=\"" + list.name + "_table\" ";
|
||||
html += "class=\"table table-condensed";
|
||||
html += (list['class']) ? " " + list['class'] : "";
|
||||
html += (options.mode !== 'summary' && options.mode !== 'edit' && (options.mode === 'lookup' || options.id)) ?
|
||||
' table-hover-inverse' : '';
|
||||
html += (list.hover) ? ' table-hover' : '';
|
||||
html += (options.mode === 'summary') ? ' table-summary' : '';
|
||||
html += "\" ";
|
||||
html += ">\n";
|
||||
|
||||
if (!options.skipTableHead) {
|
||||
html += this.buildHeader(options);
|
||||
}
|
||||
|
||||
// table body
|
||||
html += "<tbody>\n";
|
||||
html += "<tr ng-class=\"" + list.iterator;
|
||||
html += (options.mode === 'lookup' || options.mode === 'select') ? ".success_class" : ".active_class";
|
||||
html += "\" ";
|
||||
html += "id=\"{{ " + list.iterator + ".id }}\" ";
|
||||
html += "class=\"" + list.iterator + "_class\" ";
|
||||
html += "ng-repeat=\"" + list.iterator + " in " + list.name;
|
||||
html += (list.orderBy) ? " | orderBy:'" + list.orderBy + "'" : "";
|
||||
html += (list.filterBy) ? " | filter: " + list.filterBy : "";
|
||||
html += "\">\n";
|
||||
if (list.index) {
|
||||
html += "<td class=\"index-column hidden-xs\">{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.</td>\n";
|
||||
}
|
||||
cnt = 2;
|
||||
base = (list.base) ? list.base : list.name;
|
||||
base = base.replace(/^\//, '');
|
||||
for (fld in list.fields) {
|
||||
cnt++;
|
||||
if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) &&
|
||||
!(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) {
|
||||
html += Column({
|
||||
list: list,
|
||||
fld: fld,
|
||||
options: options,
|
||||
base: base
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.mode === 'select' || options.mode === 'lookup') {
|
||||
if(options.input_type==="radio"){ //added by JT so that lookup forms can be either radio inputs or check box inputs
|
||||
html += "<td><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
|
||||
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-value=\"1\" " +
|
||||
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
|
||||
}
|
||||
else { // its assumed that options.input_type = checkbox
|
||||
html += "<td><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
|
||||
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
|
||||
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
|
||||
}
|
||||
} else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) {
|
||||
|
||||
// Row level actions
|
||||
|
||||
html += "<td class=\"actions\">";
|
||||
|
||||
for (field_action in list.fieldActions) {
|
||||
if (field_action !== 'columnClass') {
|
||||
if (list.fieldActions[field_action].type && list.fieldActions[field_action].type === 'DropDown') {
|
||||
html += DropDown({
|
||||
list: list,
|
||||
fld: field_action,
|
||||
options: options,
|
||||
base: base,
|
||||
type: 'fieldActions',
|
||||
td: false
|
||||
});
|
||||
} else {
|
||||
fAction = list.fieldActions[field_action];
|
||||
html += "<a id=\"";
|
||||
html += (fAction.id) ? fAction.id : field_action + "-action";
|
||||
html += "\" ";
|
||||
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
|
||||
html += (fAction.ngHref) ? "ng-href=\"" + fAction.ngHref + "\" " : "";
|
||||
html += (field_action === 'cancel') ? "class=\"cancel red-txt\" " : "";
|
||||
html += (fAction.awPopOver) ? "aw-pop-over=\"" + fAction.awPopOver + "\" " : "";
|
||||
html += (fAction.dataPlacement) ? Attr(fAction, 'dataPlacement') : "";
|
||||
html += (fAction.dataTitle) ? Attr(fAction, 'dataTitle') : "";
|
||||
for (itm in fAction) {
|
||||
if (itm !== 'ngHref' && itm !== 'href' && itm !== 'label' && itm !== 'icon' && itm !== 'class' &&
|
||||
itm !== 'iconClass' && itm !== "dataPlacement" && itm !== "awPopOver" &&
|
||||
itm !== "dataTitle") {
|
||||
html += Attr(fAction, itm);
|
||||
}
|
||||
}
|
||||
html += ">";
|
||||
if (fAction.iconClass) {
|
||||
html += "<i class=\"" + fAction.iconClass + "\"></i>";
|
||||
} else {
|
||||
html += SelectIcon({
|
||||
action: field_action
|
||||
});
|
||||
}
|
||||
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + list.fieldActions[field_action].label +
|
||||
// "</span>" : "";
|
||||
html += "</a>";
|
||||
}
|
||||
}
|
||||
}
|
||||
html += "</td>\n";
|
||||
}
|
||||
html += "</tr>\n";
|
||||
|
||||
// Message for when a collection is empty
|
||||
html += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == false && " + list.name + ".length == 0\">\n";
|
||||
html += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">No records matched your search.</div></td>\n";
|
||||
html += "</tr>\n";
|
||||
|
||||
// Message for loading
|
||||
html += "<tr class=\"loading-info\" ng-show=\"" + list.iterator + "Loading == true\">\n";
|
||||
html += "<td colspan=\"" + cnt + "\"><div class=\"loading-info\">Loading...</div></td>\n";
|
||||
html += "</tr>\n";
|
||||
|
||||
// End List
|
||||
html += "</tbody>\n";
|
||||
html += "</table>\n";
|
||||
html += "</div><!-- table container -->\n";
|
||||
|
||||
if (options.mode === 'select' && (options.selectButton === undefined || options.selectButton)) {
|
||||
html += "<div class=\"navigation-buttons\">\n";
|
||||
html += " <button class=\"btn btn-sm btn-primary pull-right\" id=\"select_btn\" aw-tool-tip=\"Complete your selection\" " +
|
||||
"ng-click=\"finishSelection()\" ng-disabled=\"disableSelectBtn\"><i class=\"fa fa-check\"></i> Select</button>\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
if (options.mode !== 'lookup' && (list.well === undefined || list.well === true)) {
|
||||
html += "</div>\n"; //well
|
||||
}
|
||||
|
||||
if (options.mode === 'lookup' || (options.id && options.id === "form-modal-body")) {
|
||||
html += PaginateWidget({
|
||||
set: list.name,
|
||||
iterator: list.iterator
|
||||
});
|
||||
} else {
|
||||
html += PaginateWidget({
|
||||
set: list.name,
|
||||
iterator: list.iterator
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
buildHeader: function(options) {
|
||||
var list = this.list,
|
||||
fld, html;
|
||||
|
||||
if (options === undefined) {
|
||||
options = this.options;
|
||||
}
|
||||
|
||||
html = "<thead>\n";
|
||||
html += "<tr>\n";
|
||||
if (list.index) {
|
||||
html += "<th class=\"col-lg-1 col-md-1 col-sm-2 hidden-xs\">#</th>\n";
|
||||
}
|
||||
for (fld in list.fields) {
|
||||
if ((list.fields[fld].searchOnly === undefined || list.fields[fld].searchOnly === false) &&
|
||||
!(options.mode === 'lookup' && list.fields[fld].excludeModal === true)) {
|
||||
html += "<th class=\"list-header";
|
||||
if (options.mode === 'lookup' && list.fields[fld].modalColumnClass) {
|
||||
html += " " + list.fields[fld].modalColumnClass;
|
||||
}
|
||||
else if (list.fields[fld].columnClass) {
|
||||
html += " " + list.fields[fld].columnClass;
|
||||
}
|
||||
html += "\" id=\"" + list.iterator + "-" + fld + "-header\"";
|
||||
html += (list.fields[fld].columnShow) ? " ng-show=\"" + list.fields[fld].columnShow + "\" " : "";
|
||||
html += (list.fields[fld].nosort === undefined || list.fields[fld].nosort !== true) ? " ng-click=\"sort('" + list.iterator + "','" + fld + "')\"" : "";
|
||||
html += ">";
|
||||
html += list.fields[fld].label;
|
||||
if (list.fields[fld].nosort === undefined || list.fields[fld].nosort !== true) {
|
||||
html += " <i class=\"fa ";
|
||||
if (list.fields[fld].key) {
|
||||
if (list.fields[fld].desc) {
|
||||
html += "fa-sort-down";
|
||||
} else {
|
||||
html += "fa-sort-up";
|
||||
}
|
||||
} else {
|
||||
html += "fa-sort";
|
||||
}
|
||||
html += "\"></i></a>";
|
||||
}
|
||||
html += "</th>\n";
|
||||
}
|
||||
}
|
||||
if (options.mode === 'select' || options.mode === 'lookup') {
|
||||
html += "<th class=\"col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
|
||||
} else if (options.mode === 'edit' && list.fieldActions) {
|
||||
html += "<th class=\"actions-column";
|
||||
html += (list.fieldActions && list.fieldActions.columnClass) ? " " + list.fieldActions.columnClass : "";
|
||||
html += "\">";
|
||||
html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : "";
|
||||
html += "</th>\n";
|
||||
}
|
||||
html += "</tr>\n";
|
||||
html += "</thead>\n";
|
||||
return html;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
126
awx/ui/static/js/shared/prompt-dialog.js
Normal file
126
awx/ui/static/js/shared/prompt-dialog.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/*********************************************
|
||||
* Copyright (c) 2014 AnsibleWorks, Inc.
|
||||
*/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:prompt-dialog
|
||||
* @description
|
||||
* PromptDialog
|
||||
* Prompt the user with a Yes/No dialog to confirm an action such
|
||||
* as Delete. Assumes a hidden dialog already exists in $scope.
|
||||
* See example at bottom. If user responds with Yes, execute action
|
||||
* parameter.
|
||||
*
|
||||
* params: { hdr: 'header msg',
|
||||
* body: 'body text/html',
|
||||
* class: 'btn-class for Yes button', --defaults to btn-danger
|
||||
* action: function() {} --action to take, if use clicks Yes
|
||||
* }
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name lib.ansible.function:prompt-dialog#PromptDialog
|
||||
* @methodOf lib.ansible.function:prompt-dialog
|
||||
* @description discuss difference b/t this and other modals
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('PromptDialog', ['Utilities'])
|
||||
.factory('Prompt', ['$sce',
|
||||
function ($sce) {
|
||||
return function (params) {
|
||||
|
||||
var dialog = angular.element(document.getElementById('prompt-modal')),
|
||||
scope = dialog.scope(), cls, local_backdrop;
|
||||
|
||||
scope.promptHeader = params.hdr;
|
||||
scope.promptBody = $sce.trustAsHtml(params.body);
|
||||
scope.promptAction = params.action;
|
||||
|
||||
local_backdrop = (params.backdrop === undefined) ? "static" : params.backdrop;
|
||||
|
||||
cls = (params['class'] === null || params['class'] === undefined) ? 'btn-danger' : params['class'];
|
||||
|
||||
$('#prompt_action_btn').removeClass(cls).addClass(cls);
|
||||
|
||||
// bootstrap modal's have an open defect with disallowing tab index's of the background of the modal
|
||||
// This will keep the tab indexing on the modal's focus. This is to fix an issue with tabbing working when
|
||||
// the user is attempting to delete something. Might need to be checked for other occurances of the bootstrap
|
||||
// modal other than deleting
|
||||
function disableTabModalShown() {
|
||||
|
||||
$('.modal').on('shown.bs.modal', function() {
|
||||
|
||||
var modal = $(this),
|
||||
focusableChildren = modal.find('a[href], a[data-dismiss], area[href], input, select, textarea, button, iframe, object, embed, *[tabindex], *[contenteditable]'),
|
||||
numElements = focusableChildren.length,
|
||||
currentIndex = 0,
|
||||
focus,
|
||||
focusPrevious,
|
||||
focusNext;
|
||||
|
||||
$(document.activeElement).blur();
|
||||
|
||||
focus = function() {
|
||||
var focusableElement = focusableChildren[currentIndex];
|
||||
if (focusableElement) {
|
||||
focusableElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
focusPrevious = function () {
|
||||
currentIndex--;
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = numElements - 1;
|
||||
}
|
||||
|
||||
focus();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
focusNext = function () {
|
||||
currentIndex++;
|
||||
if (currentIndex >= numElements) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
focus();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$(document).on('keydown', function (e) {
|
||||
|
||||
if (e.keyCode === 9 && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
focusPrevious();
|
||||
}
|
||||
else if (e.keyCode === 9) {
|
||||
e.preventDefault();
|
||||
focusNext();
|
||||
}
|
||||
});
|
||||
|
||||
$(this).focus();
|
||||
});
|
||||
|
||||
$('.modal').on('hidden.bs.modal', function() {
|
||||
$(document).unbind('keydown');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('#prompt-modal').off('hidden.bs.modal');
|
||||
$('#prompt-modal').modal({
|
||||
backdrop: 'local_backdrop',
|
||||
keyboard: true,
|
||||
show: true
|
||||
});
|
||||
disableTabModalShown();
|
||||
|
||||
};
|
||||
}
|
||||
]);
|
||||
327
awx/ui/static/js/shared/pwdmeter.js
Normal file
327
awx/ui/static/js/shared/pwdmeter.js
Normal file
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
** Created by: Jeff Todnem (http://www.todnem.com/)
|
||||
** Created on: 2007-08-14
|
||||
** Modified: 2013-07-31 by James Cammarata
|
||||
**
|
||||
** License Information:
|
||||
** -------------------------------------------------------------------------
|
||||
** Copyright (C) 2007 Jeff Todnem
|
||||
**
|
||||
** This program is free software; you can redistribute it and/or modify it
|
||||
** under the terms of the GNU General Public License as published by the
|
||||
** Free Software Foundation; either version 2 of the License, or (at your
|
||||
** option) any later version.
|
||||
**
|
||||
** This program is distributed in the hope that it will be useful, but
|
||||
** WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
** General Public License for more details.
|
||||
**
|
||||
** You should have received a copy of the GNU General Public License along
|
||||
** with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
** 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
**
|
||||
**/
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name lib.ansible.function:pwdmeter
|
||||
* @description
|
||||
** CLH 09/05/13 - Set required strength in config.js
|
||||
** 02/10/14 - Applied jsHint
|
||||
*/
|
||||
|
||||
/*jshint eqeqeq:false, unused:false */
|
||||
|
||||
String.prototype.strReverse = function () {
|
||||
var newstring = "", s;
|
||||
for (s = 0; s < this.length; s++) {
|
||||
newstring = this.charAt(s) + newstring;
|
||||
}
|
||||
return newstring;
|
||||
};
|
||||
|
||||
var nScore = 0;
|
||||
|
||||
export function chkPass(pwd) {
|
||||
// Simultaneous variable declaration and value assignment aren't supported in IE apparently
|
||||
// so I'm forced to assign the same value individually per var to support a crappy browser *sigh*
|
||||
var nLength = 0,
|
||||
nAlphaUC = 0,
|
||||
nAlphaLC = 0,
|
||||
nNumber = 0,
|
||||
nSymbol = 0,
|
||||
nMidChar = 0,
|
||||
nRequirements = 0,
|
||||
nAlphasOnly = 0,
|
||||
nNumbersOnly = 0,
|
||||
nUnqChar = 0,
|
||||
nRepChar = 0,
|
||||
nRepInc = 0,
|
||||
nConsecAlphaUC = 0,
|
||||
nConsecAlphaLC = 0,
|
||||
nConsecNumber = 0,
|
||||
nConsecSymbol = 0,
|
||||
nConsecCharType = 0,
|
||||
nSeqAlpha = 0,
|
||||
nSeqNumber = 0,
|
||||
nSeqSymbol = 0,
|
||||
nSeqChar = 0,
|
||||
nReqChar = 0,
|
||||
nMultConsecCharType = 0,
|
||||
nMultRepChar = 1,
|
||||
nMultConsecSymbol = 1,
|
||||
nMultMidChar = 2,
|
||||
nMultRequirements = 2,
|
||||
nMultConsecAlphaUC = 2,
|
||||
nMultConsecAlphaLC = 2,
|
||||
nMultConsecNumber = 2,
|
||||
nReqCharType = 3,
|
||||
nMultAlphaUC = 3,
|
||||
nMultAlphaLC = 3,
|
||||
nMultSeqAlpha = 3,
|
||||
nMultSeqNumber = 3,
|
||||
nMultSeqSymbol = 3,
|
||||
nMultLength = 4,
|
||||
nMultNumber = 4,
|
||||
nMultSymbol = 6,
|
||||
nTmpAlphaUC = "",
|
||||
nTmpAlphaLC = "",
|
||||
nTmpNumber = "",
|
||||
nTmpSymbol = "",
|
||||
sAlphaUC = "0",
|
||||
sAlphaLC = "0",
|
||||
sNumber = "0",
|
||||
sSymbol = "0",
|
||||
sMidChar = "0",
|
||||
sRequirements = "0",
|
||||
sAlphasOnly = "0",
|
||||
sNumbersOnly = "0",
|
||||
sRepChar = "0",
|
||||
sConsecAlphaUC = "0",
|
||||
sConsecAlphaLC = "0",
|
||||
sConsecNumber = "0",
|
||||
sSeqAlpha = "0",
|
||||
sSeqNumber = "0",
|
||||
sSeqSymbol = "0",
|
||||
sAlphas = "abcdefghijklmnopqrstuvwxyz",
|
||||
sNumerics = "01234567890",
|
||||
sSymbols = ")_!@#$%^&*()",
|
||||
sComplexity = "Too Short",
|
||||
sStandards = "Below",
|
||||
nMinPwdLen = 8,
|
||||
a, nd, arrPwd, arrPwdLen,bCharExists,b,s,sFwd,sRev,progbar,required_strength,warning_level;
|
||||
|
||||
if (document.all) {
|
||||
nd = 0;
|
||||
} else {
|
||||
nd = 1;
|
||||
}
|
||||
if (pwd) {
|
||||
nScore = parseInt(pwd.length * nMultLength);
|
||||
nLength = pwd.length;
|
||||
arrPwd = pwd.replace(/\s+/g, "").split(/\s*/);
|
||||
arrPwdLen = arrPwd.length;
|
||||
|
||||
/* Loop through password to check for Symbol, Numeric, Lowercase and Uppercase pattern matches */
|
||||
for (a = 0; a < arrPwdLen; a++) {
|
||||
if (arrPwd[a].match(/[A-Z]/g)) {
|
||||
if (nTmpAlphaUC !== "") {
|
||||
if ((nTmpAlphaUC + 1) == a) {
|
||||
nConsecAlphaUC++;
|
||||
nConsecCharType++;
|
||||
}
|
||||
}
|
||||
nTmpAlphaUC = a;
|
||||
nAlphaUC++;
|
||||
} else if (arrPwd[a].match(/[a-z]/g)) {
|
||||
if (nTmpAlphaLC !== "") {
|
||||
if ((nTmpAlphaLC + 1) == a) {
|
||||
nConsecAlphaLC++;
|
||||
nConsecCharType++;
|
||||
}
|
||||
}
|
||||
nTmpAlphaLC = a;
|
||||
nAlphaLC++;
|
||||
} else if (arrPwd[a].match(/[0-9]/g)) {
|
||||
if (a > 0 && a < (arrPwdLen - 1)) {
|
||||
nMidChar++;
|
||||
}
|
||||
if (nTmpNumber !== "") {
|
||||
if ((nTmpNumber + 1) == a) {
|
||||
nConsecNumber++;
|
||||
nConsecCharType++;
|
||||
}
|
||||
}
|
||||
nTmpNumber = a;
|
||||
nNumber++;
|
||||
} else if (arrPwd[a].match(/[^a-zA-Z0-9_]/g)) {
|
||||
if (a > 0 && a < (arrPwdLen - 1)) {
|
||||
nMidChar++;
|
||||
}
|
||||
if (nTmpSymbol !== "") {
|
||||
if ((nTmpSymbol + 1) == a) {
|
||||
nConsecSymbol++;
|
||||
nConsecCharType++;
|
||||
}
|
||||
}
|
||||
nTmpSymbol = a;
|
||||
nSymbol++;
|
||||
}
|
||||
|
||||
/* Internal loop through password to check for repeat characters */
|
||||
bCharExists = false;
|
||||
for (b = 0; b < arrPwdLen; b++) {
|
||||
if (arrPwd[a] == arrPwd[b] && a != b) { /* repeat character exists */
|
||||
bCharExists = true;
|
||||
/*
|
||||
Calculate icrement deduction based on proximity to identical characters
|
||||
Deduction is incremented each time a new match is discovered
|
||||
Deduction amount is based on total password length divided by the
|
||||
difference of distance between currently selected match
|
||||
*/
|
||||
nRepInc += Math.abs(arrPwdLen / (b - a));
|
||||
}
|
||||
}
|
||||
if (bCharExists) {
|
||||
nRepChar++;
|
||||
nUnqChar = arrPwdLen - nRepChar;
|
||||
nRepInc = (nUnqChar) ? Math.ceil(nRepInc / nUnqChar) : Math.ceil(nRepInc);
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for sequential alpha string patterns (forward and reverse) */
|
||||
for (s = 0; s < 23; s++) {
|
||||
sFwd = sAlphas.substring(s, parseInt(s + 3));
|
||||
sRev = sFwd.strReverse();
|
||||
if (pwd.toLowerCase().indexOf(sFwd) != -1 || pwd.toLowerCase().indexOf(sRev) != -1) {
|
||||
nSeqAlpha++;
|
||||
nSeqChar++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for sequential numeric string patterns (forward and reverse) */
|
||||
for (s = 0; s < 8; s++) {
|
||||
sFwd = sNumerics.substring(s, parseInt(s + 3));
|
||||
sRev = sFwd.strReverse();
|
||||
if (pwd.toLowerCase().indexOf(sFwd) != -1 || pwd.toLowerCase().indexOf(sRev) != -1) {
|
||||
nSeqNumber++;
|
||||
nSeqChar++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for sequential symbol string patterns (forward and reverse) */
|
||||
for (s = 0; s < 8; s++) {
|
||||
sFwd = sSymbols.substring(s, parseInt(s + 3));
|
||||
sRev = sFwd.strReverse();
|
||||
if (pwd.toLowerCase().indexOf(sFwd) != -1 || pwd.toLowerCase().indexOf(sRev) != -1) {
|
||||
nSeqSymbol++;
|
||||
nSeqChar++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modify overall score value based on usage vs requirements */
|
||||
|
||||
/* General point assignment */
|
||||
if (nAlphaUC > 0 && nAlphaUC < nLength) {
|
||||
nScore = parseInt(nScore + ((nLength - nAlphaUC) * 2));
|
||||
sAlphaUC = "+ " + parseInt((nLength - nAlphaUC) * 2);
|
||||
}
|
||||
if (nAlphaLC > 0 && nAlphaLC < nLength) {
|
||||
nScore = parseInt(nScore + ((nLength - nAlphaLC) * 2));
|
||||
sAlphaLC = "+ " + parseInt((nLength - nAlphaLC) * 2);
|
||||
}
|
||||
if (nNumber > 0 && nNumber < nLength) {
|
||||
nScore = parseInt(nScore + (nNumber * nMultNumber));
|
||||
sNumber = "+ " + parseInt(nNumber * nMultNumber);
|
||||
}
|
||||
if (nSymbol > 0) {
|
||||
nScore = parseInt(nScore + (nSymbol * nMultSymbol));
|
||||
sSymbol = "+ " + parseInt(nSymbol * nMultSymbol);
|
||||
}
|
||||
if (nMidChar > 0) {
|
||||
nScore = parseInt(nScore + (nMidChar * nMultMidChar));
|
||||
sMidChar = "+ " + parseInt(nMidChar * nMultMidChar);
|
||||
}
|
||||
|
||||
/* Point deductions for poor practices */
|
||||
if ((nAlphaLC > 0 || nAlphaUC > 0) && nSymbol === 0 && nNumber === 0) { // Only Letters
|
||||
nScore = parseInt(nScore - nLength);
|
||||
nAlphasOnly = nLength;
|
||||
sAlphasOnly = "- " + nLength;
|
||||
}
|
||||
if (nAlphaLC === 0 && nAlphaUC === 0 && nSymbol === 0 && nNumber > 0) { // Only Numbers
|
||||
nScore = parseInt(nScore - nLength);
|
||||
nNumbersOnly = nLength;
|
||||
sNumbersOnly = "- " + nLength;
|
||||
}
|
||||
if (nRepChar > 0) { // Same character exists more than once
|
||||
nScore = parseInt(nScore - nRepInc);
|
||||
sRepChar = "- " + nRepInc;
|
||||
}
|
||||
if (nConsecAlphaUC > 0) { // Consecutive Uppercase Letters exist
|
||||
nScore = parseInt(nScore - (nConsecAlphaUC * nMultConsecAlphaUC));
|
||||
sConsecAlphaUC = "- " + parseInt(nConsecAlphaUC * nMultConsecAlphaUC);
|
||||
}
|
||||
if (nConsecAlphaLC > 0) { // Consecutive Lowercase Letters exist
|
||||
nScore = parseInt(nScore - (nConsecAlphaLC * nMultConsecAlphaLC));
|
||||
sConsecAlphaLC = "- " + parseInt(nConsecAlphaLC * nMultConsecAlphaLC);
|
||||
}
|
||||
if (nConsecNumber > 0) { // Consecutive Numbers exist
|
||||
nScore = parseInt(nScore - (nConsecNumber * nMultConsecNumber));
|
||||
sConsecNumber = "- " + parseInt(nConsecNumber * nMultConsecNumber);
|
||||
}
|
||||
if (nSeqAlpha > 0) { // Sequential alpha strings exist (3 characters or more)
|
||||
nScore = parseInt(nScore - (nSeqAlpha * nMultSeqAlpha));
|
||||
sSeqAlpha = "- " + parseInt(nSeqAlpha * nMultSeqAlpha);
|
||||
}
|
||||
if (nSeqNumber > 0) { // Sequential numeric strings exist (3 characters or more)
|
||||
nScore = parseInt(nScore - (nSeqNumber * nMultSeqNumber));
|
||||
sSeqNumber = "- " + parseInt(nSeqNumber * nMultSeqNumber);
|
||||
}
|
||||
if (nSeqSymbol > 0) { // Sequential symbol strings exist (3 characters or more)
|
||||
nScore = parseInt(nScore - (nSeqSymbol * nMultSeqSymbol));
|
||||
sSeqSymbol = "- " + parseInt(nSeqSymbol * nMultSeqSymbol);
|
||||
}
|
||||
|
||||
progbar = $("#progbar");
|
||||
required_strength = $AnsibleConfig.password_strength;
|
||||
warning_level = ($AnsibleConfig.password_strength - 15 < 0) ? 0 : $AnsibleConfig.password_strength - 15;
|
||||
|
||||
/* Enforce a minimum length of 8 */
|
||||
if (nLength < 8) {
|
||||
nScore = 0;
|
||||
}
|
||||
|
||||
/* Determine complexity based on overall score */
|
||||
if (nScore > 100) {
|
||||
nScore = 100;
|
||||
} else if (nScore < 0) {
|
||||
nScore = 0;
|
||||
} else if (nScore >= required_strength) {
|
||||
nScore = 100; //a green progress bar should be at 100%
|
||||
}
|
||||
|
||||
progbar.css("width", nScore + '%');
|
||||
|
||||
if (nScore >= 0 && nScore <= warning_level) {
|
||||
sComplexity = 'Weak';
|
||||
progbar.addClass('progress-bar-danger');
|
||||
progbar.removeClass('progress-bar-success progress-bar-warning');
|
||||
} else if (nScore > warning_level && nScore <= required_strength) {
|
||||
sComplexity = 'Good';
|
||||
progbar.addClass('progress-bar-warning');
|
||||
progbar.removeClass('progress-bar-success progress-bar-danger');
|
||||
} else if (nScore > required_strength) {
|
||||
sComplexity = "Strong";
|
||||
progbar.addClass('progress-bar-success');
|
||||
progbar.removeClass('progress-bar-warning progress-bar-danger');
|
||||
}
|
||||
} else {
|
||||
/* no password, so reset the displays */
|
||||
progbar = $("#progbar");
|
||||
progbar.css("width", '0%');
|
||||
progbar.removeClass('progress-bar-success progress-bar-warning');
|
||||
}
|
||||
|
||||
return nScore;
|
||||
}
|
||||
Reference in New Issue
Block a user