diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.component.js similarity index 90% rename from awx/ui/client/features/output/details.directive.js rename to awx/ui/client/features/output/details.component.js index f13de2821b..d74cea977a 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.component.js @@ -2,7 +2,6 @@ const templateUrl = require('~features/output/details.partial.html'); let $http; let $filter; -let $scope; let $state; let error; @@ -10,7 +9,6 @@ let parse; let prompt; let resource; let strings; -let status; let wait; let vm; @@ -372,7 +370,7 @@ function getJobTagDetails () { let jobTags; if (tagString) { - jobTags = tagString.split(','); + jobTags = tagString.split(',').filter(tag => tag !== ''); } else { jobTags = []; } @@ -384,7 +382,7 @@ function getJobTagDetails () { const label = 'Job Tags'; const more = false; - const value = jobTags.filter(tag => tag !== '').map($filter('sanitize')); + const value = jobTags.map($filter('sanitize')); return { label, more, value }; } @@ -395,7 +393,7 @@ function getSkipTagDetails () { let skipTags; if (tagString) { - skipTags = tagString.split(','); + skipTags = tagString.split(',').filter(tag => tag !== ''); } else { skipTags = []; } @@ -406,7 +404,7 @@ function getSkipTagDetails () { const label = 'Skip Tags'; const more = false; - const value = skipTags.filter(tag => tag !== '').map($filter('sanitize')); + const value = skipTags.map($filter('sanitize')); return { label, more, value }; } @@ -446,7 +444,7 @@ function createErrorHandler (path, action) { const hdr = strings.get('error.HEADER'); const msg = strings.get('error.CALL', { path, action, status: res.status }); - error($scope, res.data, res.status, null, { hdr, msg }); + error(null, res.data, res.status, null, { hdr, msg }); }; } @@ -546,33 +544,33 @@ function deleteJob () { prompt({ hdr, resourceName, body, actionText, action }); } -function AtJobDetailsController ( +function JobDetailsController ( _$http_, _$filter_, _$state_, _error_, _prompt_, _strings_, - _status_, _wait_, _parse_, + { subscribe }, ) { vm = this || {}; $http = _$http_; $filter = _$filter_; $state = _$state_; - error = _error_; + parse = _parse_; prompt = _prompt_; strings = _strings_; - status = _status_; wait = _wait_; - vm.init = _$scope_ => { - $scope = _$scope_; - resource = $scope.resource; // eslint-disable-line prefer-destructuring + let unsubscribe; + + vm.$onInit = () => { + resource = this.resource; // eslint-disable-line prefer-destructuring vm.status = getStatusDetails(); vm.started = getStartDetails(); @@ -606,54 +604,42 @@ function AtJobDetailsController ( vm.cancelJob = cancelJob; vm.deleteJob = deleteJob; - vm.toggleLabels = toggleLabels; vm.toggleJobTags = toggleJobTags; vm.toggleSkipTags = toggleSkipTags; + vm.toggleLabels = toggleLabels; - const observe = (getter, transform, key) => { - $scope.$watch(getter, value => { vm[key] = transform(value); }); - }; - - observe(status.getStarted, getStartDetails, 'started'); - observe(status.getFinished, getFinishDetails, 'finished'); - observe(status.getProjectUpdateId, getProjectUpdateDetails, 'projectUpdate'); - observe(status.getProjectStatus, getProjectStatusDetails, 'projectStatus'); - - $scope.$watch(status.getJobStatus, jobStatus => { - vm.status = getStatusDetails(jobStatus); - vm.job.status = jobStatus; + unsubscribe = subscribe(({ status, started, finished, scm }) => { + vm.started = getStartDetails(started); + vm.finished = getFinishDetails(finished); + vm.projectUpdate = getProjectUpdateDetails(scm.id); + vm.projectStatus = getProjectStatusDetails(scm.status); + vm.status = getStatusDetails(status); + vm.job.status = status; }); }; + + vm.$onDestroy = () => { + unsubscribe(); + }; } -AtJobDetailsController.$inject = [ +JobDetailsController.$inject = [ '$http', '$filter', '$state', 'ProcessErrors', 'Prompt', 'JobStrings', - 'JobStatusService', 'Wait', 'ParseVariableString', + 'JobStatusService', ]; -function atJobDetailsLink (scope, el, attrs, controllers) { - const [atDetailsController] = controllers; - - atDetailsController.init(scope); -} - -function atJobDetails () { - return { - templateUrl, - restrict: 'E', - require: ['atJobDetails'], - controllerAs: 'vm', - link: atJobDetailsLink, - controller: AtJobDetailsController, - scope: { resource: '=', }, - }; -} - -export default atJobDetails; +export default { + templateUrl, + controller: JobDetailsController, + controllerAs: 'vm', + bindings: { + resource: '<' + }, +}; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 488d994dc2..e84c93d708 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -38,9 +38,8 @@ function JobsIndexController ( vm.clear = devClear; // Expand/collapse - // vm.toggle = toggle; - // vm.expand = expand; - vm.isExpanded = true; + vm.expanded = false; + vm.toggleExpanded = toggleExpanded; // Panel vm.resource = resource; @@ -55,10 +54,6 @@ function JobsIndexController ( up: scrollPageUp }; - vm.fullscreen = { - isFullscreen: false - }; - render.requestAnimationFrame(() => init()); } @@ -96,13 +91,14 @@ function init () { }, onStop () { status.updateStats(); + status.dispatch(); } }); $scope.$on(resource.ws.events, handleJobEvent); $scope.$on(resource.ws.status, handleStatusEvent); - if (!status.isRunning()) { + if (!status.state.running) { next(); } } @@ -281,9 +277,9 @@ function scrollIsAtRest (isAtRest) { vm.scroll.showBackToTop = !isAtRest; } -// function expand () { -// vm.toggle(parent, true); -// } +function toggleExpanded () { + vm.expanded = !vm.expanded; +} // function showHostDetails (id) { // jobEvent.request('get', id) diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 0bb295a714..6b14296553 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -8,11 +8,12 @@ import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; import EngineService from '~features/output/engine.service'; import StatusService from '~features/output/status.service'; +import MessageService from '~features/output/message.service'; import LegacyRedirect from '~features/output/legacy.route'; -import DetailsDirective from '~features/output/details.directive'; -import SearchDirective from '~features/output/search.directive'; -import StatsDirective from '~features/output/stats.directive'; +import DetailsComponent from '~features/output/details.component'; +import SearchComponent from '~features/output/search.component'; +import StatsComponent from '~features/output/stats.component'; import HostEvent from './host-event/index'; const Template = require('~features/output/index.view.html'); @@ -175,7 +176,7 @@ function JobsRun ($stateRegistry, strings) { templateUrl: Template, controller: Controller, controllerAs: 'vm' - } + }, }, resolve: { webSocketConnection: [ @@ -221,9 +222,10 @@ angular .service('JobRenderService', RenderService) .service('JobEventEngine', EngineService) .service('JobStatusService', StatusService) - .directive('atJobDetails', DetailsDirective) - .directive('atJobSearch', SearchDirective) - .directive('atJobStats', StatsDirective) + .service('JobMessageService', MessageService) + .component('atJobSearch', SearchComponent) + .component('atJobStats', StatsComponent) + .component('atJobDetails', DetailsComponent) .run(JobsRun) .run(LegacyRedirect); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index f25171f9f2..cb932fafd3 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,23 +1,25 @@
- - + + + - +
{{ vm.title }}
+ expanded="vm.expanded">
-
+
+ ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }">
diff --git a/awx/ui/client/features/output/message.service.js b/awx/ui/client/features/output/message.service.js new file mode 100644 index 0000000000..7e15ff302f --- /dev/null +++ b/awx/ui/client/features/output/message.service.js @@ -0,0 +1,41 @@ +function MessageService () { + const listeners = {}; + const registry = {}; + + this.subscribe = (key, listener) => { + registry[key] = registry[key] || 0; + + listeners[key] = listeners[key] || {}; + listeners[key][registry[key]] = listener; + + const unsubscribe = this.createCallback(key, registry[key]); + + registry[key]++; + + return unsubscribe; + }; + + this.dispatch = (key, data) => { + if (!listeners[key]) { + return; + } + + const indices = Object.keys(listeners[key]); + + for (let i = 0; i < indices.length; i++) { + listeners[key][indices[i]](data); + } + }; + + this.createCallback = (key, index) => { + const callback = () => { + if (listeners[key]) { + delete listeners[key][index]; + } + }; + + return callback; + }; +} + +export default MessageService; diff --git a/awx/ui/client/features/output/search.directive.js b/awx/ui/client/features/output/search.component.js similarity index 72% rename from awx/ui/client/features/output/search.directive.js rename to awx/ui/client/features/output/search.component.js index 0a688f92bb..d1e37edd14 100644 --- a/awx/ui/client/features/output/search.directive.js +++ b/awx/ui/client/features/output/search.component.js @@ -8,7 +8,6 @@ const PLACEHOLDER_RUNNING = 'CANNOT SEARCH RUNNING JOB'; const PLACEHOLDER_DEFAULT = 'SEARCH'; let $state; -let status; let qs; let vm; @@ -65,15 +64,8 @@ function clearSearch () { $state.transitionTo($state.current, $state.params, searchReloadOptions); } -function atJobSearchLink (scope, el, attrs, controllers) { - const [atJobSearchController] = controllers; - - atJobSearchController.init(scope); -} - -function AtJobSearchController (_$state_, _status_, _qs_) { +function JobSearchController (_$state_, _qs_, { subscribe }) { $state = _$state_; - status = _status_; qs = _qs_; vm = this || {}; @@ -91,39 +83,33 @@ function AtJobSearchController (_$state_, _status_, _qs_) { vm.removeSearchTag = removeSearchTag; vm.submitSearch = submitSearch; - vm.init = scope => { - vm.examples = scope.examples || searchKeyExamples; - vm.fields = scope.fields || searchKeyFields; - vm.placeholder = PLACEHOLDER_DEFAULT; - vm.relatedFields = scope.relatedFields || []; + let unsubscribe; - scope.$watch(status.isRunning, value => { - vm.disabled = value; - vm.placeholder = value ? PLACEHOLDER_RUNNING : PLACEHOLDER_DEFAULT; + vm.$onInit = () => { + vm.examples = searchKeyExamples; + vm.fields = searchKeyFields; + vm.placeholder = PLACEHOLDER_DEFAULT; + vm.relatedFields = []; + + unsubscribe = subscribe(({ running }) => { + vm.disabled = running; + vm.placeholder = running ? PLACEHOLDER_RUNNING : PLACEHOLDER_DEFAULT; }); }; -} -AtJobSearchController.$inject = [ - '$state', - 'JobStatusService', - 'QuerySet', -]; - -function atJobSearch () { - return { - templateUrl, - restrict: 'E', - require: ['atJobSearch'], - controllerAs: 'vm', - link: atJobSearchLink, - controller: AtJobSearchController, - scope: { - examples: '=', - fields: '=', - relatedFields: '=', - }, + vm.$onDestroy = () => { + unsubscribe(); }; } -export default atJobSearch; +JobSearchController.$inject = [ + '$state', + 'QuerySet', + 'JobStatusService', +]; + +export default { + templateUrl, + controller: JobSearchController, + controllerAs: 'vm', +}; diff --git a/awx/ui/client/features/output/stats.component.js b/awx/ui/client/features/output/stats.component.js new file mode 100644 index 0000000000..a7a0ec61f3 --- /dev/null +++ b/awx/ui/client/features/output/stats.component.js @@ -0,0 +1,74 @@ +const templateUrl = require('~features/output/stats.partial.html'); + +let vm; + +function createStatsBarTooltip (key, count) { + const label = `${key}`; + const badge = `${count}`; + + return `${label}${badge}`; +} + +function JobStatsController (strings, { subscribe }) { + vm = this || {}; + + let unsubscribe; + + vm.tooltips = { + running: strings.get('status.RUNNING'), + unavailable: strings.get('status.UNAVAILABLE'), + }; + + vm.$onInit = () => { + vm.download = vm.resource.model.get('related.stdout'); + vm.toggleStdoutFullscreenTooltip = strings.get('expandCollapse.EXPAND'); + + unsubscribe = subscribe(({ running, elapsed, counts, stats, hosts }) => { + vm.plays = counts.plays; + vm.tasks = counts.tasks; + vm.hosts = counts.hosts; + vm.elapsed = elapsed; + vm.running = running; + vm.setHostStatusCounts(stats, hosts); + }); + }; + + vm.$onDestroy = () => { + unsubscribe(); + }; + + vm.setHostStatusCounts = (stats, counts) => { + Object.keys(counts).forEach(key => { + const count = counts[key]; + const statusBarElement = $(`.HostStatusBar-${key}`); + + statusBarElement.css('flex', `${count} 0 auto`); + + vm.tooltips[key] = createStatsBarTooltip(key, count); + }); + + vm.statsAreAvailable = stats; + }; + + vm.toggleExpanded = () => { + vm.expanded = !vm.expanded; + vm.toggleStdoutFullscreenTooltip = vm.expanded ? + strings.get('expandCollapse.COLLAPSE') : + strings.get('expandCollapse.EXPAND'); + }; +} + +JobStatsController.$inject = [ + 'JobStrings', + 'JobStatusService', +]; + +export default { + templateUrl, + controller: JobStatsController, + controllerAs: 'vm', + bindings: { + resource: '<', + expanded: '=', + }, +}; diff --git a/awx/ui/client/features/output/stats.directive.js b/awx/ui/client/features/output/stats.directive.js deleted file mode 100644 index 789fc29de6..0000000000 --- a/awx/ui/client/features/output/stats.directive.js +++ /dev/null @@ -1,91 +0,0 @@ -const templateUrl = require('~features/output/stats.partial.html'); - -let status; -let strings; - -function createStatsBarTooltip (key, count) { - const label = `${key}`; - const badge = `${count}`; - - return `${label}${badge}`; -} - -function atJobStatsLink (scope, el, attrs, controllers) { - const [atJobStatsController] = controllers; - - atJobStatsController.init(scope); -} - -function AtJobStatsController (_strings_, _status_) { - status = _status_; - strings = _strings_; - - const vm = this || {}; - - vm.tooltips = { - running: strings.get('status.RUNNING'), - unavailable: strings.get('status.UNAVAILABLE'), - }; - - vm.init = scope => { - const { resource } = scope; - - vm.fullscreen = scope.fullscreen; - - vm.download = resource.model.get('related.stdout'); - - vm.toggleStdoutFullscreenTooltip = strings.get('expandCollapse.EXPAND'); - - vm.setHostStatusCounts(status.getHostStatusCounts()); - - scope.$watch(status.getPlayCount, value => { vm.plays = value; }); - scope.$watch(status.getTaskCount, value => { vm.tasks = value; }); - scope.$watch(status.getElapsed, value => { vm.elapsed = value; }); - scope.$watch(status.getHostCount, value => { vm.hosts = value; }); - scope.$watch(status.isRunning, value => { vm.running = value; }); - - scope.$watchCollection(status.getHostStatusCounts, vm.setHostStatusCounts); - }; - - vm.setHostStatusCounts = counts => { - Object.keys(counts).forEach(key => { - const count = counts[key]; - const statusBarElement = $(`.HostStatusBar-${key}`); - - statusBarElement.css('flex', `${count} 0 auto`); - - vm.tooltips[key] = createStatsBarTooltip(key, count); - }); - - vm.statsAreAvailable = Boolean(status.getStatsEvent()); - }; - - vm.toggleFullscreen = () => { - vm.fullscreen.isFullscreen = !vm.fullscreen.isFullscreen; - vm.toggleStdoutFullscreenTooltip = vm.fullscreen.isFullscreen ? - strings.get('expandCollapse.COLLAPSE') : - strings.get('expandCollapse.EXPAND'); - }; -} - -function atJobStats () { - return { - templateUrl, - restrict: 'E', - require: ['atJobStats'], - controllerAs: 'vm', - link: atJobStatsLink, - controller: [ - 'JobStrings', - 'JobStatusService', - '$scope', - AtJobStatsController - ], - scope: { - resource: '=', - fullscreen: '=' - } - }; -} - -export default atJobStats; diff --git a/awx/ui/client/features/output/stats.partial.html b/awx/ui/client/features/output/stats.partial.html index 6427735236..c55ba4bc8d 100644 --- a/awx/ui/client/features/output/stats.partial.html +++ b/awx/ui/client/features/output/stats.partial.html @@ -31,8 +31,8 @@ aw-tool-tip="{{ vm.toggleStdoutFullscreenTooltip }}" data-tip-watch="vm.toggleStdoutFullscreenTooltip" data-placement="top" - ng-class="{'at-Input-button--active': vm.fullscreen.isFullscreen}" - ng-click="vm.toggleFullscreen()"> + ng-class="{'at-Input-button--active': vm.expanded}" + ng-click="vm.toggleExpanded()">
diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index 638d2ff399..12e3fd2544 100644 --- a/awx/ui/client/features/output/status.service.js +++ b/awx/ui/client/features/output/status.service.js @@ -6,34 +6,46 @@ const TASK_START = 'playbook_on_task_start'; const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped']; const FINISHED = ['successful', 'failed', 'error']; -let moment; - -function JobStatusService (_moment_) { - moment = _moment_; +function JobStatusService (moment, message) { + this.dispatch = () => message.dispatch('status', this.state); + this.subscribe = listener => message.subscribe('status', listener); this.init = ({ resource }) => { + const { model } = resource; + + this.created = model.get('created'); + this.job = model.get('id'); + this.jobType = model.get('type'); + this.project = model.get('project'); + + this.active = false; + this.latestTime = null; this.counter = -1; - this.created = resource.model.get('created'); - this.job = resource.model.get('id'); - this.jobType = resource.model.get('type'); - this.project = resource.model.get('project'); - this.elapsed = resource.model.get('elapsed'); - this.started = resource.model.get('started'); - this.finished = resource.model.get('finished'); - this.jobStatus = resource.model.get('status'); - this.projectStatus = resource.model.get('summary_fields.project_update.status'); - this.projectUpdateId = resource.model.get('summary_fields.project_update.id'); + this.state = { + running: false, + stats: false, + counts: { + plays: null, + tasks: null, + hosts: null, + }, + hosts: {}, + status: model.get('status'), + elapsed: model.get('elapsed'), + started: model.get('started'), + finished: model.get('finished'), + scm: { + id: model.get('summary_fields.project_update.id'), + status: model.get('summary_fields.project_update.status') + }, + }; - this.latestTime = null; - this.playCount = null; - this.taskCount = null; - this.hostCount = null; - this.active = false; - this.hostStatusCounts = {}; - - this.statsEvent = resource.stats; + this.setStatsEvent(resource.stats); this.updateStats(); + this.updateRunningState(); + + this.dispatch(); }; this.pushStatusEvent = data => { @@ -59,24 +71,46 @@ function JobStatusService (_moment_) { if (isLatest) { this.counter = data.counter; this.latestTime = data.created; - this.elapsed = moment(data.created).diff(this.created, 'seconds'); + this.setElapsed(moment(data.created).diff(this.created, 'seconds')); } if (data.event === JOB_START) { - this.started = this.started || data.created; + this.setStarted(this.state.started || data.created); } if (data.event === PLAY_START) { - this.playCount++; + this.state.counts.plays++; } if (data.event === TASK_START) { - this.taskCount++; + this.state.counts.tasks++; } if (data.event === JOB_END) { - this.statsEvent = data; + this.setStatsEvent(data); } + + this.dispatch(); + }; + + this.isExpectingStatsEvent = () => (this.jobType === 'job') || + (this.jobType === 'project_update'); + + this.updateStats = () => { + this.updateHostCounts(); + + if (this.statsEvent) { + this.state.stats = true; + this.setFinished(this.statsEvent.created); + this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful'); + } + }; + + this.updateRunningState = () => { + this.state.running = (Boolean(this.state.started) && !this.state.finished) || + (this.state.status === 'running') || + (this.state.status === 'pending') || + (this.state.status === 'waiting'); }; this.updateHostCounts = () => { @@ -98,74 +132,66 @@ function JobStatusService (_moment_) { }); }); - this.hostCount = countedHostNames.length; - this.hostStatusCounts = counts; + this.state.counts.hosts = countedHostNames.length; + this.setHostStatusCounts(counts); }; - this.updateStats = () => { - this.updateHostCounts(); - - if (this.statsEvent) { - this.setFinished(this.statsEvent.created); - this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful'); - } - }; - - this.isRunning = () => (Boolean(this.started) && !this.finished) || - (this.jobStatus === 'running') || - (this.jobStatus === 'pending') || - (this.jobStatus === 'waiting'); - - this.isExpectingStatsEvent = () => (this.jobType === 'job') || - (this.jobType === 'project_update'); - - this.getPlayCount = () => this.playCount; - this.getTaskCount = () => this.taskCount; - this.getHostCount = () => this.hostCount; - this.getHostStatusCounts = () => this.hostStatusCounts || {}; - this.getJobStatus = () => this.jobStatus; - this.getProjectStatus = () => this.projectStatus; - this.getProjectUpdateId = () => this.projectUpdateId; - this.getElapsed = () => this.elapsed; - this.getStatsEvent = () => this.statsEvent; - this.getStarted = () => this.started; - this.getFinished = () => this.finished; - this.setJobStatus = status => { - this.jobStatus = status; + this.state.status = status; if (!this.isExpectingStatsEvent() && _.includes(FINISHED, status)) { if (this.latestTime) { this.setFinished(this.latestTime); - - if (!this.started && this.elapsed) { - this.started = moment(this.latestTime).subtract(this.elapsed, 'seconds'); + if (!this.state.started && this.state.elapsed) { + this.setStarted(moment(this.latestTime) + .subtract(this.state.elapsed, 'seconds')); } } } + + this.updateRunningState(); + }; + + this.setElapsed = elapsed => { + this.state.elapsed = elapsed; + }; + + this.setStarted = started => { + this.state.started = started; + this.updateRunningState(); }; this.setProjectStatus = status => { - this.projectStatus = status; + this.state.scm.status = status; }; this.setProjectUpdateId = id => { - this.projectUpdateId = id; + this.state.scm.id = id; }; this.setFinished = time => { - this.finished = time; + this.state.finished = time; + this.updateRunningState(); + }; + + this.setStatsEvent = data => { + this.statsEvent = data; + }; + + this.setHostStatusCounts = counts => { + this.state.hosts = counts; }; this.resetCounts = () => { - this.playCount = 0; - this.taskCount = 0; - this.hostCount = 0; + this.state.counts.plays = 0; + this.state.counts.tasks = 0; + this.state.counts.hosts = 0; }; } JobStatusService.$inject = [ 'moment', + 'JobMessageService', ]; export default JobStatusService;