From 49c26aad10d9c7dffb02a7bd6152a6e50eb82488 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 23 Apr 2014 18:07:16 -0400 Subject: [PATCH] Track inventory group expand/collapse in local storage. Support multiple inventory syncs running simultaneously. Moved socket connection for job_events to app root. Added on/off socket button so user can tell state of connection and reset/reconnect when needed. Applied AC-1211 fix. --- awx/ui/static/js/app.js | 41 ++++++++- awx/ui/static/js/controllers/Inventories.js | 74 +++++++++-------- awx/ui/static/js/helpers/Children.js | 73 ++++++++++++---- awx/ui/static/js/helpers/Groups.js | 7 +- awx/ui/static/js/lists/InventoryGroups.js | 7 ++ awx/ui/static/less/ansible-ui.less | 12 +++ .../lib/angular-tz-extensions/.bower.json | 13 +-- .../lib/angular-tz-extensions/bower.json | 2 +- .../lib/angular-tz-extensions.js | 5 +- .../lib/angular-tz-extensions.min.js | 2 +- awx/ui/static/lib/ansible/InventoryTree.js | 75 ++++++++++++++--- awx/ui/static/lib/ansible/Socket.js | 83 ++++++++++++++----- .../static/lib/ansible/generator-helpers.js | 17 +++- awx/ui/static/lib/ansible/list-generator.js | 3 +- awx/ui/static/partials/inventory-edit.html | 2 +- bower.json | 3 +- 16 files changed, 317 insertions(+), 102 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 75f1f1ac42..73a40e680c 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -409,9 +409,11 @@ angular.module('Tower', [ } ]) .run(['$cookieStore', '$rootScope', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'ViewLicense', - 'Timer', 'ClearScope', 'HideStream', + 'Timer', 'ClearScope', 'HideStream', 'Socket', function ($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense, - Timer, ClearScope, HideStream) { + Timer, ClearScope, HideStream, Socket) { + + var base, sock; LoadBasePaths(); @@ -470,7 +472,7 @@ angular.module('Tower', [ } // If browser refresh, activate the correct tab - var base = ($location.path().replace(/^\//, '').split('/')[0]); + base = ($location.path().replace(/^\//, '').split('/')[0]); if (base === '') { base = 'home'; $location.path('/home'); @@ -493,5 +495,38 @@ angular.module('Tower', [ e.preventDefault(); $('#' + tabs + ' #' + tab).tab('show'); }; + + // Listen for job changes and issue callbacks to initiate + // DOM updates + function openSocket() { + sock = Socket({ scope: $rootScope, endpoint: "jobs" }); + sock.init(); + setTimeout(function() { + $rootScope.$apply(function() { + sock.checkStatus(); + $rootScope.$emit('SocketErrorEncountered'); + }); + }); + sock.on("status_changed", function(data) { + $rootScope.$emit('JobStatusChange', data); + }); + } + openSocket(); + + $rootScope.socketToggle = function() { + switch($rootScope.socketStatus) { + case 'ok': + case 'connecting': + sock = null; + $rootScope.socketStatus = 'error'; + $rootScope.socketTip = 'Disconnected. Click to connect.'; + break; + case 'error': + sock = null; + $rootScope.socketStatus = ''; + $rootScope.socketTip = ''; + setTimeout(openSocket, 500); + } + }; } ]); diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index acf62cf560..10fc2968b5 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -471,11 +471,11 @@ InventoriesAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log -function InventoriesEdit($scope, $location, $routeParams, $compile, $log, GenerateList, ClearScope, InventoryGroups, InventoryHosts, BuildTree, Wait, +function InventoriesEdit($scope, $location, $routeParams, $compile, $log, $rootScope, GenerateList, ClearScope, InventoryGroups, InventoryHosts, BuildTree, Wait, GetSyncStatusMsg, InjectHosts, HostsReload, GroupsEdit, GroupsDelete, Breadcrumbs, LoadBreadCrumbs, Empty, Rest, ProcessErrors, InventoryUpdate, Alert, ToggleChildren, ViewUpdateStatus, GroupsCancelUpdate, Find, EditInventoryProperties, HostsEdit, HostsDelete, ToggleHostEnabled, CopyMoveGroup, CopyMoveHost, Stream, GetBasePath, ShowJobSummary, ApplyEllipsis, WatchInventoryWindowResize, - HelpDialog, InventoryGroupsHelp, Store, ViewJob, Socket) { + HelpDialog, InventoryGroupsHelp, Store, ViewJob) { ClearScope(); @@ -489,6 +489,40 @@ function InventoriesEdit($scope, $location, $routeParams, $compile, $log, Genera title: '{{ inventory_name }}' }); + // Handle inventory sync status changes + if ($rootScope.rmoveJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) { + var group, stat; + if ($scope.groups) { + // Assuming we have a list of groups available + group = Find({ list: $scope.groups, key: 'group_id', val: data.group_id }); + if (group) { + // And we found the affected group + $log.debug('Received event for group: ' + group.name); + if (data.status === 'failed' || data.status === 'successful') { + $log.debug('Update completed. Refreshing the tree.'); + $scope.refreshGroups(group.id, group.group_id); + } + else { + $log.debug('Status changed to: ' + data.status); + stat = GetSyncStatusMsg({ + status: data.status, + has_inventory_sources: group.has_inventory_sources, + source: group.source + }); + $log.debug('Changing tooltip to: ' + stat.tooltip); + group.status = data.status; + group.status_class = stat['class']; + group.status_tooltip = stat.tooltip; + group.launch_tooltip = stat.launch_tip; + group.launch_class = stat.launch_class; + } + } + } + }); + // After the tree data loads for the first time, generate the groups and hosts lists if ($scope.removeGroupTreeLoaded) { $scope.removeGroupTreeLoaded(); @@ -643,38 +677,6 @@ function InventoriesEdit($scope, $location, $routeParams, $compile, $log, Genera } }; - if ($scope.removeWatchUpdateStatus) { - $scope.removeWatchUpdateStatus(); - } - $scope.removeWatchUpdateStatus = $scope.$on('WatchUpdateStatus', function(e, job_id, group_id, tree_id) { - var io = Socket({ scope: $scope, endpoint: "jobs" }), - group = Find({ list: $scope.groups, key: 'id', val: tree_id }), - stat; - $log.debug('Watching for updates to job: ' + job_id + ' for group: ' + group_id + ' ' + group.name); - io.init(); - io.on("status_changed", function(data) { - Wait('stop'); - if (data.status === "failed" || data.status === "successful") { - $log.debug('Update completed. Refreshing the tree.'); - $scope.refreshGroups(tree_id, group_id); - } - else { - $log.debug('Status changed to: ' + data.status); - stat = GetSyncStatusMsg({ - status: data.status, - has_inventory_sources: group.has_inventory_sources, - source: group.source - }); - $log.debug('changing tooltip to: ' + stat.tooltip); - group.status = data.status; - group.status_class = stat['class']; - group.status_tooltip = stat.tooltip; - group.launch_tooltip = stat.launch_tip; - group.launch_class = stat.launch_class; - } - }); - }); - $scope.createGroup = function () { GroupsEdit({ scope: $scope, @@ -845,10 +847,10 @@ function InventoriesEdit($scope, $location, $routeParams, $compile, $log, Genera } -InventoriesEdit.$inject = ['$scope', '$location', '$routeParams', '$compile', '$log', 'GenerateList', 'ClearScope', 'InventoryGroups', 'InventoryHosts', +InventoriesEdit.$inject = ['$scope', '$location', '$routeParams', '$compile', '$log', '$rootScope', 'GenerateList', 'ClearScope', 'InventoryGroups', 'InventoryHosts', 'BuildTree', 'Wait', 'GetSyncStatusMsg', 'InjectHosts', 'HostsReload', 'GroupsEdit', 'GroupsDelete', 'Breadcrumbs', 'LoadBreadCrumbs', 'Empty', 'Rest', 'ProcessErrors', 'InventoryUpdate', 'Alert', 'ToggleChildren', 'ViewUpdateStatus', 'GroupsCancelUpdate', 'Find', 'EditInventoryProperties', 'HostsEdit', 'HostsDelete', 'ToggleHostEnabled', 'CopyMoveGroup', 'CopyMoveHost', 'Stream', 'GetBasePath', 'ShowJobSummary', 'ApplyEllipsis', 'WatchInventoryWindowResize', 'HelpDialog', 'InventoryGroupsHelp', 'Store', - 'ViewJob', 'Socket' + 'ViewJob' ]; diff --git a/awx/ui/static/js/helpers/Children.js b/awx/ui/static/js/helpers/Children.js index 4bc3701c69..8194d38653 100644 --- a/awx/ui/static/js/helpers/Children.js +++ b/awx/ui/static/js/helpers/Children.js @@ -12,53 +12,98 @@ 'use strict'; angular.module('ChildrenHelper', ['RestServices', 'Utilities']) - .factory('ToggleChildren', [ function () { + .factory('ToggleChildren', ['$location', 'Store', function ($location, Store) { return function (params) { var scope = params.scope, list = params.list, id = params.id, set = scope[list.name], - i, clicked, found = false; + clicked, + //base = $location.path().replace(/^\//, '').split('/')[0], + path = $location.path(), + local_child_store; - function expand(node) { - var i; - set[node].ngicon = 'fa fa-minus-square-o node-toggle'; - for (i = node + 1; i < set.length; i++) { - if (set[i].parent === set[node].id) { - set[i].show = true; + function updateExpand(key, expand) { + var found = false; + local_child_store.every(function(child, i) { + if (child.key === key) { + local_child_store[i].expand = expand; + found = true; + return false; } + return true; + }); + if (!found) { + local_child_store.push({ key: key, expand: expand }); } } + function updateShow(key, show) { + var found = false; + local_child_store.every(function(child, i) { + if (child.key === key) { + local_child_store[i].show = show; + found = true; + return false; + } + return true; + }); + if (!found) { + local_child_store.push({ key: key, show: show }); + } + } + + function expand(node) { + var i, has_children = false; + for (i = node + 1; i < set.length; i++) { + if (set[i].parent === set[node].id) { + updateShow(set[i].key, true); + set[i].show = true; + } + } + set[node].ngicon = (has_children) ? 'fa fa-minus-square-o node-toggle' : 'fa fa-minus-square-o node-toggle'; + } + function collapse(node) { - var i; - set[node].ngicon = 'fa fa-plus-square-o node-toggle'; + var i, has_children = false; for (i = node + 1; i < set.length; i++) { if (set[i].parent === set[node].id) { set[i].show = false; + has_children = true; + updateShow(set[i].key, false); if (set[i].related.children) { collapse(i); } } } + set[node].ngicon = (has_children) ? 'fa fa-plus-square-o node-toggle' : 'fa fa-square-o node-toggle'; + } + + local_child_store = Store(path + '_children'); + if (!local_child_store) { + local_child_store = []; } // Scan the array list and find the clicked element - for (i = 0; i < set.length && found === false; i++) { - if (set[i].id === id) { + set.every(function(row, i) { + if (row.id === id) { clicked = i; - found = true; + return false; } - } + return true; + }); // Expand or collapse children based on clicked element's icon if (/plus-square-o/.test(set[clicked].ngicon)) { // Expand: lookup and display children expand(clicked); + updateExpand(set[clicked].key, true); } else if (/minus-square-o/.test(set[clicked].ngicon)) { collapse(clicked); + updateExpand(set[clicked].key, false); } + Store(path + '_children', local_child_store); }; } ]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 82415f5ece..c4fc8ae201 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -1064,6 +1064,8 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched if (!error) { // Update the parent view with any changes if (groups_reload) { + $log.debug('calling UpdateGroup group_id: ' + group_id + ' name: ' + properties_scope.name + ' description: ' + properties_scope.description + + 'has_inventory_sources: ' + ((sources_scope.source && sources_scope.source.value) ? 'true' : 'false') + ' source: ' + sources_scope.source.value ); UpdateGroup({ scope: parent_scope, group_id: group_id, @@ -1221,8 +1223,9 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched Rest.post(data) .success(function (data) { group_created = true; + group_id = data.id; + sources_scope.source_url = data.related.inventory_source; if (properties_scope.variables) { - sources_scope.source_url = data.related.inventory_source; modal_scope.$emit('updateVariables', json_data, data.related.variable_data); } else { @@ -1414,7 +1417,7 @@ function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, Sched var group; if (groups && groups.length > 0) { group = groups.pop(); - Rest.setUrl(GetBasePath('group') + group.group_id + '/'); + Rest.setUrl(GetBasePath('groups') + group.group_id + '/'); Rest.destroy() .success(function() { scope.$emit('DeleteNextGroup'); diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index e2bbdd3616..72e0af8a3d 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -56,6 +56,13 @@ angular.module('InventoryGroupsDefinition', []) awToolTip: "Refresh the page", ngClick: "refreshGroups()" },*/ + socket: { + mode: 'all', + iconClass: "{{ 'fa fa-power-off fa-lg socket-' + socketStatus }}", + awToolTip: "{{ socketTip }}", + dataTipWatch: "socketTip", + ngClick: "socketToggle()", + }, stream: { ngClick: "showGroupActivity()", awToolTip: "View Activity Stream", diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 71968627e6..a2bdf96101 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1203,6 +1203,18 @@ input[type="checkbox"].checkbox-no-label { } /* end */ +/* Socket icon */ + .socket-ok { + color: @green; + } + .socket-error { + color: @red; + } + .socket-connecting { + color: @warning + } +/* end */ + .field-success { color: #5bb75b; } diff --git a/awx/ui/static/lib/angular-tz-extensions/.bower.json b/awx/ui/static/lib/angular-tz-extensions/.bower.json index 7eeae678b9..16fe500909 100644 --- a/awx/ui/static/lib/angular-tz-extensions/.bower.json +++ b/awx/ui/static/lib/angular-tz-extensions/.bower.json @@ -1,6 +1,6 @@ { "name": "angular-tz-extensions", - "version": "0.3.9", + "version": "0.3.10", "main": "js/angular-timezones.js", "ignore": [ ".bowerrc", @@ -34,13 +34,14 @@ "Chris Houseknecht" ], "license": "MIT", - "_release": "0.3.9", + "_release": "0.3.10", "_resolution": { "type": "version", - "tag": "0.3.9", - "commit": "dded062e72274e2fc5379bb251c476d152ecc1a1" + "tag": "0.3.10", + "commit": "244ac659f0a15fe389f4cb1222ecafdc890e39ef" }, "_source": "git://github.com/chouseknecht/angular-tz-extensions.git", - "_target": "*", - "_originalSource": "angular-tz-extensions" + "_target": "~0.3.10", + "_originalSource": "angular-tz-extensions", + "_direct": true } \ No newline at end of file diff --git a/awx/ui/static/lib/angular-tz-extensions/bower.json b/awx/ui/static/lib/angular-tz-extensions/bower.json index 980b6e49d0..5384a14404 100644 --- a/awx/ui/static/lib/angular-tz-extensions/bower.json +++ b/awx/ui/static/lib/angular-tz-extensions/bower.json @@ -1,6 +1,6 @@ { "name": "angular-tz-extensions", - "version": "0.3.9", + "version": "0.3.10", "main": "js/angular-timezones.js", "ignore": [ ".bowerrc", diff --git a/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.js b/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.js index 03b2b76023..1dca025925 100644 --- a/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.js +++ b/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.js @@ -91,8 +91,8 @@ name: name, abbreviation: reference.getTimezoneAbbreviation(), offset: reference.getTimezoneOffset(), - region: name.split('/')[0], - locality: name.split('/')[1].replace('_', ' ') + region: ( (name) ? name.split('/')[0] : '' ), + locality: ( (name) ? name.split('/')[1].replace('_', ' ') : '' ) }; return result; @@ -174,6 +174,7 @@ } name = jstz.determine().name(); + name = (name === null || name === '' || name === undefined) ? 'America/New_York' : name; now = new Date(); return resolve(name, now); }, diff --git a/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.min.js b/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.min.js index 7c2608ce33..81589f5488 100644 --- a/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.min.js +++ b/awx/ui/static/lib/angular-tz-extensions/lib/angular-tz-extensions.min.js @@ -1 +1 @@ -/*! angular-tz-extensions - v0.0.1 - 2014-03-03 */!function(a){var b=a.angular,c=a.timezoneJS,d=a.jstz,e=function(a){var b,c=new Date;for(b in a)c[b]=a[b];return c},f=b.module("Timezones",[]);f.config(function(){c.fromLocalString=function(a,b){var d,f,g=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a),h=[1,4,5,6,7,10,11];for(d=0;f=h[d];++d)g[f]=+g[f]||0;return g[2]=(+g[2]||1)-1,g[3]=+g[3]||1,e(new c.Date(g[1],g[2],g[3],g[4],g[5],g[6],g[7],b))}}),f.constant("$timezones.definitions.location","/tz/data"),f.run(["$timezones.definitions.location",function(a){c.timezone.zoneFileBasePath=a,c.timezone.init({async:!1})}]),f.factory("$timezones",["$log","$http","$timezones.definitions.location",function(a,f,g){var h=function(a,d){var e,f;if("number"==typeof d&&(d=new Date(d)),!b.isDate(d))throw{name:"DateObjectExpected",message:'Expected a Date object; got "'+d+'".'};return d=new c.Date(d,a),e=d.getTimezone(),f={name:e,abbreviation:d.getTimezoneAbbreviation(),offset:d.getTimezoneOffset(),region:e.split("/")[0],locality:e.split("/")[1].replace("_"," ")}};return{align:function(a,d,f){if(!b.isDate(a))throw{name:"DateObjectExpected",message:'Expected a Date object; got "'+a+'".'};try{if(b.isObject(d)&&d.name)return e(new c.Date(a,d.name));if(b.isString(d))return e(new c.Date(a,d))}catch(g){if(!0===f)return a;throw new Error("The timezone argument must either be an Olson name (e.g., America/New_York), or a timezone object (produced by the resolve function) bearing an Olson name on the name property.")}},resolve:h,getLocal:function(){var a,b;if("undefined"==typeof d||"function"!=typeof d.determine)throw{name:"JSTZLibraryMissing",message:"The jsTimezoneDetect library, available at https://bitbucket.org/pellepim/jstimezonedetect, is required to detect the local timezone."};return a=d.determine().name(),b=new Date,h(a,b)},toUTC:function(a,c){var d,e,f=b.isDate(a)?a:new Date(a);return d=f.getUTCFullYear()+"-"+("00"+(f.getUTCMonth()+1)).substr(-2,2)+"-"+("00"+f.getUTCDate()).substr(-2,2)+"T"+("00"+f.getUTCHours()).substr(-2,2)+":"+("00"+f.getUTCMinutes()).substr(-2,2)+":"+("00"+f.getUTCSeconds()).substr(-2,2)+".000Z",e=this.align(new Date(d),c),new Date(e.getTime()+6e4*e.getTimezoneOffset())},getZoneList:function(b){var c,d=[];localStorage.zones?b.$emit("zonesReady"):f({method:"GET",url:g+"/zone.tab"}).success(function(a){var e,f,g=a.match(/[^\r\n]+/g);for(e=0;e 0) ? getExpandState(sorted[i].id) : false; + show = getShowState(sorted[i].id); + group = { name: sorted[i].name, has_active_failures: sorted[i].has_active_failures, @@ -112,10 +142,11 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P 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, - ngicon: (sorted[i].children.length > 0) ? 'fa fa-minus-square-o node-toggle' : 'fa fa-square-o node-no-toggle', + show: show, related: sorted[i].related, status: sorted[i].summary_fields.inventory_source.status, status_class: stat['class'], @@ -127,16 +158,37 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P hosts_status_class: hosts_status['class'], inventory_id: inventory_id, selected_class: '', - show: true, isDraggable: true, isDroppable: true }; - groups.push(group); + 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) { // For new group + // Find parent's expand state and set the show property accordingly. + // If parent is not expanded, then child should be hidden. + if (parent > 0) { + scope.groups.every(function(g) { + if (g.id === group.parent) { + group.show = getExpandState(g.key); + return false; + } + return true; + }); + } 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); } @@ -185,7 +237,10 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P }); }); } - + local_child_store = Store(path + '_children'); + if (!local_child_store) { + local_child_store = []; + } loadTreeData(); }; } @@ -222,8 +277,8 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P // Update date sync status links/icons stat = GetSyncStatusMsg({ status: scope.groups[i].status, - has_inventory_sources: scope.groups[i].has_inventory_sources, - source: scope.groups[i].source + has_inventory_sources: properties.has_inventory_sources, + source: properties.source }); scope.groups[i].status_class = stat['class']; scope.groups[i].status_tooltip = stat.tooltip; diff --git a/awx/ui/static/lib/ansible/Socket.js b/awx/ui/static/lib/ansible/Socket.js index 2fad2709d6..ae248bb831 100644 --- a/awx/ui/static/lib/ansible/Socket.js +++ b/awx/ui/static/lib/ansible/Socket.js @@ -29,9 +29,10 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) $rootScope.sessionTimer.expireSession(); $location.url('/login'); } - else { - Alert("Socket Error", "Attempt to refresh page failed with: " + reason, "alert-danger"); - // should we do something more here? + else if (scope.socketStatus === 'error') { + Alert("Connection Error", "Error encountered while attempting to connect to the websocket server. Confirm the server " + + "is up. Use the button found on the Inventories, Projects and Jobs pages to reconnect.", + "alert-danger"); } }); @@ -49,53 +50,75 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) self.socket = io.connect(url, { headers: { 'Authorization': 'Token ' + token, - "X-Auth-Token": 'Token ' + token + 'X-Auth-Token': 'Token ' + token }, 'connect timeout': 3000, 'try multiple transports': false, 'max reconneciton attemps': 3, 'reconnection limit': 3000, - }); self.socket.on('connection', function() { $log.debug('Socket connecting...'); - self.scope.socket_status = 'connecting'; + self.scope.$apply(function () { + self.scope.socketStatus = 'connecting'; + self.scope.socketTip = 'Connecting. Click to cancel.'; + }); }); self.socket.on('connect', function() { $log.debug('Socket connection established'); - self.scope.socket_status = 'ok'; + self.scope.$apply(function () { + self.scope.socketStatus = 'ok'; + self.scope.socketTip = 'Connected. Click to close.'; + }); }); self.socket.on('connect_failed', function(reason) { var r = reason || 'connection refused by host'; $log.error('Socket connection failed: ' + r); - self.scope.socket_status = 'error'; - self.scope.socket_reason = r; - self.scope.$emit('SocketErrorEncountered', 'Connection failed: ' + r); + self.scope.$apply(function () { + self.scope.socketStatus = 'error'; + self.scope.socketTip = 'Connection failed. Click to retry.'; + self.scope.$emit('SocketErrorEncountered'); + }); + }); self.socket.on('diconnect', function() { $log.debug('Socket disconnected'); - self.scope.socket_status = 'disconnected'; + self.scope.$apply(function() { + self.socketStatus = 'error'; + self.socketTip = 'Disconnected. Click to connect.'; + self.scope.$emit('SocketErrorEncountered'); + }); }); self.socket.on('error', function(reason) { var r = reason || 'connection refused by host'; - $log.error('Socket error encountered: ' + r); - self.scope.socket_status = 'error'; - self.scope.socket_reason = r; - self.scope.$emit('SocketErrorEncountered', r); + $log.debug('Socket error: ' + r); + self.scope.$apply(function() { + self.scope.socketStatus = 'error'; + self.scope.socketTip = 'Connection error encountered. Click to retry.'; + self.scope.$emit('SocketErrorEncountered'); + }); }); self.socket.on('reconnecting', function() { $log.debug('Socket attempting reconnect...'); - self.scope.socket_status = 'connecting'; + self.scope.$apply(function() { + self.scope.socketStatus = 'connecting'; + self.scope.socketTip = 'Connecting. Click to cancel.'; + }); }); self.socket.on('reconnect', function() { $log.debug('Socket reconnected'); - self.scope.socket_status = 'ok'; + self.scope.$apply(function() { + self.scope.socketStatus = 'ok'; + self.scope.socketTip = 'Connected. Click to close.'; + }); }); self.socket.on('reconnect_failed', function(reason) { $log.error('Socket reconnect failed: ' + reason); - self.scope.socket_status = 'error'; - self.scope.socket_reason = reason; - self.scope.$emit('SocketErrorEncountered', 'Connection failed: ' + reason); + self.scope.$apply(function() { + self.scope.socketStatus = 'error'; + self.scope.socketTip = 'Connection failed. Click to retry.'; + self.scope.$emit('SocketErrorEncountered'); + }); }); } else { @@ -103,6 +126,26 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) self.scope.$emit('SocketErrorEncountered', 'Session expired'); } }, + checkStatus: function() { + // Check connection status + var self = this; + if (self.socket.socket.connected) { + $log.debug('Socket connected'); + self.scope.socketStatus = 'ok'; + self.scope.socketTip = 'Connected. Click to close.'; + } + else if (self.socket.socket.connecting || self.socket.socket.reconnecting) { + $log.debug('Socket connecting...'); + self.scope.socketStatus = 'connecting'; + self.scope.socketTip = 'Connecting. Click to cancel.'; + } + else { + $log.debug('Socket error: connection refused'); + self.scope.socketStatus = 'error'; + self.scope.socketTip = 'Connection failed. Click to retry'; + } + return self.scope.socketStatus; + }, on: function (eventName, callback) { var self = this; self.socket.on(eventName, function () { diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index e9f4fe7245..86c6f70199 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -117,6 +117,9 @@ angular.module('GeneratorHelpers', []) case 'stream': icon = 'fa-clock-o'; break; + case 'socket': + icon = 'fa-power-off'; + break; case 'refresh': icon = 'fa-refresh'; break; @@ -191,6 +194,7 @@ angular.module('GeneratorHelpers', []) 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') : ""; @@ -204,10 +208,15 @@ angular.module('GeneratorHelpers', []) html += " >"; html += (btn.img) ? "" : ""; - html += SelectIcon({ - action: action, - size: btn.iconSize - }); + if (btn.iconClass) { + html += ""; + } + else { + html += SelectIcon({ + action: action, + size: btn.iconSize + }); + } html += (btn.label) ? " " + btn.label : ""; html += " "; diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index 439484d71e..2bb1ac8bb5 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -395,7 +395,8 @@ angular.module('ListGenerator', ['GeneratorHelpers']) 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") { + itm !== 'iconClass' && itm !== "dataPlacement" && itm !== "awPopOver" && + itm !== "dataTitle") { html += Attr(fAction, itm); } } diff --git a/awx/ui/static/partials/inventory-edit.html b/awx/ui/static/partials/inventory-edit.html index 3737b77693..54549261ba 100644 --- a/awx/ui/static/partials/inventory-edit.html +++ b/awx/ui/static/partials/inventory-edit.html @@ -97,7 +97,7 @@
-
Delete group {{ group_name }}?
+
Delete group {{ group_name }}?
diff --git a/bower.json b/bower.json index a375bd3fe2..e9f8ae9056 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,8 @@ "less.js": "~1.6.3", "select2": "~3.4.5", "sizzle": "1.10.16", - "d3js": "*" + "d3js": "*", + "angular-tz-extensions": "~0.3.10" }, "resolutions": { "angular": "1.2.15-build.2398+sha.4bab3d8"