diff --git a/awx/ui/static/html/event_log.html b/awx/ui/static/html/event_log.html new file mode 100644 index 0000000000..e55e3ae972 --- /dev/null +++ b/awx/ui/static/html/event_log.html @@ -0,0 +1,618 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 2, + "url": "/api/v1/event_log/2/", + "related": { + "user": "/users/N/", + "object1": "/groups/N/", + "object2": "" + }, + + "summary_fields": { + "inventory": { + "name": "Test Inventory", + "description": "Testing activity stream" + }, + "object1": { + "name": "Group A", + "description": "The A group" + }, + "user": { + "username": "chouseknecht" + }, + "object2": {} + }, + + "created": "2013-11-05T15:18:55.000Z", + "modified": "2013-11-05T15:18:55.000Z", + "user": 1, + "event_time": "2013-11-06T15:18:55.000Z", + "operation": "create", + "changes": { + "before": { "groups": [ "Group X", "Group Y", "Group Z" ] }, + "after": { "groups": [ "Group A", "Group X", "Group Y", "Group Z" ] } + }, + "relationship": "" + }, + { + "id": 3, + "url": "/api/v1/event_log/2/", + "related": { + "user": "/users/N/", + "object1": "/groups/N/children", + "object2": "/groups/N/" + }, + + "summary_fields": { + "inventory": { + "name": "Test Inventory", + "description": "Testing activity stream" + }, + "object1": { + "name": "Group A", + "description": "The A group" + }, + "user": { "username": "chouseknecht" }, + "object2": { + "name": "Group B", + "description": "The B group" + } + }, + + "created": "2013-11-05T15:18:58.391Z", + "modified": "2013-11-05T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "associate", + "changes": { + "before": { "groups": [ "Group X", "Group Y", "Group Z" ] }, + "after": { "groups": [ "Group A", "Group X", "Group Y", "Group Z" ] } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + }, + { + "id": 1, + "url": "/api/v1/event_log/1/", + "related": { + "user": "/users/N/", + "object1": "/organizations/N/", + "object2": "" + }, + + "summary_fields": { + "object1": { + "name": "Frito Lay", + "description": "Salty Snacks" + }, + "user": { + "username": "chouseknecht" + } + }, + + "created": "2013-11-06T15:18:58.391Z", + "modified": "2013-11-06T15:18:58.514Z", + "user": 1, + "event_time": "2013-11-06T15:18:58.514Z", + "operation": "change", + "changes": { + "before": { "description": "Healthy Snacks" }, + "after": { "description": "Salty Snacks" } + }, + "relationship": "" + } + ] +} \ No newline at end of file diff --git a/awx/ui/static/img/cow.png b/awx/ui/static/img/cow.png deleted file mode 100644 index bd9ac11bf6..0000000000 Binary files a/awx/ui/static/img/cow.png and /dev/null differ diff --git a/awx/ui/static/img/footsteps.png b/awx/ui/static/img/footsteps.png new file mode 100644 index 0000000000..42bdfd90da Binary files /dev/null and b/awx/ui/static/img/footsteps.png differ diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 75dabd23f7..c92697f086 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -5,7 +5,7 @@ * */ -var urlPrefix = '/static/'; +var urlPrefix = $basePath; angular.module('ansible', [ 'RestServices', @@ -74,13 +74,16 @@ angular.module('ansible', [ 'InventorySyncStatusWidget', 'SCMSyncStatusWidget', 'ObjectCountWidget', + 'StreamWidget', 'JobsHelper', 'InventoryStatusDefinition', 'InventorySummaryHelpDefinition', 'InventoryHostsHelpDefinition', 'TreeSelector', 'CredentialsHelper', - 'TimerService' + 'TimerService', + 'StreamListDefinition', + 'HomeGroupListDefinition' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. @@ -245,13 +248,15 @@ angular.module('ansible', [ when('/logout', { templateUrl: urlPrefix + 'partials/organizations.html', controller: Authenticate }). when('/home', { templateUrl: urlPrefix + 'partials/home.html', controller: Home }). + + when('/home/groups', { templateUrl: urlPrefix + 'partials/subhome.html', controller: HomeGroups }). otherwise({redirectTo: '/home'}); }]) .run(['$cookieStore', '$rootScope', 'CheckLicense', '$location', 'Authorization','LoadBasePaths', 'ViewLicense', - 'Timer', + 'Timer', 'ClearScope', 'HideStream', function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense, - Timer) { + Timer, ClearScope, HideStream) { LoadBasePaths(); @@ -260,12 +265,17 @@ angular.module('ansible', [ $rootScope.sessionTimer = Timer.init(); $rootScope.$on("$routeChangeStart", function(event, next, current) { + + // Before navigating away from current tab, make sure the primary view is visible + if ($('#stream-container').is(':visible')) { + HideStream(); + } + // On each navigation request, check that the user is logged in - - var tst = /login/; + var tst = /(login|logout)/; var path = $location.path(); if ( !tst.test($location.path()) ) { - // capture most recent URL, excluding login + // capture most recent URL, excluding login/logout $rootScope.lastPath = path; $cookieStore.put('lastPath', path); } @@ -288,11 +298,6 @@ angular.module('ansible', [ CheckLicense(); } - if ($rootScope.timer) { - clearInterval($rootScope.timer); - $rootScope.timer = null; - } - // Make the correct tab active var base = $location.path().replace(/^\//,'').split('/')[0]; if (base == '') { diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index ae05097723..8ad5229ae3 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -227,6 +227,9 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa data['username'] = scope['access_key']; data['password'] = scope['secret_key']; break; + case 'scm': + data['ssh_key_unlock'] = scope['scm_key_unlock']; + break; } if (Empty(data.team) && Empty(data.user)) { @@ -415,7 +418,10 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP scope['ssh_password'] = data.password; master['ssh_username'] = scope['ssh_username']; master['ssh_password'] = scope['ssh_password']; - break; + break; + case 'scm': + scope['scm_key_unlock'] = data['ssh_key_unlock']; + break; } scope.$emit('credentialLoaded'); @@ -451,7 +457,6 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP } } - if (!Empty(scope.team)) { data.team = scope.team; data.user = ""; @@ -472,6 +477,9 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP data['username'] = scope['access_key']; data['password'] = scope['secret_key']; break; + case 'scm': + data['ssh_key_unlock'] = scope['scm_key_unlock']; + break; } if (Empty(data.team) && Empty(data.user)) { diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index b1e99a22c8..922f4e0fa5 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -11,7 +11,7 @@ 'use strict'; function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, JobStatus, InventorySyncStatus, SCMSyncStatus, - ClearScope) { + ClearScope, Stream) { ClearScope('home'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -28,6 +28,8 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J InventorySyncStatus({ target: 'container2' }); SCMSyncStatus({ target: 'container4' }); ObjectCount({ target: 'container3' }); + + $rootScope.showActivity = function() { Stream(); } $rootScope.$on('WidgetLoaded', function() { // Once all the widgets report back 'loaded', turn off Wait widget @@ -39,4 +41,82 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J } Home.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', 'Wait', 'ObjectCount', 'JobStatus', 'InventorySyncStatus', - 'SCMSyncStatus', 'ClearScope']; \ No newline at end of file + 'SCMSyncStatus', 'ClearScope', 'Stream']; + + +function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, + GetBasePath, SearchInit, PaginateInit, FormatDate, HostsStatusMsg, UpdateStatusMsg, ViewUpdateStatus) { + + ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior + //scope. + + var generator = GenerateList; + var list = HomeGroupList; + var defaultUrl=GetBasePath('groups'); + + var scope = generator.inject(list, { mode: 'edit' }); + var base = $location.path().replace(/^\//,'').split('/')[0]; + + if (scope.removePostRefresh) { + scope.removePostRefresh(); + } + scope.removePostRefresh = scope.$on('PostRefresh', function() { + var msg, update_status, last_update; + for (var i=0; i < scope.groups.length; i++) { + + scope['groups'][i]['inventory_name'] = scope['groups'][i]['summary_fields']['inventory']['name']; + + last_update = (scope.groups[i].summary_fields.inventory_source.last_updated == null) ? null : + FormatDate(new Date(scope.groups[i].summary_fields.inventory_source.last_updated)); + + // Set values for Failed Hosts column + scope.groups[i].failed_hosts = scope.groups[i].hosts_with_active_failures + ' / ' + scope.groups[i].total_hosts; + + msg = HostsStatusMsg({ + active_failures: scope.groups[i].hosts_with_active_failures, + total_hosts: scope.groups[i].total_hosts, + inventory_id: scope.groups[i].inventory + }); + + update_status = UpdateStatusMsg({ status: scope.groups[i].summary_fields.inventory_source.status }); + + scope.groups[i].failed_hosts_tip = msg['tooltip']; + scope.groups[i].failed_hosts_link = msg['url']; + scope.groups[i].failed_hosts_class = msg['class']; + scope.groups[i].status = update_status['status']; + scope.groups[i].source = scope.groups[i].summary_fields.inventory_source.source; + scope.groups[i].last_updated = last_update; + scope.groups[i].status_badge_class = update_status['class']; + scope.groups[i].status_badge_tooltip = update_status['tooltip']; + } + }); + + SearchInit({ scope: scope, set: 'groups', list: list, url: defaultUrl }); + PaginateInit({ scope: scope, list: list, url: defaultUrl }); + + if ($routeParams['status']) { + // with status param, called post update-submit + scope[list.iterator + 'SearchField'] = 'status'; + scope[list.iterator + 'SelectShow'] = true; + scope[list.iterator + 'SearchSelectOpts'] = list.fields['status'].searchOptions; + scope[list.iterator + 'SearchFieldLabel'] = list.fields['status'].label.replace(/\/g,' '); + for (var opt in list.fields['status'].searchOptions) { + if (list.fields['status'].searchOptions[opt].value == $routeParams['status']) { + scope[list.iterator + 'SearchSelectValue'] = list.fields['status'].searchOptions[opt]; + break; + } + } + } + + scope.search(list.iterator); + + LoadBreadCrumbs(); + + scope.viewUpdateStatus = function(id) { ViewUpdateStatus({ scope: scope, group_id: id }) }; + + } + +HomeGroups.$inject = [ '$location', '$routeParams', 'HomeGroupList', 'GenerateList', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', + 'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'HostsStatusMsg', 'UpdateStatusMsg', 'ViewUpdateStatus' + ]; + \ No newline at end of file diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index bfe9635ff9..724a007d02 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -13,7 +13,7 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, LookUpInit, SubmitJob, FormatDate, Refresh, - JobStatusToolTip) + JobStatusToolTip, Empty) { ClearScope('htmlTemplate'); var list = JobList; @@ -52,7 +52,7 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, if ($routeParams['job_host_summaries__host']) { defaultUrl += '?job_host_summaries__host=' + $routeParams['job_host_summaries__host']; } - if ($routeParams['inventory__int'] && $routeParams['status']) { + else if ($routeParams['inventory__int'] && $routeParams['status']) { defaultUrl += '?inventory__int=' + $routeParams['inventory__int'] + '&status=' + $routeParams['status']; } @@ -70,6 +70,18 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, scope[list.iterator + 'SearchValue'] = $routeParams['id__int']; scope[list.iterator + 'SearchFieldLabel'] = 'Job ID'; } + if ($routeParams['status']) { + scope[list.iterator + 'SearchField'] = 'status'; + scope[list.iterator + 'SelectShow'] = true; + scope[list.iterator + 'SearchSelectOpts'] = list.fields['status'].searchOptions; + scope[list.iterator + 'SearchFieldLabel'] = list.fields['status'].label.replace(/\/g,' '); + for (var opt in list.fields['status'].searchOptions) { + if (list.fields['status'].searchOptions[opt].value == $routeParams['status']) { + scope[list.iterator + 'SearchSelectValue'] = list.fields['status'].searchOptions[opt]; + break; + } + } + } scope.search(list.iterator); @@ -162,7 +174,8 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, JobsListCtrl.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors','GetBasePath', 'LookUpInit', 'SubmitJob', 'FormatDate', 'Refresh', 'JobStatusToolTip' + 'ProcessErrors','GetBasePath', 'LookUpInit', 'SubmitJob', 'FormatDate', 'Refresh', 'JobStatusToolTip', + 'Empty' ]; diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index 6262ff3e78..75d13fdd64 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -186,6 +186,26 @@ angular.module('CredentialFormDefinition', []) awPassMatch: true, associated: 'ssh_key_unlock' }, + "scm_key_unlock": { + label: 'Key Password', + type: 'password', + ngShow: "kind.value == 'scm'", + addRequired: false, + editRequired: false, + ngChange: "clearPWConfirm('scm_key_unlock_confirm')", + associated: 'scm_key_unlock_confirm', + ask: false, + clear: true + }, + "scm_key_unlock_confirm": { + label: 'Confirm Key Password', + type: 'password', + ngShow: "kind.value == 'scm'", + addRequired: false, + editRequired: false, + awPassMatch: true, + associated: 'scm_key_unlock' + }, "sudo_username": { label: 'Sudo Username', type: 'text', diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 040be96606..fe180554c5 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -79,6 +79,49 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' } }]) + .factory('ViewUpdateStatus', [ 'Rest', 'ProcessErrors', 'GetBasePath', 'ShowUpdateStatus', 'Alert', + function(Rest, ProcessErrors, GetBasePath, ShowUpdateStatus, Alert) { + return function(params) { + + var scope = params.scope; + var id = params.group_id; + var found = false; + var group; + for (var i=0; i < scope.groups.length; i++) { + if (scope.groups[i].id == id) { + found = true; + group = scope.groups[i]; + } + } + if (found) { + if (group.summary_fields.inventory_source.source == "" || group.summary_fields.inventory_source.source == null) { + Alert('Missing Configuration', 'The selected group is not configured for inventory updates. ' + + 'You must first edit the group, provide Source settings, and then run an update.', 'alert-info'); + } + else if (group.summary_fields.inventory_source.status == "" || group.summary_fields.inventory_source.status == null || + group.summary_fields.inventory_source.status == "never updated") { + Alert('No Status Available', 'The inventory update process has not run for the selected group. Start the process by ' + + 'clicking the Update button.', 'alert-info'); + } + else { + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success( function(data, status, headers, config) { + var url = (data.related.current_update) ? data.related.current_update : data.related.last_update; + ShowUpdateStatus({ group_name: data.summary_fields.group.name, + last_update: url }); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' POST returned status: ' + status }); + }); + } + } + + } + }]) + .factory('HostsStatusMsg', [ function() { return function(params) { var active_failures = params.active_failures; @@ -278,11 +321,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' }]) .factory('InventoryStatus', [ '$rootScope', '$routeParams', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'InventorySummary', - 'GenerateList', 'ClearScope', 'SearchInit', 'PaginateInit', 'Refresh', 'InventoryUpdate', 'GroupsEdit', 'ShowUpdateStatus', 'HelpDialog', - 'InventorySummaryHelp', 'BuildTree', 'ClickNode', 'HostsStatusMsg', 'UpdateStatusMsg', - function($rootScope, $routeParams, Rest, Alert, ProcessErrors, GetBasePath, FormatDate, InventorySummary, GenerateList, ClearScope, SearchInit, - PaginateInit, Refresh, InventoryUpdate, GroupsEdit, ShowUpdateStatus, HelpDialog, InventorySummaryHelp, BuildTree, ClickNode, - HostsStatusMsg, UpdateStatusMsg) { + 'GenerateList', 'ClearScope', 'SearchInit', 'PaginateInit', 'Refresh', 'InventoryUpdate', 'GroupsEdit', 'HelpDialog', + 'InventorySummaryHelp', 'BuildTree', 'ClickNode', 'HostsStatusMsg', 'UpdateStatusMsg', 'ViewUpdateStatus', + function($rootScope, $routeParams, Rest, Alert, ProcessErrors, GetBasePath, FormatDate, InventorySummary, GenerateList, ClearScope, + SearchInit, PaginateInit, Refresh, InventoryUpdate, GroupsEdit, HelpDialog, InventorySummaryHelp, BuildTree, ClickNode, + HostsStatusMsg, UpdateStatusMsg, ViewUpdateStatus) { return function(params) { //Build a summary of a given inventory @@ -373,41 +416,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', ' HelpDialog({ defn: InventorySummaryHelp }); } - scope.viewUpdateStatus = function(id) { - var found = false; - var group; - for (var i=0; i < scope.groups.length; i++) { - if (scope.groups[i].id == id) { - found = true; - group = scope.groups[i]; - } - } - if (found) { - if (group.summary_fields.inventory_source.source == "" || group.summary_fields.inventory_source.source == null) { - Alert('Missing Configuration', 'The selected group is not configured for inventory updates. ' + - 'You must first edit the group, provide Source settings, and then run an update.', 'alert-info'); - } - else if (group.summary_fields.inventory_source.status == "" || group.summary_fields.inventory_source.status == null || - group.summary_fields.inventory_source.status == "never updated") { - Alert('No Status Available', 'The inventory update process has not run for the selected group. Start the process by ' + - 'clicking the Update button.', 'alert-info'); - } - else { - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success( function(data, status, headers, config) { - var url = (data.related.current_update) ? data.related.current_update : data.related.last_update; - ShowUpdateStatus({ group_name: data.summary_fields.group.name, - last_update: url }); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' POST returned status: ' + status }); - }); - } - } - } + scope.viewUpdateStatus = function(group_id) { ViewUpdateStatus({ scope: scope, group_id: group_id }) }; // Click on group name scope.GroupsEdit = function(group_id) { diff --git a/awx/ui/static/js/lists/HomeGroups.js b/awx/ui/static/js/lists/HomeGroups.js new file mode 100644 index 0000000000..18eb328cad --- /dev/null +++ b/awx/ui/static/js/lists/HomeGroups.js @@ -0,0 +1,119 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * HomeGroups.js + * + * List view object for Group data model. Used + * on the home tab. + * + */ +angular.module('HomeGroupListDefinition', []) + .value( + 'HomeGroupList', { + + name: 'groups', + iterator: 'group', + editTitle: 'Groups', + index: true, + hover: true, + + fields: { + name: { + key: true, + label: 'Group', + ngClick: "\{\{ 'GroupsEdit(' + group.id + ')' \}\}", + columnClass: 'col-lg-3 col-md3 col-sm-2', + linkTo: "\{\{ '/#/inventories/' + group.inventory + '/groups/?name=' + group.name \}\}" + }, + inventory_name: { + label: 'Inventory', + sourceModel: 'inventory', + sourceField: 'name', + columnClass: 'col-lg-3 col-md3 col-sm-2', + linkTo: "\{\{ '/#/inventories/' + group.inventory \}\}" + }, + failed_hosts: { + label: 'Failed Hosts', + ngHref: "\{\{ group.failed_hosts_link \}\}", + badgeIcon: "\{\{ 'icon-failures-' + group.failed_hosts_class \}\}", + badgeNgHref: "\{\{ group.failed_hosts_link \}\}", + badgePlacement: 'left', + badgeToolTip: "\{\{ group.failed_hosts_tip \}\}", + badgeTipPlacement: 'top', + awToolTip: "\{\{ group.failed_hosts_tip \}\}", + dataPlacement: "top", + searchable: false, + excludeModal: true, + sortField: "hosts_with_active_failures" + }, + status: { + label: 'Status', + ngClick: "viewUpdateStatus(\{\{ group.id \}\})", + searchType: 'select', + badgeIcon: "\{\{ 'icon-cloud-' + group.status_badge_class \}\}", + badgeToolTip: "\{\{ group.status_badge_tooltip \}\}", + awToolTip: "\{\{ group.status_badge_tooltip \}\}", + dataPlacement: 'top', + badgeTipPlacement: 'top', + badgePlacement: 'left', + searchOptions: [ + { name: "failed", value: "failed" }, + { name: "never", value: "never updated" }, + { name: "n/a", value: "none" }, + { name: "successful", value: "successful" }, + { name: "updating", value: "updating" }], + sourceModel: 'inventory_source', + sourceField: 'status' + }, + last_updated: { + label: 'Last
Updated', + sourceModel: 'inventory_source', + sourceField: 'last_updated', + searchable: false, + nosort: false + }, + source: { + label: 'Source', + searchType: 'select', + searchOptions: [ + { name: "ec2", value: "ec2" }, + { name: "none", value: "" }, + { name: "rackspace", value: "rackspace" }], + sourceModel: 'inventory_source', + sourceField: 'source', + searchOnly: true + }, + has_external_source: { + label: 'Has external source?', + searchType: 'in', + searchValue: 'ec2,rackspace', + searchOnly: true, + sourceModel: 'inventory_source', + sourceField: 'source' + }, + has_active_failures: { + label: 'Has failed hosts?', + searchSingleValue: true, + searchType: 'boolean', + searchValue: 'true', + searchOnly: true + }, + last_update_failed: { + label: 'Update failed?', + searchType: 'select', + searchSingleValue: true, + searchValue: 'failed', + searchOnly: true, + sourceModel: 'inventory_source', + sourceField: 'status' + } + }, + + actions: { + + }, + + fieldActions: { + + } + }); diff --git a/awx/ui/static/js/lists/InventorySummary.js b/awx/ui/static/js/lists/InventorySummary.js index 046b1dcad4..fc680356f1 100644 --- a/awx/ui/static/js/lists/InventorySummary.js +++ b/awx/ui/static/js/lists/InventorySummary.js @@ -133,7 +133,8 @@ angular.module('InventorySummaryDefinition', []) mode: 'all', 'class': 'btn-xs btn-primary', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refresh()", + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/JobEvents.js b/awx/ui/static/js/lists/JobEvents.js index 9f8bdd8093..0e9acaffc0 100644 --- a/awx/ui/static/js/lists/JobEvents.js +++ b/awx/ui/static/js/lists/JobEvents.js @@ -92,7 +92,8 @@ angular.module('JobEventsListDefinition', []) ngShow: "job_status == 'pending' || job_status == 'waiting' || job_status == 'running'", 'class': 'btn-xs btn-primary', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refresh()", + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/JobHosts.js b/awx/ui/static/js/lists/JobHosts.js index 8784c14c18..4cfd73c2b7 100644 --- a/awx/ui/static/js/lists/JobHosts.js +++ b/awx/ui/static/js/lists/JobHosts.js @@ -126,7 +126,8 @@ angular.module('JobHostDefinition', []) ngShow: "host_id == null && (job_status == 'pending' || job_status == 'waiting' || job_status == 'running')", 'class': 'btn-xs btn-primary', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refresh()", + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/Jobs.js b/awx/ui/static/js/lists/Jobs.js index 5ebd0e1a61..80ef8b7245 100644 --- a/awx/ui/static/js/lists/Jobs.js +++ b/awx/ui/static/js/lists/Jobs.js @@ -81,7 +81,8 @@ angular.module('JobsListDefinition', []) mode: 'all', 'class': 'btn-xs btn-primary', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refresh()", + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/Projects.js b/awx/ui/static/js/lists/Projects.js index 517e4936eb..64d37e6c91 100644 --- a/awx/ui/static/js/lists/Projects.js +++ b/awx/ui/static/js/lists/Projects.js @@ -78,7 +78,8 @@ angular.module('ProjectsListDefinition', []) mode: 'all', 'class': 'btn-xs btn-primary', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refresh()", + iconSize: 'large' } }, diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js new file mode 100644 index 0000000000..ff17ad1cb4 --- /dev/null +++ b/awx/ui/static/js/lists/Streams.js @@ -0,0 +1,62 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * Streams.js + * List view object for activity stream data model. + * + * + */ +angular.module('StreamListDefinition', []) + .value( + 'StreamList', { + + name: 'activities', + iterator: 'activity', + editTitle: 'Activity Stream', + selectInstructions: '', + index: false, + hover: true, + "class": "table-condensed", + + fields: { + event_time: { + key: true, + label: 'When' + }, + user: { + label: 'Who', + sourceModel: 'user', + sourceField: 'username' + }, + operation: { + label: 'Operation' + }, + description: { + label: 'Description' + } + }, + + actions: { + refresh: { + dataPlacement: 'top', + icon: "icon-refresh", + mode: 'all', + 'class': 'btn-xs btn-primary', + awToolTip: "Refresh the page", + ngClick: "refreshStream()", + iconSize: 'large' + }, + close: { + dataPlacement: 'top', + icon: "icon-arrow-left", + mode: 'all', + 'class': 'btn-xs btn-primary', + awToolTip: "Close Activity Stream view", + ngClick: "closeStream()", + iconSize: 'large' + } + }, + + fieldActions: { + } + }); \ No newline at end of file diff --git a/awx/ui/static/js/widgets/JobStatus.js b/awx/ui/static/js/widgets/JobStatus.js index e8d015ba3d..574ada224d 100644 --- a/awx/ui/static/js/widgets/JobStatus.js +++ b/awx/ui/static/js/widgets/JobStatus.js @@ -15,7 +15,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities']) var scope = $rootScope.$new(); var jobCount, jobFails, inventoryCount, inventoryFails, groupCount, groupFails, hostCount, hostFails; var counts = 0; - var expectedCounts = 8; + var expectedCounts = 6; var target = params.target; if (scope.removeCountReceived) { @@ -25,17 +25,21 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities']) var rowcount = 0; - function makeRow(label, count, fail) { + function makeRow(params) { var html = ''; + var label = params.label; + var link = params.link; + var fail_link = params.fail_link; + var count = params.count; + var fail = params.fail; html += "\n"; - html += "\n"; html += ""; - html += (fail > 0) ? "" + fail + "" : ""; + html += "" + fail + ""; html += "\n"; html += "" - html += (count > 0) ? "" + count + "" : ""; + html += "" + count + ""; html += "\n"; return html; } @@ -57,19 +61,33 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities']) html += "\n"; if (jobCount > 0) { - html += makeRow('Jobs', jobCount, jobFails); - rowcount++; - } - if (inventoryCount > 0) { - html += makeRow('Inventories', inventoryCount, inventoryFails); + html += makeRow({ + label: 'Jobs', + link: '/#/jobs', + count: jobCount, + fail: jobFails, + fail_link: '/#/jobs/?status=failed' + }); rowcount++; } if (groupCount > 0) { - html += makeRow('Groups', groupCount, groupFails); + html += makeRow({ + label: 'Groups', + link: '/#/home/groups', + count: groupCount, + fail: groupFails, + fail_link: '/#/home/groups/?status=failed' + }); rowcount++; } if (hostCount > 0) { - html += makeRow('Hosts', hostCount, hostFails); + html += makeRow({ + label: 'Hosts', + link: '#/home/hosts', + count: hostCount, + fail: hostFails, + fail_link: '/#/home/hosts/?status=failed' + }); rowcount++; } @@ -114,7 +132,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities']) { hdr: 'Error!', msg: 'Failed to get ' + url + '. GET status: ' + status }); }); - url = GetBasePath('inventory') + '?page=1'; + /*url = GetBasePath('inventory') + '?page=1'; Rest.setUrl(url); Rest.get() .success( function(data, status, headers, config) { @@ -136,7 +154,7 @@ angular.module('JobStatusWidget', ['RestServices', 'Utilities']) .error( function(data, status, headers, config) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get ' + url + '. GET status: ' + status }); - }); + });*/ url = GetBasePath('groups') + '?page=1'; Rest.setUrl(url); diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js new file mode 100644 index 0000000000..3210f6a3ca --- /dev/null +++ b/awx/ui/static/js/widgets/Stream.js @@ -0,0 +1,102 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * Stream.js + * + * Activity stream widget that can be called from anywhere + * + */ + +angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', 'SearchHelper', 'PaginateHelper', + 'RefreshHelper', 'ListGenerator', 'StreamWidget']) + + .factory('ShowStream', [ function() { + return function() { + // Slide in the Stream widget + var stream = $('#stream-container'); + stream.css({ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + 'min-height': '100%', + 'background-color': '#FFF' + }); + stream.show('slide', {'direction': 'left'}, {'duration': 500, 'queue': false }); + } + }]) + + .factory('HideStream', [ 'ClearScope', function(ClearScope) { + return function() { + // Remove the stream widget + var stream = $('#stream-container'); + stream.hide('slide', {'direction': 'left'}, {'duration': 500, 'queue': false }); + + // Completely destroy the container so we don't experience random flashes of it later. + // There was some sort weirdness with the tab 'show' causing the stream to slide in when + // a tab was clicked, after the stream had been hidden. Seemed like timing- wait long enough + // before clicking a tab, and it would not happen. + setTimeout( function() { + stream.detach(); + stream.empty(); + stream.unbind(); + }, 500); + } + }]) + + .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', + 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', + function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, + FormatDate, ShowStream, HideStream) { + return function(params) { + + var list = StreamList; + var defaultUrl = $basePath + 'html/event_log.html/'; + var view = GenerateList; + + // Push the current page onto browser histor. If user clicks back button, restore current page without + // stream widget + // window.history.pushState({}, "AnsibleWorks AWX", $location.path()); + + // Add a container for the stream widget + $('#tab-content-container').append('
'); + + // Generate the list + var scope = view.inject(list, { + mode: 'edit', + id: 'stream-content', + breadCrumbs: true, + searchSize: 'col-lg-4' + }); + + scope.closeStream = function() { + HideStream(); + } + + scope.refreshStream = function() { + scope['activities'].splice(10,10); + //scope.search(list.iterator); + } + + if (scope.removePostRefresh) { + scope.removePostRefresh(); + } + scope.removePostRefresh = scope.$on('PostRefresh', function() { + for (var i=0; i < scope['activities'].length; i++) { + // Convert event_time date to local time zone + cDate = new Date(scope['activities'][i].event_time); + scope['activities'][i].event_time = FormatDate(cDate); + // Display username + scope['activities'][i].user = scope.activities[i].summary_fields.user.username; + } + ShowStream(); + }); + + // Initialize search and paginate pieces and load data + SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl }); + PaginateInit({ scope: scope, list: list, url: defaultUrl }); + scope.search(list.iterator); + + } + }]); + \ No newline at end of file diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 3cfa4f9ec0..2746804dd4 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -487,10 +487,10 @@ legend { margin: 10px 0 0 0; } -.page-size { - height: 25px; - font-size: 10.5px; - line-height: normal; +select.page-size { + width: 65px; + height: 24px; + font-size: 10px; } .page-size-label { @@ -642,7 +642,7 @@ input[type="checkbox"].checkbox-no-label { text-align: right; button { - margin-left: 8px; + margin-left: 4px; } } @@ -1374,6 +1374,31 @@ tr td button i { } +/* Activity Stream Widget */ + + #stream-container { + display: none; + } + + #stream-content { + border: 1px solid @grey; + border-radius: 8px; + padding: 8px; + } + + /* + .activity-btn { + padding-left: 2px; + padding-right: 2px; + padding-bottom: 2px; + img { + width: 16px; + height: 16px; + } + } + */ + + /* Large desktop */ @media (min-width: 1200px) { diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index bf39e22598..99d579d1f9 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -109,14 +109,10 @@ angular.module('Utilities',['RestServices', 'Utilities']) } Alert(defaultMsg.hdr, msg); } - else if (status == 401 && data.detail && data.detail == 'Token is expired') { + else if ( (status == 401 && data.detail && data.detail == 'Token is expired') || + (status == 401 && data.detail && data.detail == 'Invalid token') ) { $rootScope.sessionTimer.expireSession(); - window.location = '/#/login'; //resetting location so that we drop search params - } - else if (status == 401 && data.detail && data.detail == 'Invalid token') { - // should this condition be treated as an expired session?? Yes, for now. - $rootScope.sessionTimer.expireSession(); - window.location = '/#/login'; //resetting location so that we drop search params + $location.url('/login'); } else if (data.non_field_errors) { Alert('Error!', data.non_field_errors); diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 49d323504e..2e6b037cae 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -138,6 +138,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html += (btn.ngShow) ? Attr(btn, 'ngShow') : ""; html += (btn.ngHide) ? Attr(btn, 'ngHide') : ""; html += " >"; + html += (btn['img']) ? "" : ""; html += (btn['icon']) ? Attr(btn,'icon') : ""; html += (btn['awRefresh'] && !btn['icon']) ? " " : ""; html += (btn.label) ? " " + btn.label : ""; @@ -595,7 +596,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html += "