From 60a137225aba1e09dd193fe17120ca227ef96aca Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 17 Feb 2020 16:52:08 -0500 Subject: [PATCH 01/18] Changes how the jobs list reacts to socket messages. We now only make targeted GET requests for new rows. We use the available information in the socket message to update the relevant row (if visible in the list). --- .../features/jobs/jobsList.controller.js | 104 +++++++++++++----- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js index 2a7225de6a..986ad12f8e 100644 --- a/awx/ui/client/features/jobs/jobsList.controller.js +++ b/awx/ui/client/features/jobs/jobsList.controller.js @@ -25,15 +25,15 @@ function ListJobsController ( vm.strings = strings; + let newJobs = []; + // smart-search const name = 'jobs'; const iterator = 'job'; let paginateQuerySet = {}; let launchModalOpen = false; - let refreshAfterLaunchClose = false; - let pendingRefresh = false; - let refreshTimerRunning = false; + let newJobsTimerRunning = false; vm.searchBasePath = SearchBasePath; @@ -104,23 +104,44 @@ function ListJobsController ( $scope.$emit('updateCount', vm.job_dataset.count, 'jobs'); }); - $scope.$on('ws-jobs', () => { - if (!launchModalOpen) { - if (!refreshTimerRunning) { - refreshJobs(); - } else { - pendingRefresh = true; + const canAddRowsDynamically = () => { + const orderByValue = _.get($state.params, 'job_search.order_by'); + const pageValue = _.get($state.params, 'job_search.page'); + const idInValue = _.get($state.params, 'job_search.id__in'); + + return (!idInValue && (!pageValue || pageValue === '1') + && (orderByValue === '-finished' || orderByValue === '-started')); + }; + + const updateJobRow = (msg) => { + // Loop across the jobs currently shown and update the row + // if it exists + for (let i = 0; i < vm.jobs.length; i++) { + if (vm.jobs[i].id === msg.unified_job_id) { + // Update the job status. + // If/when we get more info in the message we can + // update those fields here as well. + vm.jobs[i].status = msg.status; + i = vm.jobs.length; } - } else { - refreshAfterLaunchClose = true; + } + }; + + $scope.$on('ws-jobs', (e, msg) => { + if (msg.status === 'pending' && canAddRowsDynamically()) { + newJobs.push(msg.unified_job_id); + if (!launchModalOpen && !newJobsTimerRunning) { + fetchNewJobs(); + } + } else if (!newJobs.includes(msg.unified_job_id)) { + updateJobRow(msg); } }); $scope.$on('launchModalOpen', (evt, isOpen) => { evt.stopPropagation(); - if (!isOpen && refreshAfterLaunchClose) { - refreshAfterLaunchClose = false; - refreshJobs(); + if (!isOpen && newJobs.length > 0) { + fetchNewJobs(); } launchModalOpen = isOpen; }); @@ -289,22 +310,49 @@ function ListJobsController ( }); }; - function refreshJobs () { - qs.search(SearchBasePath, $state.params.job_search, { 'X-WS-Session-Quiet': true }) + const fetchNewJobs = () => { + newJobsTimerRunning = true; + const newJobIdsFilter = newJobs.join(','); + newJobs = []; + const newJobsSearchParams = Object.assign({}, $state.params.job_search); + newJobsSearchParams.count_disabled = 1; + newJobsSearchParams.id__in = newJobIdsFilter; + delete newJobsSearchParams.page_size; + const stringifiedSearchParams = qs.encodeQueryset(newJobsSearchParams, false); + Rest.setUrl(`${vm.searchBasePath}${stringifiedSearchParams}`); + Rest.get() .then(({ data }) => { - vm.jobs = data.results; - vm.job_dataset = data; + vm.job_dataset.count += data.results.length; + const pageSize = parseInt($state.params.job_search.page_size, 10) || 20; + const joinedJobs = data.results.concat(vm.jobs); + vm.jobs = joinedJobs.length > pageSize + ? joinedJobs.slice(0, pageSize) + : joinedJobs; + $timeout(() => { + if (canAddRowsDynamically()) { + if (newJobs.length > 0 && !launchModalOpen) { + fetchNewJobs(); + } else { + newJobsTimerRunning = false; + } + } else { + // Bail out - one of [order_by, page, id__in] params has changed since we + // received these new job messages + newJobs = []; + newJobsTimerRunning = false; + } + }, 5000); + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: strings.get('error.HEADER'), + msg: strings.get('error.CALL', { + path: `${vm.searchBasePath}${stringifiedSearchParams}`, + status + }) + }); }); - pendingRefresh = false; - refreshTimerRunning = true; - $timeout(() => { - if (pendingRefresh) { - refreshJobs(); - } else { - refreshTimerRunning = false; - } - }, 5000); - } + }; vm.isCollapsed = true; From 98f5525d289e9d94f046c8953d67d72ac63e1798 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Fri, 21 Feb 2020 14:02:32 -0500 Subject: [PATCH 02/18] added unified job template ID to websocket emits --- awx/main/models/unified_jobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 666cf8c6bf..062e6a0854 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1220,10 +1220,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status_data['instance_group_name'] = None status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' + status_data['unified_job_template_id'] = self.unified_job_template.id emit_channel_notification('jobs-status_changed', status_data) if self.spawned_by_workflow: status_data['group_name'] = "workflow_events" + status_data['workflow_job_template_id'] = self.unified_job_template.id emit_channel_notification('workflow_events-' + str(self.workflow_job_id), status_data) except IOError: # includes socket errors logger.exception('%s failed to emit channel msg about status change', self.log_format) From 179c62e2f3079a84b026902accd3d353a7ceda7b Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 20 Feb 2020 16:54:51 -0500 Subject: [PATCH 03/18] Stop making rest calls for project sync updates on projects list --- awx/ui/client/features/projects/projectsList.controller.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awx/ui/client/features/projects/projectsList.controller.js b/awx/ui/client/features/projects/projectsList.controller.js index f90597ac98..1997361c4d 100644 --- a/awx/ui/client/features/projects/projectsList.controller.js +++ b/awx/ui/client/features/projects/projectsList.controller.js @@ -113,9 +113,7 @@ function projectsListController ( // And we found the affected project $log.debug(`Received event for project: ${project.name}`); $log.debug(`Status changed to: ${data.status}`); - if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { - reloadList(); - } else { + if (!(data.status === 'successful' || data.status === 'failed' || data.status === 'canceled')) { project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); } project.status = data.status; From e75f7b0beb48a34ea949c3adb00d90c0dbf4f250 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 20 Feb 2020 17:29:34 -0500 Subject: [PATCH 04/18] Stop making rest calls for inventory source sync updates on inventory sources list --- .../sources/list/sources-list.controller.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js index e1ba3dfd75..5efa694624 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js @@ -42,21 +42,7 @@ $scope.$on(`ws-jobs`, function(e, data){ inventory_source = Find({ list: $scope.inventory_sources, key: 'id', val: data.inventory_source_id }); - if (inventory_source === undefined || inventory_source === null) { - inventory_source = {}; - } - - if(data.status === 'failed' || data.status === 'successful'){ - let path = GetBasePath('inventory') + $stateParams.inventory_id + '/inventory_sources'; - - qs.search(path, $state.params[`${list.iterator}_search`]) - .then((searchResponse)=> { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - _.forEach($scope[list.name], buildStatusIndicators); - optionsRequestDataProcessing(); - }); - } else { + if (inventory_source) { var status = GetSyncStatusMsg({ status: data.status }); From 51a6194b8dc1e696204c9988d444606d19e3d22c Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 24 Feb 2020 11:08:39 -0500 Subject: [PATCH 05/18] Removes logic performing GET requests on `api/v2/templates` whenever a job status update message comes across the websocket. We now use data exclusively from the websocket to update the UI. --- .../templates/templatesList.controller.js | 61 +++++++++---------- .../smart-status/smart-status.controller.js | 4 +- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/awx/ui/client/features/templates/templatesList.controller.js b/awx/ui/client/features/templates/templatesList.controller.js index 8c95aa745a..8481135d1e 100644 --- a/awx/ui/client/features/templates/templatesList.controller.js +++ b/awx/ui/client/features/templates/templatesList.controller.js @@ -24,7 +24,6 @@ function ListTemplatesController( qs, GetBasePath, ngToast, - $timeout ) { const vm = this || {}; const [jobTemplate, workflowTemplate] = resolvedModels; @@ -32,10 +31,6 @@ function ListTemplatesController( const choices = workflowTemplate.options('actions.GET.type.choices') .concat(jobTemplate.options('actions.GET.type.choices')); - let launchModalOpen = false; - let refreshAfterLaunchClose = false; - let pendingRefresh = false; - let refreshTimerRunning = false; let paginateQuerySet = {}; vm.strings = strings; @@ -120,25 +115,35 @@ function ListTemplatesController( setToolbarSort(); }, true); - $scope.$on(`ws-jobs`, () => { - if (!launchModalOpen) { - if (!refreshTimerRunning) { - refreshTemplates(); - } else { - pendingRefresh = true; - } - } else { - refreshAfterLaunchClose = true; - } - }); + $scope.$on(`ws-jobs`, (e, msg) => { + if (msg.unified_job_template_id && vm.templates) { + const template = vm.templates.find((t) => t.id === msg.unified_job_template_id); + if (template) { + if (msg.status === 'pending') { + // This is a new job - add it to the front of the + // recent_jobs array + if (template.summary_fields.recent_jobs.length === 10) { + template.summary_fields.recent_jobs.pop(); + } - $scope.$on('launchModalOpen', (evt, isOpen) => { - evt.stopPropagation(); - if (!isOpen && refreshAfterLaunchClose) { - refreshAfterLaunchClose = false; - refreshTemplates(); + template.summary_fields.recent_jobs.unshift({ + id: msg.unified_job_id, + status: msg.status, + type: msg.type + }); + } else { + // This is an update to an existing job. Check to see + // if we have it in our array of recent_jobs + for (let i=0; i { @@ -265,15 +270,6 @@ function ListTemplatesController( vm.templates = vm.dataset.results; }) .finally(() => Wait('stop')); - pendingRefresh = false; - refreshTimerRunning = true; - $timeout(() => { - if (pendingRefresh) { - refreshTemplates(); - } else { - refreshTimerRunning = false; - } - }, 5000); } function createErrorHandler(path, action) { @@ -483,8 +479,7 @@ ListTemplatesController.$inject = [ 'Wait', 'QuerySet', 'GetBasePath', - 'ngToast', - '$timeout' + 'ngToast' ]; export default ListTemplatesController; diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index a974e31ee3..1e07342d58 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -90,9 +90,9 @@ export default ['$scope', '$filter', 'i18n', 'JobsStrings', $scope.sparkArray = sparkData; $scope.placeholders = new Array(10 - sparkData.length); } - $scope.$watchCollection('jobs', function(){ + $scope.$watch('jobs', function(){ init(); - }); + }, true); }]; From 1fe28463da27b128d54f788ec55bb71e8d6c167b Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Mon, 24 Feb 2020 11:53:49 -0500 Subject: [PATCH 06/18] added finished job timestamp to websocket emit --- awx/main/models/unified_jobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 062e6a0854..f278435636 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1218,6 +1218,8 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status_data['instance_group_name'] = self.instance_group.name else: status_data['instance_group_name'] = None + elif status in ['successful', 'failed', 'cancelled', 'failed']: + status_data['finished'] = self.finished status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' status_data['unified_job_template_id'] = self.unified_job_template.id From a6cd32522f10cad406cf45311a5d01d7ed8e92b9 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 24 Feb 2020 11:32:11 -0500 Subject: [PATCH 07/18] Removes GET requests in response to websocket messages on the org job templates list. List is solely updated based on data from the websocket messages. --- .../organizations-job-templates.controller.js | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js index d01049ed21..ff67fce559 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js @@ -4,25 +4,41 @@ * All Rights Reserved *************************************************/ -export default ['$scope', '$rootScope', - '$stateParams', 'Rest', 'ProcessErrors', - 'GetBasePath', 'Wait', - '$state', 'OrgJobTemplateList', 'OrgJobTemplateDataset', 'QuerySet', - function($scope, $rootScope, - $stateParams, Rest, ProcessErrors, - GetBasePath, Wait, - $state, OrgJobTemplateList, Dataset, qs) { +export default ['$scope', '$stateParams', 'Rest', 'GetBasePath', '$state', 'OrgJobTemplateList', 'OrgJobTemplateDataset', + function($scope, $stateParams, Rest, GetBasePath, $state, OrgJobTemplateList, Dataset) { var list = OrgJobTemplateList, orgBase = GetBasePath('organizations'); - $scope.$on(`ws-jobs`, function () { - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(function(searchResponse) { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); + $scope.$on(`ws-jobs`, function (e, msg) { + if (msg.unified_job_template_id && $scope[list.name]) { + const template = $scope[list.name].find((t) => t.id === msg.unified_job_template_id); + if (template) { + if (msg.status === 'pending') { + // This is a new job - add it to the front of the + // recent_jobs array + if (template.summary_fields.recent_jobs.length === 10) { + template.summary_fields.recent_jobs.pop(); + } + + template.summary_fields.recent_jobs.unshift({ + id: msg.unified_job_id, + status: msg.status, + type: msg.type + }); + } else { + // This is an update to an existing job. Check to see + // if we have it in our array of recent_jobs + for (let i=0; i Date: Mon, 24 Feb 2020 14:23:38 -0500 Subject: [PATCH 08/18] fixed the spelling of cancelled to be canceled, note to us later -> we need to stick with a single spelling of the word --- awx/main/models/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f278435636..9f4df503e7 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1218,7 +1218,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status_data['instance_group_name'] = self.instance_group.name else: status_data['instance_group_name'] = None - elif status in ['successful', 'failed', 'cancelled', 'failed']: + elif status in ['successful', 'failed', 'canceled']: status_data['finished'] = self.finished status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' From 3234f246db186d52722cde6990705efa5a8f17de Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 24 Feb 2020 12:28:46 -0500 Subject: [PATCH 09/18] Consume finished timestamp from websocket message when it's available to update the relevant row in the templates list. --- awx/ui/client/features/templates/templatesList.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/ui/client/features/templates/templatesList.controller.js b/awx/ui/client/features/templates/templatesList.controller.js index 8481135d1e..2500a78816 100644 --- a/awx/ui/client/features/templates/templatesList.controller.js +++ b/awx/ui/client/features/templates/templatesList.controller.js @@ -138,6 +138,10 @@ function ListTemplatesController( const recentJob = template.summary_fields.recent_jobs[i]; if (recentJob.id === msg.unified_job_id) { recentJob.status = msg.status; + if (msg.finished) { + recentJob.finished = msg.finished; + template.last_job_run = msg.finished; + } break; } }; From db43341f96387d7e06874254fdfe4d8bfcf1399c Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 24 Feb 2020 16:41:35 -0500 Subject: [PATCH 10/18] Consume finished timestamp from websocket message and update the relevant job row. Also adds logic to attempt to re-order the list when the sort order is -finished since we have enough information client-side to do that. --- .../client/features/jobs/jobsList.controller.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js index 986ad12f8e..5720a53b80 100644 --- a/awx/ui/client/features/jobs/jobsList.controller.js +++ b/awx/ui/client/features/jobs/jobsList.controller.js @@ -119,10 +119,19 @@ function ListJobsController ( for (let i = 0; i < vm.jobs.length; i++) { if (vm.jobs[i].id === msg.unified_job_id) { // Update the job status. - // If/when we get more info in the message we can - // update those fields here as well. vm.jobs[i].status = msg.status; - i = vm.jobs.length; + if (msg.finished) { + vm.jobs[i].finished = msg.finished; + const orderByValue = _.get($state.params, 'job_search.order_by'); + if (orderByValue === '-finished') { + // Attempt to sort the rows in the list by their finish + // timestamp in descending order + vm.jobs.sort((a, b) => + (!b.finished) - (!a.finished) + || new Date(b.finished) - new Date(a.finished)); + } + } + break; } } }; From d5dd3c521fde85c55254cc15bb879653ebd16f51 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 24 Feb 2020 16:49:27 -0500 Subject: [PATCH 11/18] Consume finished timestamp on org templates list when available via websocket message --- .../controllers/organizations-job-templates.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js index ff67fce559..61c807b25e 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js @@ -33,6 +33,10 @@ export default ['$scope', '$stateParams', 'Rest', 'GetBasePath', '$state', 'OrgJ const recentJob = template.summary_fields.recent_jobs[i]; if (recentJob.id === msg.unified_job_id) { recentJob.status = msg.status; + if (msg.finished) { + recentJob.finished = msg.finished; + template.last_job_run = msg.finished; + } break; } }; From b09ac716476c29739faf7b6e240f0409bfac7572 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 25 Feb 2020 18:14:31 -0500 Subject: [PATCH 12/18] Trims down GET requests made on the dashboard in response to websocket messages --- .../job-templates-list.directive.js | 4 +- awx/ui/client/src/home/home.controller.js | 154 ++++++++++++++---- 2 files changed, 121 insertions(+), 37 deletions(-) diff --git a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js index 1673d56d53..b1ba95a73d 100644 --- a/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js @@ -11,7 +11,7 @@ export default templateUrl: templateUrl('home/dashboard/lists/job-templates/job-templates-list') }; - function link(scope, element, attr) { + function link(scope) { scope.$watch("data", function(data) { if (data) { @@ -22,7 +22,7 @@ export default scope.noJobTemplates = true; } } - }); + }, true); scope.canAddJobTemplate = false; let url = GetBasePath('job_templates'); diff --git a/awx/ui/client/src/home/home.controller.js b/awx/ui/client/src/home/home.controller.js index f964d9d7ef..dece46ab1c 100644 --- a/awx/ui/client/src/home/home.controller.js +++ b/awx/ui/client/src/home/home.controller.js @@ -12,18 +12,64 @@ export default ['$scope','Wait', '$timeout', 'i18n', var dataCount = 0; let launchModalOpen = false; let refreshAfterLaunchClose = false; - let pendingRefresh = false; - let refreshTimerRunning = false; + let pendingDashboardRefresh = false; + let dashboardTimerRunning = false; + let newJobsTimerRunning = false; + let newTemplatesTimerRunning = false; + let newJobs = []; + let newTemplates =[]; - $scope.$on('ws-jobs', function () { - if (!launchModalOpen) { - if (!refreshTimerRunning) { - refreshLists(); + $scope.$on('ws-jobs', function (e, msg) { + if (msg.status === 'successful' || msg.status === 'failed' || msg.status === 'canceled') { + newJobs.push(msg.unified_job_id); + if (!newJobsTimerRunning) { + fetchNewJobs(); + } + if (!launchModalOpen) { + if (!dashboardTimerRunning) { + fetchDashboardData(); + } else { + pendingDashboardRefresh = true; + } } else { - pendingRefresh = true; + refreshAfterLaunchClose = true; + } + } + + const template = $scope.dashboardJobTemplatesListData.find((t) => t.id === msg.unified_job_template_id); + if (template) { + if (msg.status === 'pending') { + if (template.summary_fields.recent_jobs.length === 10) { + template.summary_fields.recent_jobs.pop(); + } + + template.summary_fields.recent_jobs.unshift({ + id: msg.unified_job_id, + status: msg.status, + type: msg.type + }); + } else { + for (let i=0; i new Date(b.last_job_run) - new Date(a.last_job_run)); + } } } else { - refreshAfterLaunchClose = true; + newTemplates.push(msg.unified_job_template_id); + if (!launchModalOpen && !newTemplatesTimerRunning) { + fetchNewTemplates(); + } } }); @@ -31,7 +77,10 @@ export default ['$scope','Wait', '$timeout', 'i18n', evt.stopPropagation(); if (!isOpen && refreshAfterLaunchClose) { refreshAfterLaunchClose = false; - refreshLists(); + fetchDashboardData(); + if (newTemplates.length > 0) { + fetchNewTemplates(); + } } launchModalOpen = isOpen; }); @@ -75,7 +124,7 @@ export default ['$scope','Wait', '$timeout', 'i18n', $scope.$emit('dashboardDataLoadComplete'); }); - function refreshLists () { + const fetchDashboardData = () => { Rest.setUrl(GetBasePath('dashboard')); Rest.get() .then(({data}) => { @@ -85,25 +134,6 @@ export default ['$scope','Wait', '$timeout', 'i18n', ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard host graph data: ${status}`) }); }); - Rest.setUrl(GetBasePath("unified_jobs") + "?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job&count_disabled=1"); - Rest.setHeader({'X-WS-Session-Quiet': true}); - Rest.get() - .then(({data}) => { - $scope.dashboardJobsListData = data.results; - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard jobs list: ${status}`) }); - }); - - Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template&count_disabled=1"); - Rest.get() - .then(({data}) => { - $scope.dashboardJobTemplatesListData = data.results; - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard jobs list: ${status}`) }); - }); - if ($scope.graphData) { Rest.setUrl(`${GetBasePath('dashboard')}graphs/jobs/?period=${$scope.graphData.period}&job_type=${$scope.graphData.jobType}`); Rest.setHeader({'X-WS-Session-Quiet': true}); @@ -119,16 +149,70 @@ export default ['$scope','Wait', '$timeout', 'i18n', }); } - pendingRefresh = false; - refreshTimerRunning = true; + pendingDashboardRefresh = false; + dashboardTimerRunning = true; $timeout(() => { - if (pendingRefresh) { - refreshLists(); + if (pendingDashboardRefresh) { + fetchDashboardData(); } else { - refreshTimerRunning = false; + dashboardTimerRunning = false; } }, 5000); - } + }; + + const fetchNewJobs = () => { + newJobsTimerRunning = true; + const newJobIdsFilter = newJobs.join(','); + newJobs = []; + Rest.setUrl(`${GetBasePath("unified_jobs")}?id__in=${newJobIdsFilter}&order_by=-finished&finished__isnull=false&type=workflow_job,job&count_disabled=1`); + Rest.get() + .then(({ data }) => { + const joinedJobs = data.results.concat($scope.dashboardJobsListData); + $scope.dashboardJobsListData = joinedJobs.length > 5 + ? joinedJobs.slice(0, 5) + : joinedJobs; + $timeout(() => { + if (newJobs.length > 0) { + fetchNewJobs(); + } else { + newJobsTimerRunning = false; + } + }, 5000); + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n._(`Failed to get new jobs for dashboard: ${status}`) + }); + }); + }; + + const fetchNewTemplates = () => { + newTemplatesTimerRunning = true; + const newTemplateIdsFilter = newTemplates.join(','); + newTemplates = []; + Rest.setUrl(`${GetBasePath("unified_job_templates")}?id__in=${newTemplateIdsFilter}&order_by=-last_job_run&last_job_run__isnull=false&type=workflow_job_template,job_template&count_disabled=1"`); + Rest.get() + .then(({ data }) => { + const joinedTemplates = data.results.concat($scope.dashboardJobTemplatesListData).sort((a, b) => new Date(b.last_job_run) - new Date(a.last_job_run)); + $scope.dashboardJobTemplatesListData = joinedTemplates.length > 5 + ? joinedTemplates.slice(0, 5) + : joinedTemplates; + $timeout(() => { + if (newTemplates.length > 0 && !launchModalOpen) { + fetchNewTemplates(); + } else { + newTemplatesTimerRunning = false; + } + }, 5000); + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n._(`Failed to get new templates for dashboard: ${status}`) + }); + }); + }; Wait('start'); Rest.setUrl(GetBasePath('dashboard')); From 48a615231bdd5df540b2c111f1dbfca51b11c39d Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 26 Feb 2020 10:15:24 -0500 Subject: [PATCH 13/18] Fix jshint errors --- awx/ui/client/src/home/home.controller.js | 180 +++++++++--------- .../organizations-job-templates.controller.js | 2 +- 2 files changed, 90 insertions(+), 92 deletions(-) diff --git a/awx/ui/client/src/home/home.controller.js b/awx/ui/client/src/home/home.controller.js index dece46ab1c..2a0228c124 100644 --- a/awx/ui/client/src/home/home.controller.js +++ b/awx/ui/client/src/home/home.controller.js @@ -19,6 +19,94 @@ export default ['$scope','Wait', '$timeout', 'i18n', let newJobs = []; let newTemplates =[]; + const fetchDashboardData = () => { + Rest.setUrl(GetBasePath('dashboard')); + Rest.get() + .then(({data}) => { + $scope.dashboardData = data; + }) + .catch(({data, status}) => { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard host graph data: ${status}`) }); + }); + + if ($scope.graphData) { + Rest.setUrl(`${GetBasePath('dashboard')}graphs/jobs/?period=${$scope.graphData.period}&job_type=${$scope.graphData.jobType}`); + Rest.setHeader({'X-WS-Session-Quiet': true}); + Rest.get() + .then(function(value) { + if($scope.graphData.status === "successful" || $scope.graphData.status === "failed"){ + delete value.data.jobs[$scope.graphData.status]; + } + $scope.graphData.jobStatus = value.data; + }) + .catch(function({data, status}) { + ProcessErrors(null, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard graph data: ${status}`)}); + }); + } + + pendingDashboardRefresh = false; + dashboardTimerRunning = true; + $timeout(() => { + if (pendingDashboardRefresh) { + fetchDashboardData(); + } else { + dashboardTimerRunning = false; + } + }, 5000); + }; + + const fetchNewJobs = () => { + newJobsTimerRunning = true; + const newJobIdsFilter = newJobs.join(','); + newJobs = []; + Rest.setUrl(`${GetBasePath("unified_jobs")}?id__in=${newJobIdsFilter}&order_by=-finished&finished__isnull=false&type=workflow_job,job&count_disabled=1`); + Rest.get() + .then(({ data }) => { + const joinedJobs = data.results.concat($scope.dashboardJobsListData); + $scope.dashboardJobsListData = + joinedJobs.length > 5 ? joinedJobs.slice(0, 5) : joinedJobs; + $timeout(() => { + if (newJobs.length > 0) { + fetchNewJobs(); + } else { + newJobsTimerRunning = false; + } + }, 5000); + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n._(`Failed to get new jobs for dashboard: ${status}`) + }); + }); + }; + + const fetchNewTemplates = () => { + newTemplatesTimerRunning = true; + const newTemplateIdsFilter = newTemplates.join(','); + newTemplates = []; + Rest.setUrl(`${GetBasePath("unified_job_templates")}?id__in=${newTemplateIdsFilter}&order_by=-last_job_run&last_job_run__isnull=false&type=workflow_job_template,job_template&count_disabled=1"`); + Rest.get() + .then(({ data }) => { + const joinedTemplates = data.results.concat($scope.dashboardJobTemplatesListData).sort((a, b) => new Date(b.last_job_run) - new Date(a.last_job_run)); + $scope.dashboardJobTemplatesListData = + joinedTemplates.length > 5 ? joinedTemplates.slice(0, 5) : joinedTemplates; + $timeout(() => { + if (newTemplates.length > 0 && !launchModalOpen) { + fetchNewTemplates(); + } else { + newTemplatesTimerRunning = false; + } + }, 5000); + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n._(`Failed to get new templates for dashboard: ${status}`) + }); + }); + }; + $scope.$on('ws-jobs', function (e, msg) { if (msg.status === 'successful' || msg.status === 'failed' || msg.status === 'canceled') { newJobs.push(msg.unified_job_id); @@ -59,7 +147,7 @@ export default ['$scope','Wait', '$timeout', 'i18n', } break; } - }; + } if (msg.status === 'successful' || msg.status === 'failed' || msg.status === 'canceled') { $scope.dashboardJobTemplatesListData.sort((a, b) => new Date(b.last_job_run) - new Date(a.last_job_run)); @@ -124,96 +212,6 @@ export default ['$scope','Wait', '$timeout', 'i18n', $scope.$emit('dashboardDataLoadComplete'); }); - const fetchDashboardData = () => { - Rest.setUrl(GetBasePath('dashboard')); - Rest.get() - .then(({data}) => { - $scope.dashboardData = data; - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard host graph data: ${status}`) }); - }); - - if ($scope.graphData) { - Rest.setUrl(`${GetBasePath('dashboard')}graphs/jobs/?period=${$scope.graphData.period}&job_type=${$scope.graphData.jobType}`); - Rest.setHeader({'X-WS-Session-Quiet': true}); - Rest.get() - .then(function(value) { - if($scope.graphData.status === "successful" || $scope.graphData.status === "failed"){ - delete value.data.jobs[$scope.graphData.status]; - } - $scope.graphData.jobStatus = value.data; - }) - .catch(function({data, status}) { - ProcessErrors(null, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard graph data: ${status}`)}); - }); - } - - pendingDashboardRefresh = false; - dashboardTimerRunning = true; - $timeout(() => { - if (pendingDashboardRefresh) { - fetchDashboardData(); - } else { - dashboardTimerRunning = false; - } - }, 5000); - }; - - const fetchNewJobs = () => { - newJobsTimerRunning = true; - const newJobIdsFilter = newJobs.join(','); - newJobs = []; - Rest.setUrl(`${GetBasePath("unified_jobs")}?id__in=${newJobIdsFilter}&order_by=-finished&finished__isnull=false&type=workflow_job,job&count_disabled=1`); - Rest.get() - .then(({ data }) => { - const joinedJobs = data.results.concat($scope.dashboardJobsListData); - $scope.dashboardJobsListData = joinedJobs.length > 5 - ? joinedJobs.slice(0, 5) - : joinedJobs; - $timeout(() => { - if (newJobs.length > 0) { - fetchNewJobs(); - } else { - newJobsTimerRunning = false; - } - }, 5000); - }) - .catch(({ data, status }) => { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n._(`Failed to get new jobs for dashboard: ${status}`) - }); - }); - }; - - const fetchNewTemplates = () => { - newTemplatesTimerRunning = true; - const newTemplateIdsFilter = newTemplates.join(','); - newTemplates = []; - Rest.setUrl(`${GetBasePath("unified_job_templates")}?id__in=${newTemplateIdsFilter}&order_by=-last_job_run&last_job_run__isnull=false&type=workflow_job_template,job_template&count_disabled=1"`); - Rest.get() - .then(({ data }) => { - const joinedTemplates = data.results.concat($scope.dashboardJobTemplatesListData).sort((a, b) => new Date(b.last_job_run) - new Date(a.last_job_run)); - $scope.dashboardJobTemplatesListData = joinedTemplates.length > 5 - ? joinedTemplates.slice(0, 5) - : joinedTemplates; - $timeout(() => { - if (newTemplates.length > 0 && !launchModalOpen) { - fetchNewTemplates(); - } else { - newTemplatesTimerRunning = false; - } - }, 5000); - }) - .catch(({ data, status }) => { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n._(`Failed to get new templates for dashboard: ${status}`) - }); - }); - }; - Wait('start'); Rest.setUrl(GetBasePath('dashboard')); Rest.get() diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js index 61c807b25e..5d8cacb915 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-job-templates.controller.js @@ -39,7 +39,7 @@ export default ['$scope', '$stateParams', 'Rest', 'GetBasePath', '$state', 'OrgJ } break; } - }; + } } } } From d3fa34c665518cc6a3310d98f7f3c95a1f55c46a Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 26 Feb 2020 11:30:49 -0500 Subject: [PATCH 14/18] Remove tooltip update when job finishes. This will be handled later down the line by buildTooltips(). --- awx/ui/client/features/projects/projectsList.controller.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx/ui/client/features/projects/projectsList.controller.js b/awx/ui/client/features/projects/projectsList.controller.js index 1997361c4d..df06920f60 100644 --- a/awx/ui/client/features/projects/projectsList.controller.js +++ b/awx/ui/client/features/projects/projectsList.controller.js @@ -113,9 +113,6 @@ function projectsListController ( // And we found the affected project $log.debug(`Received event for project: ${project.name}`); $log.debug(`Status changed to: ${data.status}`); - if (!(data.status === 'successful' || data.status === 'failed' || data.status === 'canceled')) { - project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); - } project.status = data.status; buildTooltips(project); } From e11ff69f3e03e0b9b1d7aa205d577a500eb1e336 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Fri, 28 Feb 2020 15:40:51 -0500 Subject: [PATCH 15/18] Added in check for the unified_job_template_id attribute to be present and populated in the object. For anyone reading this later, know that AdHocCommands still have unified_job_template and unified_job_template_id fields, they are just nonetypes because they don't get used by the AdHocCommand objects. Which means you have to actually get the object, not just check that it's there, to use it the way I am in this change. --- awx/main/models/unified_jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 9f4df503e7..b816c49f26 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1222,7 +1222,8 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status_data['finished'] = self.finished status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' - status_data['unified_job_template_id'] = self.unified_job_template.id + if getattr(self, 'unified_job_template_id', None): + status_data['unified_job_template_id'] = self.unified_job_template_id emit_channel_notification('jobs-status_changed', status_data) if self.spawned_by_workflow: From 10b5a107289b014e488aee0279af411c462f49bb Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 2 Mar 2020 14:22:51 -0500 Subject: [PATCH 16/18] Jobs that error should trigger us to to update the Recent Jobs list along with successful/failed/canceled --- awx/ui/client/src/home/home.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/home/home.controller.js b/awx/ui/client/src/home/home.controller.js index 2a0228c124..cef2324fd0 100644 --- a/awx/ui/client/src/home/home.controller.js +++ b/awx/ui/client/src/home/home.controller.js @@ -108,7 +108,7 @@ export default ['$scope','Wait', '$timeout', 'i18n', }; $scope.$on('ws-jobs', function (e, msg) { - if (msg.status === 'successful' || msg.status === 'failed' || msg.status === 'canceled') { + if (msg.status === 'successful' || msg.status === 'failed' || msg.status === 'canceled' || msg.status === 'error') { newJobs.push(msg.unified_job_id); if (!newJobsTimerRunning) { fetchNewJobs(); From 07752f48f6d477d19a6454cd3558d7f8389d2189 Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Mon, 2 Mar 2020 16:41:50 -0500 Subject: [PATCH 17/18] formatted finished time to match microsecond expected output as is in the API. --- awx/main/models/unified_jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index b816c49f26..168f28a25a 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -3,6 +3,7 @@ # Python from io import StringIO +import datetime import codecs import json import logging @@ -1219,7 +1220,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique else: status_data['instance_group_name'] = None elif status in ['successful', 'failed', 'canceled']: - status_data['finished'] = self.finished + status_data['finished'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ") status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs' if getattr(self, 'unified_job_template_id', None): From 457dc956f12ce1575b365f841bb15f5e7af7cbfc Mon Sep 17 00:00:00 2001 From: Rebeccah Date: Wed, 4 Mar 2020 15:35:04 -0500 Subject: [PATCH 18/18] added a check for the field of finished making sure it's not none, and then breaking the DateTime function --- awx/main/models/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 168f28a25a..1b17291fba 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1219,7 +1219,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status_data['instance_group_name'] = self.instance_group.name else: status_data['instance_group_name'] = None - elif status in ['successful', 'failed', 'canceled']: + elif status in ['successful', 'failed', 'canceled'] and self.finished: status_data['finished'] = datetime.datetime.strftime(self.finished, "%Y-%m-%dT%H:%M:%S.%fZ") status_data.update(self.websocket_emit_data()) status_data['group_name'] = 'jobs'