diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c49b6ba541..126cf06d35 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -235,6 +235,11 @@ angular.module('ansible', [ function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense) { LoadBasePaths(); + + if ( !(typeof $AnsibleConfig.refresh_rate == 'number' && $AnsibleConfig.refresh_rate >= 3 + && $AnsibleConfig.refresh_rate <= 99) ) { + $AnsibleConfig.refresh_rate = 10; + } $rootScope.breadcrumbs = new Array(); $rootScope.crumbCache = new Array(); @@ -252,6 +257,7 @@ angular.module('ansible', [ } CheckLicense(); } + // Make the correct tab active var base = $location.path().replace(/^\//,'').split('/')[0]; if (base == '') { diff --git a/awx/ui/static/js/config.js b/awx/ui/static/js/config.js index d0e9f37dbd..430042ceac 100644 --- a/awx/ui/static/js/config.js +++ b/awx/ui/static/js/config.js @@ -14,6 +14,9 @@ var $AnsibleConfig = debug_mode: true, // Enable console logging messages + refresh_rate: 10, // Number of seconds before refreshing a page. Integer between 3 and 99, inclusive. + // Used by awRefresh directive to automatically refresh Jobs and Projects pages. + password_strength: 45 // User password strength. Integer between 0 and 100, 100 being impossibly strong. // This value controls progress bar colors: // 0 to password_strength - 15 = red; diff --git a/awx/ui/static/js/controllers/JobEvents.js b/awx/ui/static/js/controllers/JobEvents.js index 24a1ef61dc..95cf4042c4 100644 --- a/awx/ui/static/js/controllers/JobEvents.js +++ b/awx/ui/static/js/controllers/JobEvents.js @@ -141,6 +141,22 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, cDate = new Date(set[i].created); set[i].created = FormatDate(cDate); } + + // need job_status so we can show/hide refresh button + Rest.setUrl(GetBasePath('jobs') + scope.job_id); + Rest.get() + .success( function(data, status, headers, config) { + scope.job_status = data.status; + if (!(data.status == 'pending' || data.status == 'waiting' || data.status == 'running')) { + if ($rootScope.timer) { + clearInterval($rootScope.timer); + } + } + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to get job status for job: ' + scope.job_id + '. GET status: ' + status }); + }); }); SearchInit({ scope: scope, set: 'jobevents', list: list, url: defaultUrl }); diff --git a/awx/ui/static/js/controllers/JobHosts.js b/awx/ui/static/js/controllers/JobHosts.js index 5d45561bcd..d307dab402 100644 --- a/awx/ui/static/js/controllers/JobHosts.js +++ b/awx/ui/static/js/controllers/JobHosts.js @@ -50,7 +50,24 @@ function JobHostSummaryList ($scope, $rootScope, $location, $log, $routeParams, for( var i=0; i < scope.jobhosts.length; i++) { scope.jobhosts[i].host_name = scope.jobhosts[i].summary_fields.host.name; scope.jobhosts[i].status = (scope.jobhosts[i].failed) ? 'error' : 'success'; - } + } + if (scope.host_id == null) { + // need job_status so we can show/hide refresh button + Rest.setUrl(GetBasePath('jobs') + scope.job_id); + Rest.get() + .success( function(data, status, headers, config) { + scope.job_status = data.status; + if (!(data.status == 'pending' || data.status == 'waiting' || data.status == 'running')) { + if ($rootScope.timer) { + clearInterval($rootScope.timer); + } + } + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to get job status for job: ' + scope.job_id + '. GET status: ' + status }); + }); + } }); SearchInit({ scope: scope, set: 'jobhosts', list: list, url: defaultUrl }); diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 6eb0c8c866..1b58a829dc 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -262,6 +262,12 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, scope.playbook_options = null; scope.playbook = null; + function calcRows (content) { + var n = content.match(/\n/g); + var rows = (n) ? n.length : 1; + return (rows > 15) ? 15 : rows; + } + // Retrieve detail record and prepopulate the form Rest.setUrl(defaultUrl + ':id/'); Rest.get({ params: {id: id} }) @@ -336,17 +342,20 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; } } - + // Calc row size of stdout and traceback textarea fields - var n = scope['result_stdout'].match(/\n/g); - var rows = (n) ? n.length : 1; - rows = (rows > 15) ? 15 : rows; - scope['stdout_rows'] = rows; + //var n = scope['result_stdout'].match(/\n/g); + //var rows = (n) ? n.length : 1; + //rows = (rows > 15) ? 15 : rows; + //rows; - n = scope['result_traceback'].match(/\n/g); - var rows = (n) ? n.length : 1; - rows = (rows > 15) ? 15 : rows; - scope['traceback_rows'] = rows; + scope['stdout_rows'] = calcRows(scope['result_stdout']); + + //n = scope['result_traceback'].match(/\n/g); + //var rows = (n) ? n.length : 1; + //rows = (rows > 15) ? 15 : rows; + + scope['traceback_rows'] = calcRows(scope['result_traceback']); LookUpInit({ scope: scope, @@ -461,6 +470,13 @@ function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, scope.status = data.status; scope.result_stdout = data.result_stdout; scope.result_traceback = data.result_traceback; + scope['stdout_rows'] = calcRows(scope['result_stdout']); + scope['traceback_rows'] = calcRows(scope['result_traceback']); + if (!(data.status == 'pending' || data.status == 'waiting' || data.status == 'running')) { + if ($rootScope.timer) { + clearInterval($rootScope.timer); + } + } scope.statusSearchSpin = false; }) .error( function(data, status, headers, config) { diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index b229ff49f6..eb4a952423 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -27,8 +27,11 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, $rootScope.flashMessage = null; var url = (base == 'teams') ? GetBasePath('teams') + $routeParams.team_id + '/projects/' : defaultUrl; - SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 }); + if (mode == 'select') { + SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 }); + } + if (scope.projectsPostRefresh) { scope.projectsPostRefresh(); } diff --git a/awx/ui/static/js/forms/Jobs.js b/awx/ui/static/js/forms/Jobs.js index 3ac4469bb0..c165e85a42 100644 --- a/awx/ui/static/js/forms/Jobs.js +++ b/awx/ui/static/js/forms/Jobs.js @@ -291,12 +291,9 @@ angular.module('JobFormDefinition', []) }, statusActions: { - refresh: { - label: 'Refresh', - icon: 'icon-refresh', - ngClick: "refresh()", - "class": 'btn-sm btn-primary', - awToolTip: 'Refresh job status & output', + refresh: { + awRefresh: true, + ngShow: "(status == 'pending' || status == 'waiting' || status == 'running')", mode: 'all' } } diff --git a/awx/ui/static/js/helpers/Selection.js b/awx/ui/static/js/helpers/Selection.js index 9dc58b657f..4fd1b66f4b 100644 --- a/awx/ui/static/js/helpers/Selection.js +++ b/awx/ui/static/js/helpers/Selection.js @@ -133,22 +133,24 @@ angular.module('SelectionHelper', ['Utilities', 'RestServices']) scope.SelectPostRefreshRemove(); } scope.SelectPostRefreshRemove = scope.$on('PostRefresh', function() { - for (var i=0; i < scope[list.name].length; i++) { - var found = false; - for (var j=0; j < scope.selected.length; j++) { - if (scope.selected[j].id == scope[list.name][i].id) { - found = true; - break; - } - } - if (found) { - scope[list.name][i]['checked'] = '1'; - scope[list.name][i]['success_class'] = 'success'; - } - else { - scope[list.name][i]['checked'] = '0'; - scope[list.name][i]['success_class'] = ''; - } + if (scope[list.name]) { + for (var i=0; i < scope[list.name].length; i++) { + var found = false; + for (var j=0; j < scope.selected.length; j++) { + if (scope.selected[j].id == scope[list.name][i].id) { + found = true; + break; + } + } + if (found) { + scope[list.name][i]['checked'] = '1'; + scope[list.name][i]['success_class'] = 'success'; + } + else { + scope[list.name][i]['checked'] = '0'; + scope[list.name][i]['success_class'] = ''; + } + } } }); } diff --git a/awx/ui/static/js/lists/JobEvents.js b/awx/ui/static/js/lists/JobEvents.js index 0e10901f44..9348afa261 100644 --- a/awx/ui/static/js/lists/JobEvents.js +++ b/awx/ui/static/js/lists/JobEvents.js @@ -72,11 +72,8 @@ angular.module('JobEventsListDefinition', []) actions: { refresh: { - ngClick: "refresh()", - icon: 'icon-refresh', - label: 'Refresh', - awToolTip: 'Refresh the page', - "class": 'btn-sm btn-primary', + awRefresh: true, + ngShow: "job_status == 'pending' || job_status == 'waiting' || job_status == 'running'", mode: 'all' } }, diff --git a/awx/ui/static/js/lists/JobHosts.js b/awx/ui/static/js/lists/JobHosts.js index c2181d65c7..5bb8d84565 100644 --- a/awx/ui/static/js/lists/JobHosts.js +++ b/awx/ui/static/js/lists/JobHosts.js @@ -79,11 +79,8 @@ angular.module('JobHostDefinition', []) actions: { refresh: { - label: 'Refresh', - icon: 'icon-refresh', - ngClick: "refresh()", - "class": 'btn-primary btn-sm', - awToolTip: 'Refresh the page', + awRefresh: true, + ngShow: "host_id == null && (job_status == 'pending' || job_status == 'waiting' || job_status == 'running')", mode: 'all' }, help: { diff --git a/awx/ui/static/js/lists/Jobs.js b/awx/ui/static/js/lists/Jobs.js index 9a284b51e4..eb6138d5d4 100644 --- a/awx/ui/static/js/lists/Jobs.js +++ b/awx/ui/static/js/lists/Jobs.js @@ -59,11 +59,7 @@ angular.module('JobsListDefinition', []) actions: { refresh: { - label: 'Refresh', - "class": 'btn-primary btn-sm', - ngClick: "refreshJob(\{\{ job.id \}\})", - icon: 'icon-refresh', - awToolTip: 'Refresh the page', + awRefresh: true, mode: 'all' } }, diff --git a/awx/ui/static/js/lists/Projects.js b/awx/ui/static/js/lists/Projects.js index aa3d8e9771..1abda9fc2b 100644 --- a/awx/ui/static/js/lists/Projects.js +++ b/awx/ui/static/js/lists/Projects.js @@ -22,7 +22,7 @@ angular.module('ProjectsListDefinition', []) fields: { name: { key: true, - label: 'Name', + label: 'Name' }, description: { label: 'Description', @@ -53,11 +53,7 @@ angular.module('ProjectsListDefinition', []) awToolTip: 'Create a new project' }, refresh: { - label: 'Refresh', - "class": 'btn-primary btn-sm', - ngClick: "refresh(\{\{ job.id \}\})", - icon: 'icon-refresh', - awToolTip: 'Refresh the page', + awRefresh: true, mode: 'all' }, help: { diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 3fd5ce31bb..e20b1765fc 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -145,6 +145,18 @@ td.actions { border-color: #ddd; } +.refresh-grp { + display: inline-block; + margin-left: 10px; + margin-top: 0; + padding: 0; + line-height: normal; + + .refresh-msg { + font-size: 10px; + } +} + .btn-light:hover { color: #333; background-color: #ccc; @@ -392,7 +404,6 @@ legend { .status-actions { display: inline-block; height: 25px; - margin-bottom: 20px; } .status-spin { diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index b5e7b6a4d1..7238d2bf0f 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -154,7 +154,6 @@ angular.module('Utilities',[]) //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. - console.log($rootScope.crumbCache); if (crumb !== null && crumb !== undefined) { var found = false; for (var i=0; i < $rootScope.crumbCache.length; i++) { diff --git a/awx/ui/static/lib/ansible/api-loader.js b/awx/ui/static/lib/ansible/api-loader.js index 4041b77b08..af81c21859 100644 --- a/awx/ui/static/lib/ansible/api-loader.js +++ b/awx/ui/static/lib/ansible/api-loader.js @@ -11,34 +11,45 @@ angular.module('ApiLoader', ['ngCookies']) .factory('LoadBasePaths', ['$http', '$rootScope', '$cookieStore', 'ProcessErrors', function($http, $rootScope, $cookieStore, ProcessErrors) { return function() { - $http.get('/api/') - .success( function(data, status, headers, config) { - var base = data.current_version; - $http.get(base) - .success( function(data, status, headers, config) { - data['base'] = base; - $rootScope['defaultUrls'] = data; - $cookieStore.remove('api'); - $cookieStore.put('api',data); //Preserve in cookie to prevent against - //loss during browser refresh - }) - .error ( function(data, status, headers, config) { - $rootScope['defaultUrls'] = { status: 'error' }; - ProcessErrors(null, data, status, null, - { hdr: 'Error', msg: 'Failed to read ' + base + '. GET status: ' + status }); - }); - }) - .error( function(data, status, headers, config) { - $rootScope['defaultUrls'] = { status: 'error' }; - ProcessErrors(null, data, status, null, - { hdr: 'Error', msg: 'Failed to read /api. GET status: ' + status }); - }); + if (!$rootScope['defaultUrls']) + // if 'defaultUrls', the data used by GetBasePath(), is not in $rootScope, then we need to + // restore it from cookieStore or by querying the API. it goes missing from $rootScope + // when user hits browser refresh + if (!$cookieStore.get('api')) { + // if it's not in cookieStore, then we need to retrieve it from the API + $http.get('/api/') + .success( function(data, status, headers, config) { + var base = data.current_version; + $http.get(base) + .success( function(data, status, headers, config) { + data['base'] = base; + $rootScope['defaultUrls'] = data; + $cookieStore.remove('api'); + $cookieStore.put('api',data); //Preserve in cookie to prevent against + //loss during browser refresh + }) + .error ( function(data, status, headers, config) { + $rootScope['defaultUrls'] = { status: 'error' }; + ProcessErrors(null, data, status, null, + { hdr: 'Error', msg: 'Failed to read ' + base + '. GET status: ' + status }); + }); + }) + .error( function(data, status, headers, config) { + $rootScope['defaultUrls'] = { status: 'error' }; + ProcessErrors(null, data, status, null, + { hdr: 'Error', msg: 'Failed to read /api. GET status: ' + status }); + }); + } + else { + $rootScope['defaultUrls'] = $cookieStore.get('api'); + } } }]) .factory('GetBasePath', ['$rootScope', '$cookieStore', 'LoadBasePaths', function($rootScope, $cookieStore, LoadBasePaths) { return function(set) { + // use /api/v1/ results to construct API URLs. var answer; if ($rootScope['defaultUrls'] == null || $rootScope['defaultUrls'] == undefined) { // browser refresh must have occurred. use what's in session cookie and refresh diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index 640a221b61..8472b95fc6 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -408,6 +408,42 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Hos }); } } + }]) + + // + // awRefresh + // + // Creates a timer to call scope.refresh(iterator) ever N seconds, where + // N is a setting in config.js + // + .directive('awRefresh', [ '$rootScope', function($rootScope) { + return { + link: function(scope, elm, attrs, ctrl) { + function msg() { + var num = '' + scope.refreshCnt; + while (num.length < 2) { + num = '0' + num; + } + return 'Refresh in ' + num + ' sec.'; + } + scope.refreshCnt = $AnsibleConfig.refresh_rate; + scope.refreshMsg = msg(); + if ($rootScope.timer) { + clearInterval($rootScope.timer); + } + $rootScope.timer = setInterval( function() { + scope.refreshCnt--; + if (scope.refreshCnt <= 0) { + scope.refresh(); + scope.refreshCnt = $AnsibleConfig.refresh_rate; + } + scope.refreshMsg = msg(); + if (!scope.$$phase) { + scope.$digest(); + } + }, 1000); + } + } }]); diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index c4e6aa873b..55d693675c 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -10,8 +10,8 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) .factory('GenerateForm', [ '$location', '$cookieStore', '$compile', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column', - 'NavigationLink', 'HelpCollapse', - function($location, $cookieStore, $compile, SearchWidget, PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse) { + 'NavigationLink', 'HelpCollapse', 'Button', + function($location, $cookieStore, $compile, SearchWidget, PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse, Button) { return { setForm: function(form) { @@ -86,6 +86,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) $('.tooltip').each( function(index) { $(this).remove(); }); + $('.popover').each(function(index) { // remove lingering popover
. Seems to be a bug in TB3 RC1 $(this).remove(); @@ -277,7 +278,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) return html; }, - button: function(btn, topOrBottom) { + /*button: function(btn, topOrBottom) { // pass in a button object and get back an html string containing // a
\n"; + html += this.button(act); + } + //html += "\n"; + //html += "
\n"; } html += "
\n"; for (var fld in this.form.statusFields) { @@ -899,7 +895,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) html += "
\n"; } - if (this.form.navigation) { + /* if (this.form.navigation) { html += "
\n"; for (btn in this.form.navigation) { var btn = this.form.navigation[btn]; @@ -908,7 +904,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) } } html += "
\n"; - } + } */ // Start the well if ( this.has('well') ) { @@ -1034,7 +1030,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) html += "
\n"; } - if (this.form.navigation) { + /*if (this.form.navigation) { html += "
\n"; for (btn in this.form.navigation) { var btn = this.form.navigation[btn]; @@ -1043,7 +1039,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) } } html += "
\n"; - } + }*/ if ( this.form.collapse && this.form.collapseMode == options.mode ) { html += "
\n"; diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 7c45fac914..ee2d61a81a 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -93,6 +93,54 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) return " "; } }) + + .factory('Button', ['Attr', function(Attr) { + return function(btn) { + // pass in button object, get back html + var html = ''; + if (btn.awRefresh) { + html += "
" : ""; + html += (btn.label) ? " " + btn.label : ""; + html += " "; + if (btn['awRefresh']) { + html += '{{ refreshMsg }}\n'; + html += "
\n"; + } + return html; + } + }]) .factory('NavigationLink', ['Attr', 'Icon', function(Attr, Icon) { return function(link) { diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js index ae94641d5b..a9ca103069 100644 --- a/awx/ui/static/lib/ansible/list-generator.js +++ b/awx/ui/static/lib/ansible/list-generator.js @@ -9,8 +9,8 @@ angular.module('ListGenerator', ['GeneratorHelpers']) .factory('GenerateList', [ '$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', - 'Column', 'DropDown', 'NavigationLink', - function($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, Column, DropDown, NavigationLink) { + 'Column', 'DropDown', 'NavigationLink', 'Button', + function($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, Column, DropDown, NavigationLink, Button) { return { setList: function(list) { @@ -29,29 +29,7 @@ angular.module('ListGenerator', ['GeneratorHelpers']) $('#lookup-modal').modal('hide'); }, - button: function(btn) { - // pass in button object, get back html - var html = ''; - html += "