diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 7d3f3d302e..5db5cb5638 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -48,6 +48,7 @@ import inventoryScripts from './inventory-scripts/main'; import organizations from './organizations/main'; import managementJobs from './management-jobs/main'; import jobDetail from './job-detail/main'; +import workflowResults from './workflow-results/main'; import jobSubmission from './job-submission/main'; import notifications from './notifications/main'; import about from './about/main'; @@ -115,6 +116,7 @@ var tower = angular.module('Tower', [ activityStream.name, footer.name, jobDetail.name, + workflowResults.name, jobSubmission.name, notifications.name, standardOut.name, diff --git a/awx/ui/client/src/workflow-results/main.js b/awx/ui/client/src/workflow-results/main.js index eefa57c2dc..6e6d775dea 100644 --- a/awx/ui/client/src/workflow-results/main.js +++ b/awx/ui/client/src/workflow-results/main.js @@ -5,10 +5,13 @@ *************************************************/ -import route from './job-results.route.js'; +import route from './workflow-results.route.js'; + +import workflowResultsService from './workflow-results.service'; export default angular.module('workflowResults', []) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); - }]); + }]) + .service('workflowResultsService', workflowResultsService); diff --git a/awx/ui/client/src/workflow-results/workflow-results.block.less b/awx/ui/client/src/workflow-results/workflow-results.block.less index e69de29bb2..a053db98c1 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.block.less +++ b/awx/ui/client/src/workflow-results/workflow-results.block.less @@ -0,0 +1,114 @@ +@import '../shared/branding/colors.less'; +@import '../shared/branding/colors.default.less'; +@import '../shared/layouts/one-plus-two.less'; + +@breakpoint-md: 1200px; +@breakpoint-sm: 623px; + +.WorkflowResults { + .OnePlusTwo-container(100%, @breakpoint-md); + + &.fullscreen { + .WorkflowResults-rightSide { + max-width: 100%; + } + } +} + +.WorkflowResults-leftSide { + .OnePlusTwo-left--panel(100%, @breakpoint-md); + // TODO: needs to be set based on height of browser window + height: 870px !important; +} + +.WorkflowResults-rightSide { + .OnePlusTwo-right--panel(100%, @breakpoint-md); + // TODO: needs to be set based on height of browser window + height: 870px !important; + + @media (max-width: @breakpoint-md - 1px) { + padding-right: 15px; + } +} + +.WorkflowResults-stdoutActionButton--active { + display: none; + visibility: hidden; + flex:none; + width:0px; + padding-right: 0px; +} + +.WorkflowResults-panelHeader { + display: flex; + height: 30px; +} + +.WorkflowResults-panelHeaderText { + color: @default-interface-txt; + flex: 1 0 auto; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.WorkflowResults-resultRow { + width: 100%; + display: flex; + padding-bottom: 10px; + flex-wrap: wrap; +} + +.WorkflowResults-resultRow--variables { + flex-direction: column; +} + +.WorkflowResults-resultRowLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 14px; + font-weight: normal!important; + width: 30%; + margin-right: 20px; + + @media screen and (max-width: @breakpoint-md) { + flex: 2.5 0 auto; + } +} + +.WorkflowResults-resultRowLabel--fullWidth { + width: 100%; + margin-right: 0px; +} + +.WorkflowResults-resultRowText { + width: ~"calc(70% - 20px)"; + flex: 1 0 auto; + text-transform: none; + word-wrap: break-word; +} + +.WorkflowResults-resultRowText--fullWidth { + width: 100%; +} + +.WorkflowResults-statusResultIcon { + padding-left: 0px; + padding-right: 10px; +} + +.WorkflowResults-badgeRow { + display: flex; + align-items: center; + margin-right: 5px; +} + +.WorkflowResults-badgeTitle{ + color: @default-interface-txt; + font-size: 14px; + margin-right: 10px; + font-weight: normal; + text-transform: uppercase; + margin-left: 20px; +} diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index e69de29bb2..3b96f6e0d2 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -0,0 +1,188 @@ +export default ['workflowData', + 'workflowResultsService', + 'workflowDataOptions', + 'jobLabels', + 'workflowNodes', + '$scope', + 'ParseTypeChange', + 'ParseVariableString', + function(workflowData, + workflowResultsService, + workflowDataOptions, + jobLabels, + workflowNodes, + $scope, + ParseTypeChange, + ParseVariableString, + ) { + var getTowerLinks = function() { + var getTowerLink = function(key) { + if ($scope.workflow.related[key]) { + return '/#/' + $scope.workflow.related[key] + .split('api/v1/')[1]; + } + else { + return null; + } + }; + + $scope.workflow_template_link = '/#/templates/workflow_job_template/'+$scope.workflow.workflow_job_template; + $scope.created_by_link = getTowerLink('created_by'); + $scope.cloud_credential_link = getTowerLink('cloud_credential'); + $scope.network_credential_link = getTowerLink('network_credential'); + }; + + var getTowerLabels = function() { + var getTowerLabel = function(key) { + if ($scope.workflowOptions && $scope.workflowOptions[key]) { + return $scope.workflowOptions[key].choices + .filter(val => val[0] === $scope.workflow[key]) + .map(val => val[1])[0]; + } else { + return null; + } + }; + + $scope.status_label = getTowerLabel('status'); + $scope.type_label = getTowerLabel('job_type'); + $scope.verbosity_label = getTowerLabel('verbosity'); + }; + + var getTotalHostCount = function(count) { + return Object + .keys(count).reduce((acc, i) => acc += count[i], 0); + }; + + // put initially resolved request data on scope + $scope.workflow = workflowData; + $scope.workflow_nodes = workflowNodes; + $scope.workflowOptions = workflowDataOptions.actions.GET; + $scope.labels = jobLabels; + + // turn related api browser routes into tower routes + getTowerLinks(); + + // use options labels to manipulate display of details + getTowerLabels(); + + // set up a read only code mirror for extra vars + $scope.variables = ParseVariableString($scope.workflow.extra_vars); + $scope.parseType = 'yaml'; + ParseTypeChange({ scope: $scope, + field_id: 'pre-formatted-variables', + readOnly: true }); + + // Click binding for the expand/collapse button on the standard out log + $scope.stdoutFullScreen = false; + $scope.toggleStdoutFullscreen = function() { + $scope.stdoutFullScreen = !$scope.stdoutFullScreen; + }; + + $scope.deleteJob = function() { + workflowResultsService.deleteJob($scope.workflow); + }; + + $scope.cancelJob = function() { + workflowResultsService.cancelJob($scope.workflow); + }; + + $scope.relaunchJob = function() { + workflowResultsService.relaunchJob($scope); + }; + + $scope.stdoutArr = []; + + // EVENT STUFF BELOW + + // just putting the event queue on scope so it can be inspected in the + // console + // $scope.event_queue = eventQueue.queue; + // $scope.defersArr = eventQueue.populateDefers; + + // This is where the async updates to the UI actually happen. + // Flow is event queue munging in the service -> $scope setting in here + var processEvent = function(event) { + // put the event in the queue + eventQueue.populate(event).then(mungedEvent => { + // make changes to ui based on the event returned from the queue + if (mungedEvent.changes) { + mungedEvent.changes.forEach(change => { + // we've got a change we need to make to the UI! + // update the necessary scope and make the change + if (change === 'startTime' && !$scope.workflow.start) { + $scope.workflow.start = mungedEvent.startTime; + } + + if (change === 'count' && !$scope.countFinished) { + // for all events that affect the host count, + // update the status bar as well as the host + // count badge + $scope.count = mungedEvent.count; + $scope.hostCount = getTotalHostCount(mungedEvent + .count); + } + + if (change === 'playCount') { + $scope.playCount = mungedEvent.playCount; + } + + if (change === 'taskCount') { + $scope.taskCount = mungedEvent.taskCount; + } + + if (change === 'finishedTime' && !$scope.workflow.finished) { + $scope.workflow.finished = mungedEvent.finishedTime; + } + + if (change === 'countFinished') { + // the playbook_on_stats event actually lets + // us know that we don't need to iteratively + // look at event to update the host counts + // any more. + $scope.countFinished = true; + } + + if(change === 'stdout'){ + angular + .element(".JobResultsStdOut-stdoutContainer") + .append($compile(mungedEvent + .stdout)($scope)); + } + }); + } + + // the changes have been processed in the ui, mark it in the queue + eventQueue.markProcessed(event); + }); + }; + + // PULL! grab completed event data and process each event + // TODO: implement retry logic in case one of these requests fails + // var getEvents = function(url) { + // workflowResultsService.getEvents(url) + // .then(events => { + // events.results.forEach(event => { + // // get the name in the same format as the data + // // coming over the websocket + // event.event_name = event.event; + // processEvent(event); + // }); + // if (events.next) { + // getEvents(events.next); + // } + // }); + // }; + // getEvents($scope.job.related.job_events); + + // Processing of job_events messages from the websocket + $scope.$on(`ws-job_events-${$scope.workflow.id}`, function(e, data) { + processEvent(data); + }); + + // Processing of job-status messages from the websocket + $scope.$on(`ws-jobs`, function(e, data) { + if (parseInt(data.unified_job_id, 10) === parseInt($scope.workflow.id,10)) { + $scope.workflow.status = data.status; + } + }); +}]; diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 343c815c08..a418451de7 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -1,20 +1,20 @@ -
+
-
+
-
+
+ class="WorkflowResults-panelHeaderText"> RESULTS
@@ -37,7 +37,7 @@ List-actionButton--delete" data-placement="top" ng-click="deleteJob()" - ng-show="job_status.status == 'running' || + ng-show="workflow_status.status == 'running' || job_status.status=='pending' " aw-tool-tip="Cancel" data-original-title="" title=""> @@ -63,237 +63,97 @@
-
-