adding more workflow results pages and functionality

This commit is contained in:
jaredevantabor
2016-11-08 12:21:11 -05:00
parent abdd9f8372
commit 9cb002b4cb
7 changed files with 668 additions and 361 deletions

View File

@@ -48,6 +48,7 @@ import inventoryScripts from './inventory-scripts/main';
import organizations from './organizations/main'; import organizations from './organizations/main';
import managementJobs from './management-jobs/main'; import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main'; import jobDetail from './job-detail/main';
import workflowResults from './workflow-results/main';
import jobSubmission from './job-submission/main'; import jobSubmission from './job-submission/main';
import notifications from './notifications/main'; import notifications from './notifications/main';
import about from './about/main'; import about from './about/main';
@@ -115,6 +116,7 @@ var tower = angular.module('Tower', [
activityStream.name, activityStream.name,
footer.name, footer.name,
jobDetail.name, jobDetail.name,
workflowResults.name,
jobSubmission.name, jobSubmission.name,
notifications.name, notifications.name,
standardOut.name, standardOut.name,

View File

@@ -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 export default
angular.module('workflowResults', []) angular.module('workflowResults', [])
.run(['$stateExtender', function($stateExtender) { .run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route); $stateExtender.addState(route);
}]); }])
.service('workflowResultsService', workflowResultsService);

View File

@@ -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;
}

View File

@@ -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;
}
});
}];

View File

@@ -1,20 +1,20 @@
<div class="tab-pane" id="job-results"> <div class="tab-pane" id="workflow-results">
<div ng-cloak <div ng-cloak
id="htmlTemplate" id="htmlTemplate"
class="JobResults" class="WorkflowResults"
ng-class="{'fullscreen': stdoutFullScreen}"> ng-class="{'fullscreen': stdoutFullScreen}">
<div ui-view></div> <div ui-view></div>
<!-- LEFT PANE --> <!-- LEFT PANE -->
<div class="JobResults-leftSide" <div class="WorkflowResults-leftSide"
ng-class="{'JobResults-stdoutActionButton--active': stdoutFullScreen}"> ng-class="{'WorkflowResults-stdoutActionButton--active': stdoutFullScreen}">
<div class="Panel" <div class="Panel"
ng-show="!stdoutFullScreen"> ng-show="!stdoutFullScreen">
<!-- LEFT PANE HEADER --> <!-- LEFT PANE HEADER -->
<div class="JobResults-panelHeader"> <div class="WorkflowResults-panelHeader">
<div <div
class="JobResults-panelHeaderText"> class="WorkflowResults-panelHeaderText">
RESULTS RESULTS
</div> </div>
@@ -37,7 +37,7 @@
List-actionButton--delete" List-actionButton--delete"
data-placement="top" data-placement="top"
ng-click="deleteJob()" ng-click="deleteJob()"
ng-show="job_status.status == 'running' || ng-show="workflow_status.status == 'running' ||
job_status.status=='pending' " job_status.status=='pending' "
aw-tool-tip="Cancel" aw-tool-tip="Cancel"
data-original-title="" title=""> data-original-title="" title="">
@@ -63,237 +63,97 @@
<div> <div>
<!-- START TIME DETAIL --> <!-- START TIME DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="job.started"> ng-show="workflow.started">
<label class="JobResults-resultRowLabel"> <label class="WorkflowResults-resultRowLabel">
Started Started
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
{{ job.started | longDate }} {{ workflow.started | longDate }}
</div> </div>
</div> </div>
<!-- FINISHED TIME DETAIL --> <!-- FINISHED TIME DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="job.started"> ng-show="workflow.started">
<label class="JobResults-resultRowLabel"> <label class="WorkflowResults-resultRowLabel">
Finished Finished
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
{{ (job.finished | {{ (workflow.finished |
longDate) || "Not Finished" }} longDate) || "Not Finished" }}
</div> </div>
</div> </div>
<!-- TEMPLATE DETAIL --> <!-- TEMPLATE DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="job.summary_fields.job_template.name"> ng-show="workflow.name">
<label class="JobResults-resultRowLabel"> <label class="WorkflowResults-resultRowLabel">
Template Template
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
<a href="{{ job_template_link }}" <a href="{{ workflow_template_link }}"
aw-tool-tip="Edit the job template" aw-tool-tip="Edit the job template"
data-placement="top"> data-placement="top">
{{ job.summary_fields.job_template.name }} {{ workflow.name }}
</a> </a>
</div> </div>
</div> </div>
<!-- JOB TYPE DETAIL --> <!-- JOB TYPE DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="job.job_type"> ng-show="workflow.type">
<label class="JobResults-resultRowLabel"> <label class="WorkflowResults-resultRowLabel">
Job Type Job Type
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
{{ type_label }} Workflow Job
</div> </div>
</div> </div>
<!-- CREATED BY DETAIL --> <!-- CREATED BY DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="job.summary_fields.created_by.username"> ng-show="workflow.summary_fields.created_by.username">
<label class="JobResults-resultRowLabel"> <label class="WorkflowResults-resultRowLabel">
Launched By Launched By
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
<a href="{{ created_by_link }}" <a href="{{ created_by_link }}"
aw-tool-tip="Edit the User" aw-tool-tip="Edit the User"
data-placement="top"> data-placement="top">
{{ job.summary_fields.created_by.username }} {{ workflow.summary_fields.created_by.username }}
</a> </a>
</div> </div>
</div> </div>
<!-- INVENTORY DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.inventory.name">
<label class="JobResults-resultRowLabel">
Inventory
</label>
<div class="JobResults-resultRowText">
<a href="{{ inventory_link }}"
aw-tool-tip="Edit the inventory"
data-placement="top">
{{ job.summary_fields.inventory.name }}
</a>
</div>
</div>
<!-- PROJECT DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.project.name">
<label class="JobResults-resultRowLabel">
Project
</label>
<div class="JobResults-resultRowText">
<a href="{{ project_link }}"
aw-tool-tip="Edit the project"
data-placement="top">
{{ job.summary_fields.project.name }}
</a>
</div>
</div>
<!-- PLAYBOOK DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.playbook">
<label class="JobResults-resultRowLabel">
Playbook
</label>
<div class="JobResults-resultRowText">
{{ job.playbook }}
</div>
</div>
<!-- MACHINE CREDENTIAL DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.credential.name">
<label class="JobResults-resultRowLabel">
Machine Credential
</label>
<div class="JobResults-resultRowText">
<a href="{{ machine_credential_link }}"
aw-tool-tip="Edit the credential"
data-placement="top">
{{ job.summary_fields.credential.name }}
</a>
</div>
</div>
<!-- CLOUD CREDENTIAL DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.cloud_credential.name">
<label class="JobResults-resultRowLabel">
Cloud Credential
</label>
<div class="JobResults-resultRowText">
<a href="{{ cloud_credential_link }}"
aw-tool-tip="Edit the credential"
data-placement="top">
{{ job.summary_fields.cloud_credential.name }}
</a>
</div>
</div>
<!-- NETWORK CREDENTAIL DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.network_credential.name">
<label class="JobResults-resultRowLabel">
Network Credential
</label>
<div class="JobResults-resultRowText">
<a href="{{ network_credential_link }}"
aw-tool-tip="Edit the credential"
data-placement="top">
{{ job.summary_fields.network_credential.name }}
</a>
</div>
</div>
<!-- FORKS DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.forks !== undefined">
<label class="JobResults-resultRowLabel">
Forks
</label>
<div class="JobResults-resultRowText">
{{ job.forks }}
</div>
</div>
<!-- LIMIT DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.limit">
<label class="JobResults-resultRowLabel">
Limit
</label>
<div class="JobResults-resultRowText">
{{ job.limit }}
</div>
</div>
<!-- VERBOSITY DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.verbosity !== undefined">
<label class="JobResults-resultRowLabel">
Verbosity
</label>
<div class="JobResults-resultRowText">
{{ verbosity_label }}
</div>
</div>
<!-- TAGS DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.job_tags">
<label class="JobResults-resultRowLabel">
Job Tags
</label>
<div class="JobResults-resultRowText">
{{ job.job_tags }}
</div>
</div>
<!-- SKIP TAGS DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.skip_tags">
<label class="JobResults-resultRowLabel">
Skip Tags
</label>
<div class="JobResults-resultRowText">
{{ job.skip_tags }}
</div>
</div>
<!-- EXTRA VARIABLES DETAIL --> <!-- EXTRA VARIABLES DETAIL -->
<div class="JobResults-resultRow <div class="WorkflowResults-resultRow
JobResults-resultRow--variables" WorkflowResults-resultRow--variables"
ng-show="variables"> ng-show="variables">
<label class="JobResults-resultRowLabel <label class="WorkflowResults-resultRowLabel
JobResults-resultRowLabel--fullWidth"> WorkflowResults-resultRowLabel--fullWidth">
Extra Variables Extra Variables
</label> </label>
<textarea <textarea
rows="6" rows="6"
ng-model="variables" ng-model="variables"
name="variables" name="variables"
class="JobResults-extraVars" class="WorkflowResults-extraVars"
id="pre-formatted-variables"> id="pre-formatted-variables">
</textarea> </textarea>
</div> </div>
<!-- LABELS DETAIL --> <!-- LABELS DETAIL -->
<div class="JobResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="labels && labels.length > 0"> ng-show="labels && labels.length > 0">
<label class="JobResults-resultRowLabel <label class="WorkflowResults-resultRowLabel
JobResults-resultRowLabel--fullWidth"> WorkflowResults-resultRowLabel--fullWidth">
Labels Labels
</label> </label>
<div class="LabelList <div class="LabelList
JobResults-resultRowText WorkflowResults-resultRowText
JobResults-resultRowText--fullWidth"> WorkflowResults-resultRowText--fullWidth">
<div ng-repeat="label in labels" <div ng-repeat="label in labels"
class="LabelList-tagContainer"> class="LabelList-tagContainer">
<div class="LabelList-tag"> <div class="LabelList-tag">
@@ -308,19 +168,19 @@
<!-- STATUS DETAIL --> <!-- STATUS DETAIL -->
<!-- <div <!-- <div
class="form-group class="form-group
JobResults-resultRow WorkflowResults-resultRow
toggle-show"> toggle-show">
<label <label
class="JobResults-resultRowLabel class="WorkflowResults-resultRowLabel
col-lg-2 col-md-2 col-lg-2 col-md-2
col-sm-2 col-xs-3 col-sm-2 col-xs-3
control-label"> control-label">
Status Status
</label> </label>
<div class="JobResults-resultRowText <div class="WorkflowResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9"> col-lg-10 col-md-10 col-sm-10 col-xs-9">
<i <i
class="JobResults-statusIcon--results class="WorkflowResults-statusIcon--results
fa fa
icon-job-{{ job.status }}"> icon-job-{{ job.status }}">
</i> {{ status_label }} </i> {{ status_label }}
@@ -330,16 +190,16 @@
<!-- SCHEDULED BY DETAIL --> <!-- SCHEDULED BY DETAIL -->
<!-- <div <!-- <div
class="form-group class="form-group
JobResults-resultRow toggle-show" WorkflowResults-resultRow toggle-show"
ng-show="job.summary_fields.schedule_by.username"> ng-show="workflow.summary_fields.schedule_by.username">
<label <label
class="JobResults-resultRowLabel class="WorkflowResults-resultRowLabel
col-lg-2 col-md-2 col-lg-2 col-md-2
col-sm-2 col-xs-3 col-sm-2 col-xs-3
control-label"> control-label">
Launched By Launched By
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
<a href="{{ scheduled_by_link }}" <a href="{{ scheduled_by_link }}"
aw-tool-tip="Edit the Schedule" aw-tool-tip="Edit the Schedule"
data-placement="top"> data-placement="top">
@@ -351,16 +211,16 @@
<!-- ELAPSED TIME DETAIL --> <!-- ELAPSED TIME DETAIL -->
<!-- <div <!-- <div
class="form-group class="form-group
JobResults-resultRow toggle-show" WorkflowResults-resultRow toggle-show"
ng-show="job_status.started"> ng-show="workflow_status.started">
<label <label
class="JobResults-resultRowLabel class="WorkflowResults-resultRowLabel
col-lg-2 col-md-2 col-lg-2 col-md-2
col-sm-2 col-xs-3 col-sm-2 col-xs-3
control-label"> control-label">
Elapsed Elapsed
</label> </label>
<div class="JobResults-resultRowText"> <div class="WorkflowResults-resultRowText">
{{ job_status.elapsed }} {{ job_status.elapsed }}
</div> </div>
</div> --> </div> -->
@@ -368,11 +228,11 @@
<!-- EXPLANATION DETAIL --> <!-- EXPLANATION DETAIL -->
<!-- <div <!-- <div
class="form-group class="form-group
JobResults-resultRow WorkflowResults-resultRow
toggle-show" toggle-show"
ng-show="job_status.explanation"> ng-show="workflow_status.explanation">
<label <label
class="JobResults-resultRowLabel class="WorkflowResults-resultRowLabel
col-lg-2 col-md-2 col-lg-2 col-md-2
col-sm-2 col-xs-3 col-sm-2 col-xs-3
control-label"> control-label">
@@ -380,20 +240,20 @@
</label> --> </label> -->
<!-- PREVIOUS TASK SUCCEEDED --> <!-- PREVIOUS TASK SUCCEEDED -->
<!-- <div class="JobResults-resultRowText <!-- <div class="WorkflowResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9 col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation" job_status_explanation"
ng-show="!previousTaskFailed" ng-show="!previousTaskFailed"
ng-bind-html="job_status.explanation"> ng-bind-html="job_status.explanation">
<i <i
class="JobResults-statusIcon--results class="WorkflowResults-statusIcon--results
fa fa
icon-job-{{ job_status.status }}"> icon-job-{{ job_status.status }}">
</i> {{ job_status.status_label }} </i> {{ job_status.status_label }}
</div> --> </div> -->
<!-- PREVIOUS TASK FAILED --> <!-- PREVIOUS TASK FAILED -->
<!-- <div class="JobResults-resultRowText <!-- <div class="WorkflowResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9 col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation" job_status_explanation"
ng-show="previousTaskFailed"> ng-show="previousTaskFailed">
@@ -419,15 +279,15 @@
<!-- RESULTS TRACEBACK DETAIL --> <!-- RESULTS TRACEBACK DETAIL -->
<!-- <div <!-- <div
class="form-group class="form-group
JobResults-resultRow WorkflowResults-resultRow
toggle-show" ng-show="job.result_traceback"> toggle-show" ng-show="workflow.result_traceback">
<label <label
class="JobResults-resultRowLabel class="WorkflowResults-resultRowLabel
col-lg-2 col-md-12 col-lg-2 col-md-12
col-sm-12 col-xs-12"> col-sm-12 col-xs-12">
Results Traceback Results Traceback
</label> </label>
<div class="JobResults-resultRowText <div class="WorkflowResults-resultRowText
col-lg-10 col-md-12 col-sm-12 col-xs-12 col-lg-10 col-md-12 col-sm-12 col-xs-12
job_status_traceback" job_status_traceback"
ng-bind-html="job.result_traceback"> ng-bind-html="job.result_traceback">
@@ -440,50 +300,34 @@
</div> </div>
<!-- RIGHT PANE --> <!-- RIGHT PANE -->
<div class="JobResults-rightSide"> <div class="WorkflowResults-rightSide">
<div class="Panel"> <div class="Panel">
<!-- RIGHT PANE HEADER --> <!-- RIGHT PANE HEADER -->
<div class="StandardOut-panelHeader"> <div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText"> <div class="StandardOut-panelHeaderText">
<i class="JobResults-statusResultIcon <i class="WorkflowResults-statusResultIcon
fa icon-job-{{ job.status }}"> fa icon-job-{{ job.status }}">
</i> </i>
{{ job.name }} {{ workflow.name }}
</div> </div>
<!-- HEADER COUNTS --> <!-- HEADER COUNTS -->
<div class="JobResults-badgeRow"> <div class="WorkflowResults-badgeRow">
<!-- PLAYS COUNT --> <!-- PLAYS COUNT -->
<div class="JobResults-badgeTitle"> <div class="WorkflowResults-badgeTitle">
Plays Total Jobs
</div> </div>
<span class="badge List-titleBadge"> <span class="badge List-titleBadge">
{{ playCount || 0}} {{ workflow_nodes.count || 0}}
</span>
<!-- TASKS COUNT -->
<div class="JobResults-badgeTitle">
Tasks
</div>
<span class="badge List-titleBadge">
{{ taskCount || 0}}
</span>
<!-- HOSTS COUNT -->
<div class="JobResults-badgeTitle">
Hosts
</div>
<span class="badge List-titleBadge">
{{ hostCount || 0}}
</span> </span>
<!-- ELAPSED TIME --> <!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle"> <div class="WorkflowResults-badgeTitle">
Elapsed Elapsed
</div> </div>
<span class="badge List-titleBadge"> <span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }} {{ job.elapsed * 1000 }}
</span> </span>
</div> </div>
@@ -500,7 +344,7 @@
</button> </button>
<!-- DOWNLOAD ACTION --> <!-- DOWNLOAD ACTION -->
<a ng-show="job_status.status === 'failed' || <a ng-show="workflow_status.status === 'failed' ||
job_status.status === 'successful' || job_status.status === 'successful' ||
job_status.status === 'canceled'" job_status.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}"> href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">

View File

@@ -10,7 +10,7 @@ import workflowResultsController from './workflow-results.controller';
export default { export default {
name: 'workflowResults', name: 'workflowResults',
url: '/jobs/:id', url: '/workflows/:id',
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'jobs', parent: 'jobs',
label: '{{ job.id }} - {{ job.name }}' label: '{{ job.id }} - {{ job.name }}'
@@ -19,135 +19,95 @@ export default {
socket: { socket: {
"groups":{ "groups":{
"jobs": ["status_changed", "summary"], "jobs": ["status_changed", "summary"],
"job_events": [] // not sure if you're gonna need to use job_events
// or if y'all will come up w/ a new socket group specifically
// for workflows
// "job_events": []
} }
} }
}, },
templateUrl: templateUrl('workflow-results/workflow-results'), templateUrl: templateUrl('workflow-results/workflow-results'),
controller: workflowResultsController controller: workflowResultsController,
// resolve: { resolve: {
// // the GET for the particular job // the GET for the particular workflow
// jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { workflowData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) {
// Rest.setUrl(GetBasePath('jobs') + $stateParams.id); Rest.setUrl(GetBasePath('workflow_jobs') + $stateParams.id);
// var val = $q.defer(); var defer = $q.defer();
// Rest.get() Rest.get()
// .then(function(data) { .then(function(data) {
// val.resolve(data.data); defer.resolve(data.data);
// }, function(data) { }, function(data) {
// val.reject(data); defer.reject(data);
//
// if (data.status === 404) { if (data.status === 404) {
// Alert('Job Not Found', 'Cannot find job.', 'alert-info'); Alert('Job Not Found', 'Cannot find job.', 'alert-info');
// } else if (data.status === 403) { } else if (data.status === 403) {
// Alert('Insufficient Permissions', 'You do not have permission to view this job.', 'alert-info'); Alert('Insufficient Permissions', 'You do not have permission to view this job.', 'alert-info');
// } }
//
// $state.go('jobs'); $state.go('jobs');
// }); });
// return val.promise; return defer.promise;
// }], }],
// // after the GET for the job, this helps us keep the status bar from // after the GET for the job, this helps us keep the status bar from
// // flashing as rest data comes in. If the job is finished and // flashing as rest data comes in. Provides the list of workflow nodes
// // there's a playbook_on_stats event, go ahead and resolve the count workflowNodes: ['workflowData', 'Rest', '$q', function(workflowData, Rest, $q) {
// // so you don't get that flashing! var defer = $q.defer();
// count: ['jobData', 'jobResultsService', 'Rest', '$q', function(jobData, jobResultsService, Rest, $q) { Rest.setUrl(workflowData.related.workflow_nodes);
// var defer = $q.defer(); Rest.get()
// if (jobData.finished) { .success(function(data) {
// // if the job is finished, grab the playbook_on_stats defer.resolve(data.data);
// // role to get the final count })
// Rest.setUrl(jobData.related.job_events + .error(function() {
// "?event=playbook_on_stats"); defer.resolve(data);
// Rest.get() });
// .success(function(data) { return defer.promise;
// if(!data.results[0]){ }],
// defer.resolve({val: { // GET for the particular jobs labels to be displayed in the
// ok: 0, // left-hand pane
// skipped: 0, jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
// unreachable: 0, var getNext = function(data, arr, resolve) {
// failures: 0, Rest.setUrl(data.next);
// changed: 0 Rest.get()
// }, countFinished: false}); .success(function (data) {
// } if (data.next) {
// else { getNext(data, arr.concat(data.results), resolve);
// defer.resolve({ } else {
// val: jobResultsService resolve.resolve(arr.concat(data.results)
// .getCountsFromStatsEvent(data .map(val => val.name));
// .results[0].event_data), }
// countFinished: true}); });
// } };
// })
// .error(function() { var seeMoreResolve = $q.defer();
// defer.resolve({val: {
// ok: 0, Rest.setUrl(GetBasePath('workflow_jobs') + $stateParams.id + '/labels/');
// skipped: 0, Rest.get()
// unreachable: 0, .success(function(data) {
// failures: 0, if (data.next) {
// changed: 0 getNext(data, data.results, seeMoreResolve);
// }, countFinished: false}); } else {
// }); seeMoreResolve.resolve(data.results
// } else { .map(val => val.name));
// // job isn't finished so just send an empty count and read }
// // from events });
// defer.resolve({val: {
// ok: 0, return seeMoreResolve.promise;
// skipped: 0, }],
// unreachable: 0, // OPTIONS request for the workflow. Used to make things like the
// failures: 0, // verbosity data in the left-hand pane prettier than just an
// changed: 0 // integer
// }, countFinished: false}); workflowDataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
// } Rest.setUrl(GetBasePath('workflow_jobs') + $stateParams.id);
// return defer.promise; var defer = $q.defer();
// }], Rest.options()
// // GET for the particular jobs labels to be displayed in the .then(function(data) {
// // left-hand pane defer.resolve(data.data);
// jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { }, function(data) {
// var getNext = function(data, arr, resolve) { defer.reject(data);
// Rest.setUrl(data.next); });
// Rest.get() return defer.promise;
// .success(function (data) { }]
// if (data.next) { }
// getNext(data, arr.concat(data.results), resolve);
// } else {
// resolve.resolve(arr.concat(data.results)
// .map(val => val.name));
// }
// });
// };
//
// var seeMoreResolve = $q.defer();
//
// Rest.setUrl(GetBasePath('jobs') + $stateParams.id + '/labels/');
// Rest.get()
// .success(function(data) {
// if (data.next) {
// getNext(data, data.results, seeMoreResolve);
// } else {
// seeMoreResolve.resolve(data.results
// .map(val => val.name));
// }
// });
//
// return seeMoreResolve.promise;
// }],
// // OPTIONS request for the job. Used to make things like the
// // verbosity data in the left-hand pane prettier than just an
// // integer
// jobDataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
// Rest.setUrl(GetBasePath('jobs') + $stateParams.id);
// var val = $q.defer();
// Rest.options()
// .then(function(data) {
// val.resolve(data.data);
// }, function(data) {
// val.reject(data);
// });
// return val.promise;
// }],
// // This clears out the event queue, otherwise it'd be full of events
// // for previous job results the user had navigated to
// eventQueueInit: ['eventQueue', function(eventQueue) {
// eventQueue.initialize();
// }]
// },
//
}; };

View File

@@ -0,0 +1,196 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, InitiatePlaybookRun) {
var val = {
// the playbook_on_stats event returns the count data in a weird format.
// format to what we need!
getCountsFromStatsEvent: function(event_data) {
var hosts = {},
hostsArr;
// iterate over the event_data and populate an object with hosts
// and their status data
Object.keys(event_data).forEach(key => {
// failed passes boolean not integer
if (key === "failed") {
// array of hosts from failed type
hostsArr = Object.keys(event_data[key]);
hostsArr.forEach(host => {
if (!hosts[host]) {
// host has not been added to hosts object
// add now
hosts[host] = {};
}
hosts[host][key] = event_data[key][host];
});
} else {
// array of hosts from each type ("changed", "dark", etc.)
hostsArr = Object.keys(event_data[key]);
hostsArr.forEach(host => {
if (!hosts[host]) {
// host has not been added to hosts object
// add now
hosts[host] = {};
}
if (!hosts[host][key]) {
// host doesn't have key
hosts[host][key] = 0;
}
hosts[host][key] += event_data[key][host];
});
}
});
// use the hosts data populate above to get the count
var count = {
ok : _.filter(hosts, function(o){
return !o.failures && !o.changed && o.ok > 0;
}),
skipped : _.filter(hosts, function(o){
return o.skipped > 0;
}),
unreachable : _.filter(hosts, function(o){
return o.dark > 0;
}),
failures : _.filter(hosts, function(o){
return o.failed === true;
}),
changed : _.filter(hosts, function(o){
return o.changed > 0;
})
};
// turn the count into an actual count, rather than a list of host
// names
Object.keys(count).forEach(key => {
count[key] = count[key].length;
});
return count;
},
getEvents: function(url) {
var val = $q.defer();
Rest.setUrl(url);
Rest.get()
.success(function(data) {
val.resolve({results: data.results,
next: data.next});
})
.error(function(obj, status) {
ProcessErrors(null, obj, status, null, {
hdr: 'Error!',
msg: `Could not get job events.
Returned status: ${status}`
});
val.reject(obj);
});
return val.promise;
},
deleteJob: function(job) {
Prompt({
hdr: 'Delete Job',
body: `<div class='Prompt-bodyQuery'>
Are you sure you want to delete the job below?
</div>
<div class='Prompt-bodyTarget'>
#${job.id} ${$filter('sanitize')(job.name)}
</div>`,
action: function() {
Wait('start');
Rest.setUrl(job.url);
Rest.destroy()
.success(function() {
Wait('stop');
$('#prompt-modal').modal('hide');
$state.go('jobs');
})
.error(function(obj, status) {
Wait('stop');
$('#prompt-modal').modal('hide');
ProcessErrors(null, obj, status, null, {
hdr: 'Error!',
msg: `Could not delete job.
Returned status: ${status}`
});
});
},
actionText: 'DELETE'
});
},
cancelJob: function(job) {
var doCancel = function() {
Rest.setUrl(job.url + 'cancel');
Rest.post({})
.success(function() {
Wait('stop');
$('#prompt-modal').modal('hide');
})
.error(function(obj, status) {
Wait('stop');
$('#prompt-modal').modal('hide');
ProcessErrors(null, obj, status, null, {
hdr: 'Error!',
msg: `Could not cancel job.
Returned status: ${status}`
});
});
};
Prompt({
hdr: 'Cancel Job',
body: `<div class='Prompt-bodyQuery'>
Are you sure you want to cancel the job below?
</div>
<div class='Prompt-bodyTarget'>
#${job.id} ${$filter('sanitize')(job.name)}
</div>`,
action: function() {
Wait('start');
Rest.setUrl(job.url + 'cancel');
Rest.get()
.success(function(data) {
if (data.can_cancel === true) {
doCancel();
} else {
$('#prompt-modal').modal('hide');
ProcessErrors(null, data, null, null, {
hdr: 'Error!',
msg: `Job has completed,
unabled to be canceled.`
});
}
});
Rest.destroy()
.success(function() {
Wait('stop');
$('#prompt-modal').modal('hide');
})
.error(function(obj, status) {
Wait('stop');
$('#prompt-modal').modal('hide');
ProcessErrors(null, obj, status, null, {
hdr: 'Error!',
msg: `Could not cancel job.
Returned status: ${status}`
});
});
},
actionText: 'CANCEL'
});
},
relaunchJob: function(scope) {
InitiatePlaybookRun({ scope: scope, id: scope.job.id,
relaunch: true });
}
};
return val;
}];