Merge pull request #4046 from ansible/jobDetailsRework

Job details rework
This commit is contained in:
Jared Tabor
2016-11-18 14:23:35 -08:00
committed by GitHub
36 changed files with 4318 additions and 132 deletions

View File

@@ -21,6 +21,7 @@ body {
padding-bottom: 50px;
position: relative;
background-color: @default-secondary-bg;
padding-top: 96px;
}
.container-fluid {
@@ -61,7 +62,7 @@ body {
}
#content-container {
padding-bottom: 40px;
padding-bottom: 0px;
}
.group-breadcrumbs {

View File

@@ -53,6 +53,7 @@ import organizations from './organizations/main';
import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
import workflowResults from './workflow-results/main';
import jobResults from './job-results/main';
import jobSubmission from './job-submission/main';
import notifications from './notifications/main';
import about from './about/main';
@@ -123,6 +124,7 @@ var tower = angular.module('Tower', [
footer.name,
jobDetail.name,
workflowResults.name,
jobResults.name,
jobSubmission.name,
notifications.name,
standardOut.name,

View File

@@ -26,7 +26,6 @@
return {
// custom_logo: true // load /var/lib/awx/public/static/assets/custom_console_logo.png as the login modal header. if false, will load the standard tower console logo
// custom_login_info: "example notice" // have a notice displayed in the login modal for users. note that, as a security measure, custom html is not supported and will be escaped.
tooltip_delay: { show: 500, hide: 100 }, // Default number of milliseconds to delay displaying/hiding tooltips
password_length: 8, // Minimum user password length. Set to 0 to not set a limit
password_hasLowercase: true, // require a lowercase letter in the password

View File

@@ -101,11 +101,11 @@
font-weight: 600;
margin-bottom: 8px;
}
.HostEvent .modal-body{
max-height: 500px;
overflow-y: auto;
padding: 20px;
}
// .HostEvent .modal-body{
// max-height: 500px;
// overflow-y: auto;
// padding: 20px;
// }
.HostEvent-nav{
padding-top: 12px;
padding-bottom: 12px;

View File

@@ -2,7 +2,7 @@
@import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less';
@import '../shared/layouts/one-plus-one.less';
@import '../shared/layouts/one-plus-two.less';
@breakpoint-md: 1200px;
@breakpoint-sm: 623px;
@@ -21,7 +21,7 @@
}
}
.JobDetail{
.OnePlusOne-container(100%, @breakpoint-md);
.OnePlusTwo-container(100%, @breakpoint-md);
&.fullscreen {
.JobDetail-rightSide {
@@ -31,11 +31,11 @@
}
.JobDetail-leftSide{
.OnePlusOne-panel--left(100%, @breakpoint-md);
.OnePlusTwo-left--panel(100%, @breakpoint-md);
}
.JobDetail-rightSide{
.OnePlusOne-panel--right(100%, @breakpoint-md);
.OnePlusTwo-right--panel(100%, @breakpoint-md);
@media (max-width: @breakpoint-md - 1px) {
padding-right: 15px;
}
@@ -88,51 +88,44 @@
}
.JobDetail-resultRow{
width: 50%;
width: 100%;
display: flex;
@media screen and(max-width: @breakpoint-sm){
width: 100%;
}
padding-bottom: 10px;
flex-wrap: wrap;
}
.JobDetail-resultRow--variables {
flex-direction: column;
}
.JobDetail-resultRowLabel{
text-transform: uppercase;
}
.JobDetail-resultRow label{
color: @default-interface-txt;
font-size: 14px;
font-weight: normal!important;
flex: 1 0 auto;
width: 30%;
margin-right: 20px;
@media screen and(max-width: @breakpoint-md){
flex: 2.5 0 auto;
}
}
.JobDetail-resultRow--variables{
.JobDetail-resultRowLabel--fullWidth {
width: 100%;
display: flex;
flex-direction: column;
padding-left:15px;
}
.JobDetail-extraVars{
text-transform: none;
}
.JobDetail-extraVarsLabel{
margin-left:-15px;
padding-bottom: 15px;
margin-right: 0px;
}
.JobDetail-resultRowText{
width: 40%;
width: ~"calc(70% - 20px)";
flex: 1 0 auto;
padding:0px 29px;
text-transform: none;
word-wrap: break-word;
}
.JobDetail-resultRowText--fullWidth {
width: 100%;
}
.JobDetail-searchHeaderRow{
display: flex;
flex-wrap: wrap;

View File

@@ -1,26 +1,91 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import { templateUrl } from '../shared/template-url/template-url.factory';
export default {
name: 'jobDetail',
url: '/jobs/{id: int}',
ncyBreadcrumb: {
parent: 'jobs',
label: "{{ job.id }} - {{ job.name }}"
},
data: {
socket: {
"groups": {
"jobs": ["status_changed", "summary"],
"job_events": []
}
}
},
templateUrl: templateUrl('job-detail/job-detail'),
controller: 'JobDetailController'
};
// <<<<<<< 4cf6a946a1aa14b7d64a8e1e8dabecfd3d056f27
// //<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a
// /*************************************************
// * Copyright (c) 2016 Ansible, Inc.
// *
// * All Rights Reserved
// *************************************************/
//
// // <<<<<<< a3d9eea2c9ddb4e16deec9ec38dea16bf37c559d
// // import { templateUrl } from '../shared/template-url/template-url.factory';
// //
// // export default {
// // name: 'jobDetail',
// // url: '/jobs/{id: int}',
// // ncyBreadcrumb: {
// // parent: 'jobs',
// // label: "{{ job.id }} - {{ job.name }}"
// // },
// // data: {
// // socket: {
// // "groups": {
// // "jobs": ["status_changed", "summary"],
// // "job_events": []
// // }
// // }
// // },
// // templateUrl: templateUrl('job-detail/job-detail'),
// // controller: 'JobDetailController'
// // };
// // =======
// // import {templateUrl} from '../shared/template-url/template-url.factory';
// //
// // export default {
// // name: 'jobDetail',
// // url: '/jobs/:id',
// // ncyBreadcrumb: {
// // parent: 'jobs',
// // label: "{{ job.id }} - {{ job.name }}"
// // },
// // socket: {
// // "groups":{
// // "jobs": ["status_changed", "summary"],
// // "job_events": []
// // }
// // },
// // templateUrl: templateUrl('job-detail/job-detail'),
// // controller: 'JobDetailController'
// // };
// //=======
// =======
// >>>>>>> Rebase of devel (w/ channels) + socket rework for new job details
// // /*************************************************
// // * Copyright (c) 2016 Ansible, Inc.
// // *
// // * All Rights Reserved
// // *************************************************/
// //
// // import {templateUrl} from '../shared/template-url/template-url.factory';
// //
// // export default {
// // name: 'jobDetail',
// // url: '/jobs/:id',
// // ncyBreadcrumb: {
// // parent: 'jobs',
// // label: "{{ job.id }} - {{ job.name }}"
// // },
// // socket: {
// // "groups":{
// // "jobs": ["status_changed", "summary"],
// // "job_events": []
// // }
// // },
// // resolve: {
// // jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// // if (!$rootScope.event_socket) {
// // $rootScope.event_socket = Socket({
// // scope: $rootScope,
// // endpoint: "job_events"
// // });
// // $rootScope.event_socket.init();
// // // returns should really be providing $rootScope.event_socket
// // // otherwise, we have to inject the entire $rootScope into the controller
// // return true;
// // } else {
// // return true;
// // }
// // }]
// // },
// // templateUrl: templateUrl('job-detail/job-detail'),
// // controller: 'JobDetailController'
// // };

View File

@@ -4,21 +4,21 @@
* All Rights Reserved
*************************************************/
import route from './job-detail.route';
// import route from './job-detail.route';
import controller from './job-detail.controller';
import service from './job-detail.service';
import hostEvents from './host-events/main';
import hostEvent from './host-event/main';
// import hostEvent from './host-event/main';
import hostSummary from './host-summary/main';
export default
angular.module('jobDetail', [
hostEvents.name,
hostEvent.name,
// hostEvent.name,
hostSummary.name
])
.controller('JobDetailController', controller)
.service('JobDetailService', service)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);
.service('JobDetailService', service);
// .run(['$stateExtender', function($stateExtender) {
// $stateExtender.addState(route);
// }]);

View File

@@ -0,0 +1,251 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['jobResultsService', 'parseStdoutService', '$q', function(jobResultsService, parseStdoutService, $q){
var val = {};
val = {
populateDefers: {},
queue: {},
// Get the count of the last event
getPreviousCount: function(counter, type) {
var countAttr;
if (type === 'play') {
countAttr = 'playCount';
} else if (type === 'task') {
countAttr = 'taskCount';
} else {
countAttr = 'count';
}
var previousCount = $q.defer();
// iteratively find the last count
var findCount = function(counter) {
if (counter === 0) {
// if counter is 0, no count has been initialized
// initialize one!
if (countAttr === 'count') {
previousCount.resolve({
ok: 0,
skipped: 0,
unreachable: 0,
failures: 0,
changed: 0
});
} else {
previousCount.resolve(0);
}
} else if (val.queue[counter] && val.queue[counter][countAttr] !== undefined) {
// this event has a count, resolve!
previousCount.resolve(_.clone(val.queue[counter][countAttr]));
} else {
// this event doesn't have a count, decrement to the
// previous event and check it
findCount(counter - 1);
}
};
if (val.queue[counter - 1]) {
// if the previous event has been resolved, start the iterative
// get previous count process
findCount(counter - 1);
} else if (val.populateDefers[counter - 1]){
// if the previous event has not been resolved, wait for it to
// be and then start the iterative get previous count process
val.populateDefers[counter - 1].promise.then(function() {
findCount(counter - 1);
});
}
return previousCount.promise;
},
// munge the raw event from the backend into the event_queue's format
munge: function(event) {
var mungedEventDefer = $q.defer();
// basic data needed in the munged event
var mungedEvent = {
counter: event.counter,
id: event.id,
processed: false,
name: event.event_name,
changes: []
};
// the interface for grabbing standard out is generalized and
// present across many types of events, so go ahead and check for
// updates to it
if (event.stdout) {
mungedEvent.stdout = parseStdoutService.parseStdout(event);
mungedEvent.changes.push('stdout');
}
// for different types of events, you need different types of data
if (event.event_name === 'playbook_on_start') {
mungedEvent.count = {
ok: 0,
skipped: 0,
unreachable: 0,
failures: 0,
changed: 0
};
mungedEvent.startTime = event.modified;
mungedEvent.changes.push('count');
mungedEvent.changes.push('startTime');
} else if (event.event_name === 'playbook_on_play_start') {
val.getPreviousCount(mungedEvent.counter, "play")
.then(count => {
mungedEvent.playCount = count + 1;
mungedEvent.changes.push('playCount');
});
} else if (event.event_name === 'playbook_on_task_start') {
val.getPreviousCount(mungedEvent.counter, "task")
.then(count => {
mungedEvent.taskCount = count + 1;
mungedEvent.changes.push('taskCount');
});
} else if (event.event_name === 'runner_on_ok' ||
event.event_name === 'runner_on_async_ok') {
val.getPreviousCount(mungedEvent.counter)
.then(count => {
mungedEvent.count = count;
mungedEvent.count.ok++;
mungedEvent.changes.push('count');
});
} else if (event.event_name === 'runner_on_skipped') {
val.getPreviousCount(mungedEvent.counter)
.then(count => {
mungedEvent.count = count;
mungedEvent.count.skipped++;
mungedEvent.changes.push('count');
});
} else if (event.event_name === 'runner_on_unreachable') {
val.getPreviousCount(mungedEvent.counter)
.then(count => {
mungedEvent.count = count;
mungedEvent.count.unreachable++;
mungedEvent.changes.push('count');
});
} else if (event.event_name === 'runner_on_error' ||
event.event_name === 'runner_on_async_failed') {
val.getPreviousCount(mungedEvent.counter)
.then(count => {
mungedEvent.count = count;
mungedEvent.count.failed++;
mungedEvent.changes.push('count');
});
} else if (event.event_name === 'playbook_on_stats') {
// get the data for populating the host status bar
mungedEvent.count = jobResultsService
.getCountsFromStatsEvent(event.event_data);
mungedEvent.finishedTime = event.modified;
mungedEvent.changes.push('count');
mungedEvent.changes.push('countFinished');
mungedEvent.changes.push('finishedTime');
}
mungedEventDefer.resolve(mungedEvent);
return mungedEventDefer.promise;
},
// reinitializes the event queue value for the job results page
initialize: function() {
val.queue = {};
val.populateDefers = {};
},
// populates the event queue
populate: function(event) {
// if a defer hasn't been set up for the event,
// set one up now
if (!val.populateDefers[event.counter]) {
val.populateDefers[event.counter] = $q.defer();
}
// make sure not to send duplicate events over to the
// controller
if (val.queue[event.counter] &&
val.queue[event.counter].processed) {
val.populateDefers.reject("duplicate event: " +
event);
}
if (!val.queue[event.counter]) {
var resolvePopulation = function(event) {
// to resolve, put the event on the queue and
// then resolve the deferred value
val.queue[event.counter] = event;
val.populateDefers[event.counter].resolve(event);
};
if (event.counter === 1) {
// for the first event, go ahead and munge and
// resolve
val.munge(event).then(event => {
resolvePopulation(event);
});
} else {
// for all other events, you have to do some things
// to keep the event processing in the UI synchronous
if (!val.populateDefers[event.counter - 1]) {
// first, if the previous event doesn't have
// a defer set up (this happens when websocket
// events are coming in and you need to make
// rest calls to catch up), go ahead and set a
// defer for the previous event
val.populateDefers[event.counter - 1] = $q.defer();
}
// you can start the munging process...
val.munge(event).then(event => {
// ...but wait until the previous event has
// been resolved before resolving this one and
// doing stuff in the ui (that's why we
// needed that previous conditional).
val.populateDefers[event.counter - 1].promise
.then(() => {
resolvePopulation(event);
});
});
}
} else {
// don't repopulate the event if it's already been added
// and munged either by rest or by websocket event
val.populateDefers[event.counter]
.resolve(val.queue[event.counter]);
}
return val.populateDefers[event.counter].promise;
},
// the event has been processed in the view and should be marked as
// completed in the queue
markProcessed: function(event) {
var process = function(event) {
// the event has now done it's work in the UI, record
// that!
val.queue[event.counter].processed = true;
};
if (!val.queue[event.counter]) {
// sometimes, the process is called in the controller and
// the event queue hasn't caught up and actually added
// the event to the queue yet. Wait until that happens
val.populateDefers[event.counter].promise
.finally(function() {
process(event);
});
} else {
process(event);
}
}
};
return val;
}];

View File

@@ -0,0 +1,2 @@
<textarea id="HostEvent-codemirror" class="HostEvent-codemirror">
</textarea>

View File

@@ -0,0 +1,57 @@
<div id="HostEvent" class="HostEvent modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<!-- modal body -->
<div class="modal-body">
<div class="HostEvent-header">
<a class="HostEvents-status">
<i class="fa fa-circle" ng-class="processEventStatus(event).class"></i>
</a>
<span class="HostEvent-title">{{event.host_name}}</span>
<!-- close -->
<button ui-sref="jobDetail" type="button" class="close">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div class="HostEvent-details--left">
<div class="HostEvent-field">
<span class="HostEvent-field--label">CREATED</span>
<span class="HostEvent-field--content">{{event.created || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">PLAY</span>
<span class="HostEvent-field--content">{{event.play || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">TASK</span>
<span class="HostEvent-field--content">{{event.task || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">MODULE</span>
<span class="HostEvent-field--content">{{event.event_data.res.invocation.module_name || "No result found"}}</span>
</div>
</div>
<!-- end of details-->
<div class="HostEvent-nav">
<!-- view navigation buttons -->
<button ui-sref="jobDetail.host-event.json" type="button" class="btn btn-sm btn-default HostEvent-tab" ng-class="{'HostEvent-tab--selected' : isActiveState('jobDetail.host-event.json')}">JSON</button>
<button ng-if="stdout" ui-sref="jobDetail.host-event.stdout" type="button" class="btn btn-sm btn-default HostEvent-tab" ng-class="{'HostEvent-tab--selected' : isActiveState('jobDetail.host-event.stdout')}">Standard Out</button>
<button ng-if="stderr" ui-sref="jobDetail.host-event.stderr" type="button" class="btn btn-sm btn-default HostEvent-tab" ng-class="{'HostEvent-tab--selected' : isActiveState('jobDetail.host-event.stderr')}">Standard Error</button>
</div>
<div class="HostEvent-body">
<!-- views -->
<div class="HostEvent-view--container" ui-view></div>
</div>
<!-- controls -->
<div class="HostEvent-controls">
<button ui-sref="jobDetail" class="btn btn-sm btn-default HostEvent-close HostEvent-button" ng-show="true" >Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,152 @@
@import "./client/src/shared/branding/colors.less";
@import "./client/src/shared/branding/colors.default.less";
@import "./client/src/shared/layouts/one-plus-two.less";
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
not supported by any browser */
}
@media screen and (min-width: 768px){
.HostEvent .modal-dialog{
width: 700px;
}
}
.HostEvent .CodeMirror{
overflow-x: hidden;
}
.HostEvent-controls button.HostEvent-close{
color: #FFFFFF;
text-transform: uppercase;
padding-left: 15px;
padding-right: 15px;
background-color: @default-link;
border-color: @default-link;
&:hover{
background-color: @default-link-hov;
border-color: @default-link-hov;
}
}
.HostEvent-body{
margin-bottom: 10px;
}
.HostEvent-tab {
color: @btn-txt;
background-color: @btn-bg;
font-size: 12px;
border: 1px solid @btn-bord;
height: 30px;
border-radius: 5px;
margin-right: 20px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
padding-top: 5px;
transition: background-color 0.2s;
text-transform: uppercase;
text-align: center;
white-space: nowrap;
.noselect;
}
.HostEvent-tab:hover {
color: @btn-txt;
background-color: @btn-bg-hov;
cursor: pointer;
}
.HostEvent-tab--selected{
color: @btn-txt-sel!important;
background-color: @default-icon!important;
border-color: @default-icon!important;
}
.HostEvent-view--container{
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
}
.HostEvent .modal-footer{
border: 0;
margin-top: 0px;
padding-top: 5px;
}
.HostEvent-controls{
float: right;
button {
margin-left: 10px;
}
}
.HostEvent-status--ok{
color: @green;
}
.HostEvent-status--unreachable{
color: @unreachable;
}
.HostEvent-status--changed{
color: @changed;
}
.HostEvent-status--failed{
color: @default-err;
}
.HostEvent-status--skipped{
color: @skipped;
}
.HostEvent-header{
padding-bottom: 15px;
}
.HostEvent-title{
color: @default-interface-txt;
font-weight: 600;
margin-bottom: 8px;
}
.HostEvent .modal-body{
height: 480px;
padding: 20px;
}
.HostEvent-nav{
padding-top: 12px;
padding-bottom: 12px;
}
.HostEvent-field{
margin-bottom: 8px;
flex: 0 1 12em;
}
.HostEvent-field--label{
text-transform: uppercase;
flex: 0 1 80px;
max-width: 80px;
font-size: 12px;
word-wrap: break-word;
}
.HostEvent-field{
.OnePlusTwo-left--detailsRow;
}
.HostEvent-field--content{
word-wrap: break-word;
max-width: 13em;
flex: 0 1 13em;
}
.HostEvent-details--left, .HostEvent-details--right{
flex: 1 1 47%;
}
.HostEvent-details--left{
margin-right: 40px;
}
.HostEvent-details--right{
.HostEvent-field--label{
flex: 0 1 25em;
}
.HostEvent-field--content{
max-width: 15em;
flex: 0 1 15em;
align-self: flex-end;
}
}
.HostEvent-button:disabled {
pointer-events: all!important;
}

View File

@@ -0,0 +1,90 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'hostEvent', 'hostResults', 'parseStdoutService',
function($stateParams, $scope, $state, Wait, JobDetailService, hostEvent, hostResults, parseStdoutService){
$scope.processEventStatus = JobDetailService.processEventStatus;
$scope.hostResults = [];
// Avoid rendering objects in the details fieldset
// ng-if="processResults(value)" via host-event-details.partial.html
$scope.processResults = function(value){
if (typeof value === 'object'){return false;}
else {return true;}
};
$scope.isStdOut = function(){
if ($state.current.name === 'jobDetails.host-event.stdout' || $state.current.name === 'jobDetaisl.histe-event.stderr'){
return 'StandardOut-preContainer StandardOut-preContent';
}
};
/*ignore jslint start*/
var initCodeMirror = function(el, data, mode){
var container = document.getElementById(el);
var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line
lineNumbers: true,
mode: mode
});
editor.setSize("100%", 200);
editor.getDoc().setValue(data);
};
/*ignore jslint end*/
$scope.isActiveState = function(name){
return $state.current.name === name;
};
$scope.getActiveHostIndex = function(){
var result = $scope.hostResults.filter(function( obj ) {
return obj.id === $scope.event.id;
});
return $scope.hostResults.indexOf(result[0]);
};
var init = function(){
$scope.event = _.cloneDeep(hostEvent);
$scope.hostResults = hostResults;
$scope.json = JobDetailService.processJson(hostEvent);
// grab standard out & standard error if present, and remove from the results displayed in the details panel
if (hostEvent.stdout){
$scope.stdout = parseStdoutService.prettify(hostEvent.stdout, "unstyled");
delete $scope.event.stdout;
}
if (hostEvent.stderr){
$scope.stderr = hostEvent.stderr;
delete $scope.event.stderr;
}
// instantiate Codemirror
// try/catch pattern prevents the abstract-state controller from complaining about element being null
if ($state.current.name === 'jobDetail.host-event.json'){
try{
initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true});
}
catch(err){
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
}
}
else if ($state.current.name === 'jobDetail.host-event.stdout'){
try{
initCodeMirror('HostEvent-codemirror', $scope.stdout, 'shell');
}
catch(err){
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
}
}
else if ($state.current.name === 'jobDetail.host-event.stderr'){
try{
initCodeMirror('HostEvent-codemirror', $scope.stderr, 'shell');
}
catch(err){
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
}
}
$('#HostEvent').modal('show');
};
init();
}];

View File

@@ -0,0 +1,58 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import { templateUrl } from '../../shared/template-url/template-url.factory';
var hostEventModal = {
name: 'jobDetail.host-event',
url: '/task/:taskId/host-event/:eventId',
controller: 'HostEventController',
templateUrl: templateUrl('job-results/host-event/host-event-modal'),
'abstract': false,
resolve: {
hostEvent: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) {
return JobDetailService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId
}).then(function(res) {
return res.data.results[0]; });
}],
hostResults: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) {
return JobDetailService.getJobEventChildren($stateParams.taskId).then(res => res.data.results);
}]
},
onExit: function() {
// close the modal
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
$('#HostEvent').modal('hide');
// hacky way to handle user browsing away via URL bar
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
};
var hostEventJson = {
name: 'jobDetail.host-event.json',
url: '/json',
controller: 'HostEventController',
templateUrl: templateUrl('job-results/host-event/host-event-codemirror')
};
var hostEventStdout = {
name: 'jobDetail.host-event.stdout',
url: '/stdout',
controller: 'HostEventController',
templateUrl: templateUrl('job-results/host-event/host-event-codemirror')
};
var hostEventStderr = {
name: 'jobDetail.host-event.stderr',
url: '/stderr',
controller: 'HostEventController',
templateUrl: templateUrl('job-results/host-event/host-event-codemirror')
};
export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr };

View File

@@ -0,0 +1,20 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {hostEventModal,
hostEventJson, hostEventStdout, hostEventStderr} from './host-event.route';
import controller from './host-event.controller';
export default
angular.module('jobResults.hostEvent', [])
.controller('HostEventController', controller)
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(hostEventModal);
$stateExtender.addState(hostEventJson);
$stateExtender.addState(hostEventStdout);
$stateExtender.addState(hostEventStderr);
}]);

View File

@@ -0,0 +1,80 @@
@import '../../shared/branding/colors.default.less';
.HostStatusBar {
display: flex;
flex: 0 0 auto;
width: 100%;
margin-top: 10px;
}
.HostStatusBar-ok,
.HostStatusBar-changed,
.HostStatusBar-unreachable,
.HostStatusBar-failures,
.HostStatusBar-skipped,
.HostStatusBar-noData {
height: 15px;
border-top: 5px solid @default-bg;
border-bottom: 5px solid @default-bg;
}
.HostStatusBar-ok {
background-color: @default-succ;
display: flex;
flex: 0 0 auto;
}
.HostStatusBar-changed {
background-color: @default-warning;
flex: 0 0 auto;
}
.HostStatusBar-unreachable {
background-color: @default-unreachable;
flex: 0 0 auto;
}
.HostStatusBar-failures {
background-color: @default-err;
flex: 0 0 auto;
}
.HostStatusBar-skipped {
background-color: @default-link;
flex: 0 0 auto;
}
.HostStatusBar-noData {
background-color: @default-icon-hov;
flex: 1 0 auto;
}
.HostStatusBar-tooltipLabel {
text-transform: uppercase;
margin-right: 15px;
}
.HostStatusBar-tooltipBadge {
border-radius: 5px;
}
.HostStatusBar-tooltipBadge--ok {
background-color: @default-succ;
}
.HostStatusBar-tooltipBadge--unreachable {
background-color: @default-unreachable;
}
.HostStatusBar-tooltipBadge--skipped {
background-color: @default-link;
}
.HostStatusBar-tooltipBadge--changed {
background-color: @default-warning;
}
.HostStatusBar-tooltipBadge--failures {
background-color: @default-err;
}

View File

@@ -0,0 +1,43 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
// import hostStatusBarController from './host-status-bar.controller';
export default [ 'templateUrl',
function(templateUrl) {
return {
scope: true,
templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'),
restrict: 'E',
// controller: standardOutLogController,
link: function(scope) {
// as count is changed by event data coming in,
// update the host status bar
scope.$watch('count', function(val) {
if (val) {
Object.keys(val).forEach(key => {
// reposition the hosts status bar by setting
// the various flex values to the count of
// those hosts
$(`.HostStatusBar-${key}`)
.css('flex', `${val[key]} 0 auto`);
// set the tooltip to give how many hosts of
// each type
if (val[key] > 0) {
scope[`${key}CountTip`] = `<span class='HostStatusBar-tooltipLabel'>${key}</span><span class='badge HostStatusBar-tooltipBadge HostStatusBar-tooltipBadge--${key}'>${val[key]}</span>`;
}
});
// if there are any hosts that have finished, don't
// show default grey bar
scope.hostsFinished = (Object
.keys(val)
.filter(key => (val[key] > 0)).length > 0);
}
});
}
};
}];

View File

@@ -0,0 +1,26 @@
<div class="HostStatusBar">
<div class="HostStatusBar-ok"
data-placement="top"
aw-tool-tip="{{okCountTip}}"
data-tip-watch="okCountTip"></div>
<div class="HostStatusBar-changed"
data-placement="top"
aw-tool-tip="{{changedCountTip}}"
data-tip-watch="changedCountTip"></div>
<div class="HostStatusBar-failures"
data-placement="top"
aw-tool-tip="{{failuresCountTip}}"
data-tip-watch="failuresCountTip"></div>
<div class="HostStatusBar-unreachable"
data-placement="top"
aw-tool-tip="{{unreachableCountTip}}"
data-tip-watch="unreachableCountTip"></div>
<div class="HostStatusBar-skipped"
data-placement="top"
aw-tool-tip="{{skippedCountTip}}"
data-tip-watch="skippedCountTip"></div>
<div class="HostStatusBar-noData"
aw-tool-tip="NO HOSTS FINISHED"
ng-hide="hostsFinished"
data-placement="top"></div>
</div>

View File

@@ -0,0 +1,11 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import hostStatusBar from './host-status-bar.directive';
export default
angular.module('hostStatusBarDirective', [])
.directive('hostStatusBar', hostStatusBar);

View File

@@ -0,0 +1,240 @@
@import '../../shared/branding/colors.default.less';
@breakpoint-md: 1200px;
.JobResultsStdOut {
height: ~"calc(100% - 70px)";
}
.JobResultsStdOut-toolbar {
display: flex;
height: 38px;
margin-top: 15px;
border: 1px solid @default-list-header-bg;
border-bottom: 0px;
border-radius: 5px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.JobResultsStdOut-toolbarNumberColumn {
background-color: @default-list-header-bg;
color: @b7grey;
flex: initial;
display: flex;
justify-content: space-between;
width: 70px;
padding-bottom: 0px;
padding-left: 8px;
padding-right: 8px;
padding-top: 10px;
border-top-left-radius: 5px;
z-index: 1;
}
.JobResultsStdOut-expandAllButton {
height: 18px;
width: 18px;
padding-left: 4px;
padding-top: 1px;
border-radius: 50%;
background-color: @default-bg;
font-size: 12px;
cursor: pointer;
}
.JobResultsStdOut-expandAllButton:hover .JobResultsStdOut-expandAllIcon,
.JobResultsStdOut-expandAllIcon:hover {
color: @default-data-txt;
}
.JobResultsStdOut-toolbarStdoutColumn {
white-space: normal;
flex: 1;
display: flex;
justify-content: flex-end;
padding-right: 10px;
background-color: @default-no-items-bord;
}
.JobResultsStdOut-followButton {
cursor: pointer;
width: 18px;
height: 18px;
width: 18px;
padding-left: 3.8px;
border-radius: 50%;
margin-top: 10px;
font-size: 12px;
background-color: @default-icon;
color: @default-border;
}
.JobResultsStdOut-followIcon {
color: @default-border;
}
.JobResultsStdOut-followButton:hover {
background-color: @default-icon-hov;
}
.JobResultsStdOut-followButton:hover .JobResultsStdOut-followIcon,
.JobResultsStdOut-followIcon:hover {
color: @default-interface-txt;
}
.JobResultsStdOut-followButton.is-engaged {
background-color: @default-link;
color: @default-bg;
}
.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon {
color: @default-bg;
}
.JobResultsStdOut-followButton.is-engaged:hover {
background-color: @default-icon;
}
.JobResultsStdOut-followButton.is-engaged:hover .JobResultsStdOut-followIcon,
.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon:hover {
color: @default-border;
}
.JobResultsStdOut-stdoutContainer {
height: ~"calc(100% - 48px)";
background-color: @default-no-items-bord;
overflow-y: scroll;
overflow-x: hidden;
}
.JobResultsStdOut-numberColumnPreload {
background-color: @default-list-header-bg;
width: 70px;
position: fixed;
top: 148px;
bottom: 20px;
margin-top: 65px;
margin-bottom: 65px;
}
.JobResultsStdOut-aLineOfStdOut {
display: flex;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
.JobResultsStdOut-lineExpander {
text-align: left;
padding-left: 11px;
margin-right: auto;
}
.JobResultsStdOut-lineExpanderIcon {
font-size: 19px;
cursor: pointer;
}
.JobResultsStdOut-lineExpanderIcon:hover {
color: @default-data-txt;
}
.JobResultsStdOut-lineNumberColumn {
display: flex;
background-color: @default-list-header-bg;
text-align: right;
padding-right: 10px;
padding-top: 2px;
padding-bottom: 2px;
color: @b7grey;
width: 75px;
flex: initial;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
}
.JobResultsStdOut-stdoutColumn {
padding-left: 20px;
padding-top: 2px;
padding-bottom: 2px;
color: @default-interface-txt;
display: inline-block;
white-space: pre-wrap;
word-break: break-word;
width:100%;
}
.JobResultsStdOut-aLineOfStdOut:hover,
.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn {
background-color: @default-bg;
}
.JobResultsStdOut-footer {
height: 10px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
background-color: @default-no-items-bord;
border: 1px solid @default-list-header-bg;
border-top: 0px;
border-radius: 5px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.JobResultsStdOut-footerNumberColumn {
background-color: @default-list-header-bg;
width: 70px;
height: 100%;
}
.JobResultsStdOut-followAnchor {
height: 20px;
width: 100%;
}
.JobResultsStdOut-toTop {
margin-right: 20px;
color: #D7D7D7;
cursor: pointer;
text-align: right;
font-family: monaco;
}
.JobResultsStdOut-toTop:hover {
color: @default-interface-txt;
}
@media (max-width: @breakpoint-md) {
.JobResultsStdOut-numberColumnPreload {
display: none;
}
.JobResultsStdOut-topAnchor {
position: static;
width: 100%;
top: -20px;
margin-top: -250px;
margin-bottom: 250px;
}
.JobResultsStdOut-followAnchor {
height: 20px;
width: 100%;
border-left: 70px solid @default-list-header-bg;
}
.JobResultsStdOut-stdoutContainer {
overflow-y: auto;
}
.JobResultsStdOut-lineAnchor {
display: none !important;
}
}

View File

@@ -0,0 +1,400 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
// import hostStatusBarController from './host-status-bar.controller';
export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
function(templateUrl, $timeout, $location, $anchorScroll) {
return {
scope: false,
templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'),
restrict: 'E',
link: function(scope, element) {
// utility function used to find the top visible line and
// parent header in the pane
//
// note that while this function is called when in mobile width
// the line anchor is not displayed in the css so calls
// to lineAnchor do nothing
var findTopLines = function() {
var $container = $('.JobResultsStdOut-stdoutContainer');
// get the first visible line's head element
var getHeadElement = function (line) {
var lineHasHeaderClass = !!(line
.hasClass("header_play") ||
line.hasClass("header_task"));
var lineClassList;
var lineUUIDClass;
if (lineHasHeaderClass) {
// find head element when the first visible
// line is a header
lineClassList = line.attr("class")
.split(" ");
// get the header class w task uuid...
lineUUIDClass = lineClassList
.filter(n => n
.indexOf("header_task_") > -1)[0];
// ...if that doesn't exist get the one
// w play uuid
if (!lineUUIDClass) {
lineUUIDClass = lineClassList
.filter(n => n.
indexOf("header_play_") > -1)[0];
}
// get the header line (not their might
// be more than one, so get the one with
// the actual header class)
//
// TODO it might be better in this case to just
// return `line` (less jumping with a cowsay
// case)
return $(".actual_header." +
lineUUIDClass);
} else {
// find head element when the first visible
// line is not a header
lineClassList = line.attr("class")
.split(" ");
// get the class w task uuid...
lineUUIDClass = lineClassList
.filter(n => n
.indexOf("task_") > -1)[0];
// ...if that doesn't exist get the one
// w play uuid
if (!lineUUIDClass) {
lineUUIDClass = lineClassList
.filter(n => n
.indexOf("play_") > -1)[0];
}
// get the header line (not their might
// be more than one, so get the one with
// the actual header class)
return $(".actual_header.header_" +
lineUUIDClass);
}
};
var visItem,
parentItem;
var containerHeight = $container.height();
var containerTop = $container.position().top;
var containerNetHeight = containerHeight + containerTop;
// iterate through each line of standard out
$container.find('.JobResultsStdOut-aLineOfStdOut')
.each( function () {
var $this = $(this);
var lineHeight = $this.height();
var lineTop = $this.position().top;
var lineNetHeight = lineHeight + lineTop;
// check to see if the line is the first visible
// line in the viewport...
if (lineNetHeight > containerTop &&
lineTop < containerNetHeight) {
// ...if it is, return the line number
// for this line
visItem = parseInt($($this
.children()[0])
.text());
// as well as the line number for it's
// closest parent header line
var $head = getHeadElement($this);
parentItem = parseInt($($head
.children()[0])
.text());
// stop iterating over the standard out
// lines once the first one has been
// found
return false;
}
});
return {
visLine: visItem,
parentVisLine: parentItem
};
};
// find if window is initially mobile or desktop width
if (window.innerWidth <= 1200) {
scope.isMobile = true;
} else {
scope.isMobile = false;
}
// watch changes to the window size
$(window).resize(function() {
// and update the isMobile var accordingly
if (window.innerWidth <= 1200 && !scope.isMobile) {
scope.isMobile = true;
} else if (window.innerWidth > 1200 & scope.isMobile) {
scope.isMobile = false;
}
});
var lastScrollTop;
var initScrollTop = function() {
lastScrollTop = 0;
};
var scrollWatcher = function() {
var st = $(this).scrollTop();
var netScroll = st + $(this).innerHeight();
var fullHeight;
if (st < lastScrollTop){
// user up scrolled, so disengage follow
scope.followEngaged = false;
}
if (scope.isMobile) {
// for mobile the height is the body of the entire
// page
fullHeight = $("body").height();
} else {
// for desktop the height is the body of the
// stdoutContainer, minus the "^ TOP" indicator
fullHeight = $(this)[0].scrollHeight - 25;
}
if(netScroll >= fullHeight) {
// user scrolled all the way to bottom, so engage
// follow
scope.followEngaged = true;
}
// pane is now overflowed, show top indicator.
if (st > 0) {
scope.stdoutOverflowed = true;
}
lastScrollTop = st;
};
// update scroll watchers when isMobile changes based on
// window resize
scope.$watch('isMobile', function(val) {
if (val === true) {
// make sure ^ TOP always shown for mobile
scope.stdoutOverflowed = true;
// unbind scroll watcher on standard out container
$(".JobResultsStdOut-stdoutContainer")
.unbind('scroll');
// init scroll watcher on window
initScrollTop();
$(window).on('scroll', scrollWatcher);
} else if (val === false) {
// unbind scroll watcher on window
$(window).unbind('scroll');
// init scroll watcher on standard out container
initScrollTop();
$(".JobResultsStdOut-stdoutContainer").on('scroll',
scrollWatcher);
}
});
// called to scroll to follow anchor
scope.followScroll = function() {
// a double-check to make sure the follow anchor is at
// the bottom of the standard out container
$(".JobResultsStdOut-followAnchor")
.appendTo(".JobResultsStdOut-stdoutContainer");
$location.hash('followAnchor');
$anchorScroll();
};
// called to scroll to top of standard out (when "^ TOP" is
// clicked)
scope.toTop = function() {
$location.hash('topAnchor');
$anchorScroll();
};
// called to scroll to the current line anchor
// when expand all/collapse all/filtering is engaged
//
// note that while this function can be called when in mobile
// width the line anchor is not displayed in the css so those
// calls do nothing
scope.lineAnchor = function() {
$location.hash('lineAnchor');
$anchorScroll();
};
// if following becomes active, go ahead and get to the bottom
// of the standard out pane
scope.$watch('followEngaged', function(val) {
// scroll to follow point if followEngaged is true
if (val) {
scope.followScroll();
}
// set up tooltip changes for not finsihed job
if (!scope.jobFinished) {
if (val) {
scope.followTooltip = "Currently following standard out as it comes in. Click to unfollow.";
} else {
scope.followTooltip = "Click to follow standard out as it comes in.";
}
}
});
// follow button ng-click function
scope.followToggleClicked = function() {
if (scope.jobFinished) {
// when the job is finished engage followScroll
scope.followScroll();
} else {
// when the job is not finished toggle followEngaged
// which is watched above
scope.followEngaged = !scope.followEngaged;
}
};
// expand all/collapse all ng-click function
scope.toggleAllStdout = function(type) {
// find the top visible line in the container currently,
// as well as the header parent of that line
var topLines = findTopLines();
if (type === 'expand') {
// for expand prepend the lineAnchor to the visible
// line
$(".line_num_" + topLines.visLine)
.prepend($("#lineAnchor"));
} else {
// for collapse prepent the lineAnchor to the
// visible line's parent header
$(".line_num_" + topLines.parentVisLine)
.prepend($("#lineAnchor"));
}
var expandClass;
if (type === 'expand') {
// for expand all, you'll need to find all the
// collapsed headers to expand them
expandClass = "fa-caret-right";
} else {
// for collapse all, you'll need to find all the
// expanded headers to collapse them
expandClass = "fa-caret-down";
}
// find all applicable task headers that need to be
// toggled
element.find(".expanderizer--task."+expandClass)
.each((i, val) => {
// and trigger their expansion/collapsing
$timeout(function(){
// TODO change to a direct call of the
// toggleLine function
angular.element(val).trigger('click');
// TODO only call lineAnchor for those
// that are above the first visible line
scope.lineAnchor();
});
});
// find all applicable play headers that need to be
// toggled
element.find(".expanderizer--play."+expandClass)
.each((i, val) => {
// for collapse all, only collapse play
// headers that do not have children task
// headers
if(angular.element("." +
angular.element(val).attr("data-uuid"))
.find(".expanderizer--task")
.length === 0 ||
type !== 'collapse') {
// trigger their expansion/
// collapsing
$timeout(function(){
// TODO change to a direct
// call of the
// toggleLine function
angular.element(val)
.trigger('click');
// TODO only call lineAnchor
// for those that are above
// the first visible line
scope.lineAnchor();
});
}
});
};
// expand/collapse triangle ng-click function
scope.toggleLine = function($event, id) {
// if the section is currently expanded
if ($($event.currentTarget).hasClass("fa-caret-down")) {
// hide all the children lines
$(id).hide();
// and change the triangle for the header to collapse
$($event.currentTarget)
.removeClass("fa-caret-down");
$($event.currentTarget)
.addClass("fa-caret-right");
} else {
// if the section is currently collapsed
// show all the children lines
$(id).show();
// and change the triangle for the header to expanded
$($event.currentTarget)
.removeClass("fa-caret-right");
$($event.currentTarget)
.addClass("fa-caret-down");
// if the section you expanded is a play
if ($($event.currentTarget)
.hasClass("expanderizer--play")) {
// find all children task headers and
// expand them too
$("." + $($event.currentTarget)
.attr("data-uuid"))
.find(".expanderizer--task")
.each((i, val) => {
if ($(val)
.hasClass("fa-caret-right")) {
$timeout(function(){
// TODO change to a
// direct call of the
// toggleLine function
angular.element(val)
.trigger('click');
});
}
});
}
}
};
}
};
}];

View File

@@ -0,0 +1,49 @@
<div class="JobResultsStdOut">
<div class="JobResultsStdOut-toolbar">
<div class="JobResultsStdOut-toolbarNumberColumn">
<div class="JobResultsStdOut-expandAllButton"
ng-click="toggleAllStdout('expand')"
aw-tool-tip="Expand all lines of standard out."
data-placement="top">
<i class ="JobResultsStdOut-expandAllIcon fa fa-plus">
</i>
</div>
<div class="JobResultsStdOut-expandAllButton"
ng-click="toggleAllStdout('collapse')"
aw-tool-tip="Collapse all lines of standard out except play and task headers."
data-placement="top">
<i class ="JobResultsStdOut-expandAllIcon fa fa-minus">
</i>
</div>
</div>
<div class="JobResultsStdOut-toolbarStdoutColumn">
<div class="JobResultsStdOut-followButton"
ng-class="{'is-engaged': followEngaged && !jobFinished}"
aw-tool-tip="{{ followTooltip }}"
data-tip-watch="followTooltip"
data-placement="left"
data-trigger="hover"
data-container="body"
ng-click="followToggleClicked()">
<i class="JobResultsStdOut-followIcon fa fa-arrow-down">
</i>
</div>
</div>
</div>
<div class="JobResultsStdOut-stdoutContainer">
<div id="topAnchor" class="JobResultsStdOut-topAnchor"></div>
<div class="JobResultsStdOut-numberColumnPreload"></div>
<div id='lineAnchor' class="JobResultsStdOut-lineAnchor"></div>
<div id="followAnchor"
class="JobResultsStdOut-followAnchor">
<div class="JobResultsStdOut-toTop"
ng-click="toTop()"
ng-show="stdoutOverflowed">
^ TOP
</div>
</div>
</div>
<div class="JobResultsStdOut-footer">
<div class="JobResultsStdOut-footerNumberColumn"></div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import jobResultsStdOut from './job-results-stdout.directive';
export default
angular.module('jobResultStdOutDirective', [])
.directive('jobResultsStandardOut', jobResultsStdOut);

View File

@@ -0,0 +1,125 @@
@import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less';
@import '../shared/layouts/one-plus-two.less';
@breakpoint-md: 1200px;
.JobResults {
.OnePlusTwo-container(100%, @breakpoint-md);
&.fullscreen {
.JobResults-rightSide {
max-width: 100%;
}
}
}
.JobResults-leftSide {
.OnePlusTwo-left--panel(100%, @breakpoint-md);
height: ~"calc(100vh - 177px)";
}
.JobResults-rightSide {
.OnePlusTwo-right--panel(100%, @breakpoint-md);
height: ~"calc(100vh - 177px)";
@media (max-width: @breakpoint-md - 1px) {
padding-right: 15px;
}
}
.JobResults-detailsPanel{
overflow-y: scroll;
}
.JobResults-stdoutActionButton--active {
display: none;
visibility: hidden;
flex:none;
width:0px;
padding-right: 0px;
}
.JobResults-panelHeader {
display: flex;
height: 30px;
}
.JobResults-panelHeaderText {
color: @default-interface-txt;
flex: 1 0 auto;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.JobResults-resultRow {
width: 100%;
display: flex;
padding-bottom: 10px;
flex-wrap: wrap;
}
.JobResults-resultRow--variables {
flex-direction: column;
}
.JobResults-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;
}
}
.JobResults-resultRowLabel--fullWidth {
width: 100%;
margin-right: 0px;
}
.JobResults-resultRowText {
width: ~"calc(70% - 20px)";
flex: 1 0 auto;
text-transform: none;
word-wrap: break-word;
}
.JobResults-resultRowText--fullWidth {
width: 100%;
}
.JobResults-statusResultIcon {
padding-left: 0px;
padding-right: 10px;
}
.JobResults-badgeRow {
display: flex;
align-items: center;
margin-right: 5px;
}
.JobResults-badgeTitle{
color: @default-interface-txt;
font-size: 14px;
margin-right: 10px;
font-weight: normal;
text-transform: uppercase;
margin-left: 20px;
}
@media (max-width: @breakpoint-md) {
.JobResults-detailsPanel {
overflow-y: auto;
}
.JobResults-rightSide {
height: inherit;
}
}

View File

@@ -0,0 +1,202 @@
export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile) {
var getTowerLinks = function() {
var getTowerLink = function(key) {
if ($scope.job.related[key]) {
return '/#/' + $scope.job.related[key]
.split('api/v1/')[1];
} else {
return null;
}
};
$scope.job_template_link = getTowerLink('job_template');
$scope.created_by_link = getTowerLink('created_by');
$scope.inventory_link = getTowerLink('inventory');
$scope.project_link = getTowerLink('project');
$scope.machine_credential_link = getTowerLink('credential');
$scope.cloud_credential_link = getTowerLink('cloud_credential');
$scope.network_credential_link = getTowerLink('network_credential');
};
// uses options to set scope variables to their readable string
// value
var getTowerLabels = function() {
var getTowerLabel = function(key) {
if ($scope.jobOptions && $scope.jobOptions[key]) {
return $scope.jobOptions[key].choices
.filter(val => val[0] === $scope.job[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.job = jobData;
$scope.jobOptions = jobDataOptions.actions.GET;
$scope.labels = jobLabels;
$scope.jobFinished = jobFinished;
// 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.job.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() {
jobResultsService.deleteJob($scope.job);
};
$scope.cancelJob = function() {
jobResultsService.cancelJob($scope.job);
};
$scope.relaunchJob = function() {
jobResultsService.relaunchJob($scope);
};
// get initial count from resolve
$scope.count = count.val;
$scope.hostCount = getTotalHostCount(count.val);
$scope.countFinished = count.countFinished;
// if the job is still running engage following of the last line in the
// standard out pane
$scope.followEngaged = !$scope.jobFinished;
// follow button for completed job should specify that the
// button will jump to the bottom of the standard out pane,
// not follow lines as they come in
if ($scope.jobFinished) {
$scope.followTooltip = "Jump to last line of standard out.";
} else {
$scope.followTooltip = "Currently following standard out as it comes in. Click to unfollow.";
}
// EVENT STUFF BELOW
// 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.job.start) {
$scope.job.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.job.finished) {
$scope.job.finished = mungedEvent.finishedTime;
$scope.jobFinished = true;
$scope.followTooltip = "Jump to last line of standard out.";
}
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'){
// put stdout elements in stdout container
angular
.element(".JobResultsStdOut-stdoutContainer")
.append($compile(mungedEvent
.stdout)($scope));
// move the followAnchor to the bottom of the
// container
$(".JobResultsStdOut-followAnchor")
.appendTo(".JobResultsStdOut-stdoutContainer");
// if follow is engaged,
// scroll down to the followAnchor
if ($scope.followEngaged) {
$scope.followScroll();
}
}
});
}
// 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) {
jobResultsService.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;
delete 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.job.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.job.id,10)) {
$scope.job.status = data.status;
}
});
}];

View File

@@ -0,0 +1,522 @@
<div class="tab-pane" id="job-results">
<div ng-cloak
id="htmlTemplate"
class="JobResults"
ng-class="{'fullscreen': stdoutFullScreen}">
<div ui-view></div>
<!-- LEFT PANE -->
<div class="JobResults-leftSide"
ng-class="{'JobResults-stdoutActionButton--active': stdoutFullScreen}">
<div class="JobResults-detailsPanel Panel"
ng-show="!stdoutFullScreen">
<!-- LEFT PANE HEADER -->
<div class="JobResults-panelHeader">
<div
class="JobResults-panelHeaderText">
RESULTS
</div>
<!-- LEFT PANE HEADER ACTIONS -->
<div>
<!-- RELAUNCH ACTION -->
<button class="List-actionButton"
data-placement="top"
mode="all"
ng-click="relaunchJob()"
aw-tool-tip="Relaunch using the same parameters"
data-original-title=""
title="">
<i class="icon-launch"></i>
</button>
<!-- CANCEL ACTION -->
<button class="List-actionButton
List-actionButton--delete"
data-placement="top"
ng-click="deleteJob()"
ng-show="job_status.status == 'running' ||
job_status.status=='pending' "
aw-tool-tip="Cancel"
data-original-title="" title="">
<i class="fa fa-minus-circle"></i>
</button>
<!-- DELETE ACTION -->
<button class="List-actionButton
List-actionButton--delete"
data-placement="top"
ng-click="deleteJob()"
ng-hide="job_status.status == 'running' ||
job_status.status == 'pending' "
aw-tool-tip="Delete"
data-original-title=""
title="">
<i class="fa fa-trash-o"></i>
</button>
</div>
</div>
<!-- LEFT PANE DETAILS GROUP -->
<div>
<!-- START TIME DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.started">
<label class="JobResults-resultRowLabel">
Started
</label>
<div class="JobResults-resultRowText">
{{ job.started | longDate }}
</div>
</div>
<!-- FINISHED TIME DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.started">
<label class="JobResults-resultRowLabel">
Finished
</label>
<div class="JobResults-resultRowText">
{{ (job.finished |
longDate) || "Not Finished" }}
</div>
</div>
<!-- TEMPLATE DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.job_template.name">
<label class="JobResults-resultRowLabel">
Template
</label>
<div class="JobResults-resultRowText">
<a href="{{ job_template_link }}"
aw-tool-tip="Edit the job template"
data-placement="top">
{{ job.summary_fields.job_template.name }}
</a>
</div>
</div>
<!-- JOB TYPE DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.job_type">
<label class="JobResults-resultRowLabel">
Job Type
</label>
<div class="JobResults-resultRowText">
{{ type_label }}
</div>
</div>
<!-- CREATED BY DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.summary_fields.created_by.username">
<label class="JobResults-resultRowLabel">
Launched By
</label>
<div class="JobResults-resultRowText">
<a href="{{ created_by_link }}"
aw-tool-tip="Edit the User"
data-placement="top">
{{ job.summary_fields.created_by.username }}
</a>
</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 -->
<div class="JobResults-resultRow
JobResults-resultRow--variables"
ng-show="variables">
<label class="JobResults-resultRowLabel
JobResults-resultRowLabel--fullWidth">
Extra Variables
</label>
<textarea
rows="6"
ng-model="variables"
name="variables"
class="JobResults-extraVars"
id="pre-formatted-variables">
</textarea>
</div>
<!-- LABELS DETAIL -->
<div class="JobResults-resultRow"
ng-show="labels && labels.length > 0">
<label class="JobResults-resultRowLabel
JobResults-resultRowLabel--fullWidth">
Labels
</label>
<div class="LabelList
JobResults-resultRowText
JobResults-resultRowText--fullWidth">
<div ng-repeat="label in labels"
class="LabelList-tagContainer">
<div class="LabelList-tag">
<div class="LabelList-name">
{{ label }}
</div>
</div>
</div>
</div>
</div>
<!-- STATUS DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Status
</label>
<div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9">
<i
class="JobResults-statusIcon--results
fa
icon-job-{{ job.status }}">
</i> {{ status_label }}
</div>
</div> -->
<!-- SCHEDULED BY DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow toggle-show"
ng-show="job.summary_fields.schedule_by.username">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Launched By
</label>
<div class="JobResults-resultRowText">
<a href="{{ scheduled_by_link }}"
aw-tool-tip="Edit the Schedule"
data-placement="top">
{{ job.summary_fields.scheduled_by.username }}
</a>
</div>
</div> -->
<!-- ELAPSED TIME DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow toggle-show"
ng-show="job_status.started">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Elapsed
</label>
<div class="JobResults-resultRowText">
{{ job_status.elapsed }}
</div>
</div> -->
<!-- EXPLANATION DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show"
ng-show="job_status.explanation">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Explanation
</label> -->
<!-- PREVIOUS TASK SUCCEEDED -->
<!-- <div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation"
ng-show="!previousTaskFailed"
ng-bind-html="job_status.explanation">
<i
class="JobResults-statusIcon--results
fa
icon-job-{{ job_status.status }}">
</i> {{ job_status.status_label }}
</div> -->
<!-- PREVIOUS TASK FAILED -->
<!-- <div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation"
ng-show="previousTaskFailed">
Previous Task Failed
<a
href=""
id="explanation_help"
aw-pop-over="{{ task_detail }}"
aw-pop-over-watch="task_detail"
data-placement="bottom"
data-container="body"
class="help-link"
over-title="Failure Detail"
title=""
tabindex="-1">
<i class="fa fa-question-circle">
</i>
</a>
</div> -->
<!-- </div> -->
<!-- RESULTS TRACEBACK DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show" ng-show="job.result_traceback">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-12
col-sm-12 col-xs-12">
Results Traceback
</label>
<div class="JobResults-resultRowText
col-lg-10 col-md-12 col-sm-12 col-xs-12
job_status_traceback"
ng-bind-html="job.result_traceback">
</div>
</div> -->
</div>
</div>
</div>
<!-- RIGHT PANE -->
<div class="JobResults-rightSide">
<div class="Panel">
<!-- RIGHT PANE HEADER -->
<div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText">
<i class="JobResults-statusResultIcon
fa icon-job-{{ job.status }}">
</i>
{{ job.name }}
</div>
<!-- HEADER COUNTS -->
<div class="JobResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="JobResults-badgeTitle">
Plays
</div>
<span class="badge List-titleBadge">
{{ playCount || 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>
<!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
</span>
</div>
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="Toggle Output"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
<!-- DOWNLOAD ACTION -->
<a ng-show="job.status === 'failed' ||
job.status === 'successful' ||
job.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
<button class="StandardOut-actionButton"
aw-tool-tip="Download Output"
data-placement="top">
<i class="fa fa-download"></i>
</button>
</a>
</div>
</div>
<host-status-bar></host-status-bar>
<job-results-standard-out></job-results-standard-out>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,158 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../shared/template-url/template-url.factory';
export default {
name: 'jobDetail',
url: '/jobs/:id',
ncyBreadcrumb: {
parent: 'jobs',
label: '{{ job.id }} - {{ job.name }}'
},
data: {
socket: {
"groups":{
"jobs": ["status_changed", "summary"],
"job_events": []
}
}
},
resolve: {
// the GET for the particular job
jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) {
Rest.setUrl(GetBasePath('jobs') + $stateParams.id);
var val = $q.defer();
Rest.get()
.then(function(data) {
val.resolve(data.data);
}, function(data) {
val.reject(data);
if (data.status === 404) {
Alert('Job Not Found', 'Cannot find job.', 'alert-info');
} else if (data.status === 403) {
Alert('Insufficient Permissions', 'You do not have permission to view this job.', 'alert-info');
}
$state.go('jobs');
});
return val.promise;
}],
// used to signify if job is completed or still running
jobFinished: ['jobData', function(jobData) {
if (jobData.finished) {
return true;
} else {
return false;
}
}],
// 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
// there's a playbook_on_stats event, go ahead and resolve the count
// so you don't get that flashing!
count: ['jobData', 'jobResultsService', 'Rest', '$q', function(jobData, jobResultsService, Rest, $q) {
var defer = $q.defer();
if (jobData.finished) {
// if the job is finished, grab the playbook_on_stats
// role to get the final count
Rest.setUrl(jobData.related.job_events +
"?event=playbook_on_stats");
Rest.get()
.success(function(data) {
if(!data.results[0]){
defer.resolve({val: {
ok: 0,
skipped: 0,
unreachable: 0,
failures: 0,
changed: 0
}, countFinished: false});
}
else {
defer.resolve({
val: jobResultsService
.getCountsFromStatsEvent(data
.results[0].event_data),
countFinished: true});
}
})
.error(function() {
defer.resolve({val: {
ok: 0,
skipped: 0,
unreachable: 0,
failures: 0,
changed: 0
}, countFinished: false});
});
} else {
// job isn't finished so just send an empty count and read
// from events
defer.resolve({val: {
ok: 0,
skipped: 0,
unreachable: 0,
failures: 0,
changed: 0
}, countFinished: false});
}
return defer.promise;
}],
// GET for the particular jobs labels to be displayed in the
// left-hand pane
jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
var getNext = function(data, arr, resolve) {
Rest.setUrl(data.next);
Rest.get()
.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();
}]
},
templateUrl: templateUrl('job-results/job-results'),
controller: 'jobResultsController'
};

View File

@@ -0,0 +1,197 @@
/*************************************************
* 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;
},
// rest call to grab previously complete job_events
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;
}];

View File

@@ -0,0 +1,27 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import hostStatusBar from './host-status-bar/main';
import jobResultsStdOut from './job-results-stdout/main';
import hostEvent from './host-event/main';
import route from './job-results.route.js';
import jobResultsController from './job-results.controller';
import jobResultsService from './job-results.service';
import eventQueueService from './event-queue.service';
import parseStdoutService from './parse-stdout.service';
export default
angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, hostEvent.name])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}])
.controller('jobResultsController', jobResultsController)
.service('jobResultsService', jobResultsService)
.service('eventQueue', eventQueueService)
.service('parseStdoutService', parseStdoutService);

View File

@@ -0,0 +1,208 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$log', function($log){
var val = {
// parses stdout string from api and formats various codes to the
// correct dom structure
prettify: function(line, unstyled){
line = line
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
// TODO: remove once Chris's fixes to the [K lines comes in
if (line.indexOf("[K") > -1) {
$log.error(line);
}
if(!unstyled){
// add span tags with color styling
line = line.replace(/u001b/g, '');
// ansi classes
line = line.replace(/\[1;31m/g, '<span class="ansi1 ansi31">');
line = line.replace(/\[0;31m/g, '<span class="ansi1 ansi31">');
line = line.replace(/\[0;32m/g, '<span class="ansi32">');
line = line.replace(/\[0;32m=/g, '<span class="ansi32">');
line = line.replace(/\[0;32m1/g, '<span class="ansi36">');
line = line.replace(/\[0;33m/g, '<span class="ansi33">');
line = line.replace(/\[0;34m/g, '<span class="ansi34">');
line = line.replace(/\[0;35m/g, '<span class="ansi35">');
line = line.replace(/\[0;36m/g, '<span class="ansi36">');
line = line.replace(/(<host.*?>)\s/g, '$1');
//end span
line = line.replace(/\[0m/g, '</span>');
} else {
// For the host event modal in the standard out tab,
// the styling isn't necessary
line = line.replace(/u001b/g, '');
// ansi classes
line = line.replace(/\[1;31m/g, '');
line = line.replace(/\[0;31m/g, '');
line = line.replace(/\[0;32m/g, '');
line = line.replace(/\[0;32m=/g, '');
line = line.replace(/\[0;32m1/g, '');
line = line.replace(/\[0;33m/g, '');
line = line.replace(/\[0;34m/g, '');
line = line.replace(/\[0;35m/g, '');
line = line.replace(/\[0;36m/g, '');
line = line.replace(/(<host.*?>)\s/g, '$1');
//end span
line = line.replace(/\[0m/g, '');
}
return line;
},
// adds anchor tags and tooltips to host status lines
getAnchorTags: function(event, line){
if(event.event_name.indexOf("runner_") === -1){
return line;
}
else{
return `<a ui-sref="jobDetail.host-event.json({eventId: ${event.id}, taskId: ${event.parent} })" aw-tool-tip="Event ID: ${event.id} <br>Status: ${event.event_display} <br>Click for details" data-placement="top">${line}</a>`;
}
},
// this adds classes based on event data to the
// .JobResultsStdOut-aLineOfStdOut element
getLineClasses: function(event, line, lineNum) {
var string = "";
if (event.event_name === "playbook_on_play_start") {
// play header classes
string += " header_play";
string += " header_play_" + event.event_data.play_uuid;
// give the actual header class to the line with the
// actual header info (think cowsay)
if (line.indexOf("PLAY") > -1) {
string += " actual_header";
}
} else if (event.event_name === "playbook_on_task_start") {
// task header classes
string += " header_task";
string += " header_task_" + event.event_data.task_uuid;
// give the actual header class to the line with the
// actual header info (think cowsay)
if (line.indexOf("TASK") > -1 ||
line.indexOf("RUNNING HANDLER") > -1) {
string += " actual_header";
}
// task headers also get classed by their parent play
// if applicable
if (event.event_data.play_uuid) {
string += " play_" + event.event_data.play_uuid;
}
} else {
// host status or debug line
// these get classed by their parent play if applicable
if (event.event_data.play_uuid) {
string += " play_" + event.event_data.play_uuid;
}
// as well as their parent task if applicable
if (event.event_data.task_uuid) {
string += " task_" + event.event_data.task_uuid;
}
}
// TODO: adding this line_num_XX class is hacky because the
// line number is availabe in children of this dom element
string += " line_num_" + lineNum;
return string;
},
// used to add expand/collapse icon next to line numbers of headers
getCollapseIcon: function(event, line) {
var clickClass,
expanderizerSpecifier;
var emptySpan = `
<span class="JobResultsStdOut-lineExpander"></span>`;
if ((event.event_name === "playbook_on_play_start" ||
event.event_name === "playbook_on_task_start") &&
line !== "") {
if (event.event_name === "playbook_on_play_start" &&
line.indexOf("PLAY") > -1) {
// play header specific attrs
expanderizerSpecifier = "play";
clickClass = "play_" +
event.event_data.play_uuid;
} else if (line.indexOf("TASK") > -1 ||
line.indexOf("RUNNING HANDLER") > -1) {
// task header specific attrs
expanderizerSpecifier = "task";
clickClass = "task_" +
event.event_data.task_uuid;
} else {
// header lines that don't have PLAY, TASK,
// or RUNNING HANDLER in them don't get
// expand icon.
// This provides cowsay support.
return emptySpan;
}
var expandDom = `
<span class="JobResultsStdOut-lineExpander">
<i class="JobResultsStdOut-lineExpanderIcon fa fa-caret-down expanderizer
expanderizer--${expanderizerSpecifier} expanded"
ng-click="toggleLine($event, '.${clickClass}')"
data-uuid="${clickClass}">
</i>
</span>`;
// console.log(expandDom);
return expandDom;
} else {
// non-header lines don't get an expander
return emptySpan;
}
},
getLineArr: function(event) {
return _
.zip(_.range(event.start_line + 1,
event.end_line + 1),
event.stdout.replace("\t", " ").split("\r\n").slice(0, -1));
},
// public function that provides the parsed stdout line, given a
// job_event
parseStdout: function(event){
// this utilizes the start/end lines and stdout blob
// to create an array in the format:
// [
// [lineNum, lineText],
// [lineNum, lineText],
// ]
var lineArr = this.getLineArr(event);
// this takes each `[lineNum: lineText]` element and calls the
// relevant helper functions in this service to build the
// parsed line of standard out
lineArr = lineArr
.map(lineArr => {
return `
<div class="JobResultsStdOut-aLineOfStdOut${this.getLineClasses(event, lineArr[1], lineArr[0])}">
<div class="JobResultsStdOut-lineNumberColumn">${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}</div>
<div class="JobResultsStdOut-stdoutColumn">${this.getAnchorTags(event, this.prettify(lineArr[1]))}</div>
</div>`;
});
// this joins all the lines for this job_event together and
// returns to the mungeEvent function
return lineArr.join("");
}
};
return val;
}];

View File

@@ -492,7 +492,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
.directive('awToolTip', [function() {
return {
link: function(scope, element, attrs) {
var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : { show: 500, hide: 100 },
// if (attrs.class.indexOf("JobResultsStdOut") > -1) {
// debugger;
// }
var delay = { show: 200, hide: 0 },
placement,
stateChangeWatcher;
if (attrs.awTipPlacement) {

View File

@@ -20,6 +20,7 @@ import templateUrl from './template-url/main';
import RestServices from '../rest/main';
import stateDefinitions from './stateDefinitions.factory';
import apiLoader from './api-loader';
import 'angular-duration-format';
export default
angular.module('shared', [listGenerator.name,
@@ -36,6 +37,7 @@ angular.module('shared', [listGenerator.name,
RestServices.name,
apiLoader.name,
require('angular-cookies'),
'angular-duration-format'
])
.factory('stateDefinitions', stateDefinitions)
.factory('lodashAsPromised', lodashAsPromised)

View File

@@ -86,6 +86,11 @@
"from": "leigh-johnson/angular-drag-and-drop-lists#1.4.0",
"resolved": "git://github.com/leigh-johnson/angular-drag-and-drop-lists.git#4d32654ab7159689a7767b9be8fc85f9812ca5a8"
},
"angular-duration-format": {
"version": "1.0.1",
"from": "angular-duration-format@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/angular-duration-format/-/angular-duration-format-1.0.1.tgz"
},
"angular-filters": {
"version": "1.1.2",
"from": "angular-filters@>=1.1.2 <2.0.0",
@@ -102,9 +107,9 @@
"resolved": "https://registry.npmjs.org/angular-gettext-tools/-/angular-gettext-tools-2.3.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -113,6 +118,11 @@
"from": "angular-md5@>=0.1.8 <0.2.0",
"resolved": "https://registry.npmjs.org/angular-md5/-/angular-md5-0.1.10.tgz"
},
"angular-mocks": {
"version": "1.5.8",
"from": "angular-mocks@>=1.5.8 <2.0.0",
"resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.5.8.tgz"
},
"angular-moment": {
"version": "0.10.3",
"from": "angular-moment@>=0.10.1 <0.11.0",
@@ -207,9 +217,9 @@
"resolved": "https://registry.npmjs.org/archiver/-/archiver-1.1.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -219,9 +229,9 @@
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -311,9 +321,9 @@
"resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.14.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -333,9 +343,9 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
},
"autoprefixer": {
"version": "6.5.1",
"version": "6.5.3",
"from": "autoprefixer@>=6.0.0 <7.0.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.5.1.tgz"
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.5.3.tgz"
},
"aws-sign2": {
"version": "0.6.0",
@@ -358,9 +368,9 @@
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.18.2.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
},
"source-map": {
"version": "0.5.6",
@@ -375,9 +385,9 @@
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
},
"source-map": {
"version": "0.5.6",
@@ -397,9 +407,9 @@
"resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -429,9 +439,9 @@
"resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -445,6 +455,60 @@
"from": "babel-helpers@>=6.16.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.16.0.tgz"
},
"babel-istanbul": {
"version": "0.11.0",
"from": "babel-istanbul@>=0.11.0 <0.12.0",
"resolved": "https://registry.npmjs.org/babel-istanbul/-/babel-istanbul-0.11.0.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
"from": "async@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
},
"minimist": {
"version": "0.0.8",
"from": "minimist@0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
},
"mkdirp": {
"version": "0.5.1",
"from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
},
"source-map": {
"version": "0.4.4",
"from": "source-map@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz"
},
"supports-color": {
"version": "3.1.2",
"from": "supports-color@>=3.1.0 <3.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz"
},
"wordwrap": {
"version": "1.0.0",
"from": "wordwrap@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
}
}
},
"babel-loader": {
"version": "6.2.7",
"from": "babel-loader@>=6.2.4 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.2.7.tgz",
"dependencies": {
"minimist": {
"version": "0.0.8",
"from": "minimist@0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
},
"mkdirp": {
"version": "0.5.1",
"from": "mkdirp@>=0.5.1 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
}
}
},
"babel-messages": {
"version": "6.8.0",
"from": "babel-messages@>=6.8.0 <7.0.0",
@@ -455,6 +519,11 @@
"from": "babel-plugin-check-es2015-constants@>=6.3.13 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz"
},
"babel-plugin-istanbul": {
"version": "2.0.3",
"from": "babel-plugin-istanbul@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-2.0.3.tgz"
},
"babel-plugin-transform-es2015-arrow-functions": {
"version": "6.8.0",
"from": "babel-plugin-transform-es2015-arrow-functions@>=6.3.13 <7.0.0",
@@ -471,9 +540,9 @@
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -582,15 +651,20 @@
"from": "babel-plugin-transform-strict-mode@>=6.18.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.18.0.tgz"
},
"babel-preset-es2015": {
"version": "6.18.0",
"from": "babel-preset-es2015@>=6.9.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.18.0.tgz"
},
"babel-register": {
"version": "6.18.0",
"from": "babel-register@>=6.18.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
},
"minimist": {
"version": "0.0.8",
@@ -615,9 +689,9 @@
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.16.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -627,9 +701,9 @@
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -639,9 +713,9 @@
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.18.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -691,9 +765,9 @@
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz"
},
"beeper": {
"version": "1.1.0",
"version": "1.1.1",
"from": "beeper@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.0.tgz"
"resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz"
},
"benchmark": {
"version": "1.0.0",
@@ -795,9 +869,9 @@
"resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz"
},
"browser-sync": {
"version": "2.17.5",
"version": "2.17.6",
"from": "browser-sync@>=2.14.0 <3.0.0",
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.17.5.tgz"
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.17.6.tgz"
},
"browser-sync-client": {
"version": "2.4.3",
@@ -820,9 +894,9 @@
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.4.0.tgz"
},
"bs-recipes": {
"version": "1.2.3",
"from": "bs-recipes@1.2.3",
"resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.2.3.tgz"
"version": "1.3.2",
"from": "bs-recipes@1.3.2",
"resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.2.tgz"
},
"buffer": {
"version": "4.9.1",
@@ -872,9 +946,9 @@
}
},
"caniuse-db": {
"version": "1.0.30000576",
"from": "caniuse-db@>=1.0.30000554 <2.0.0",
"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000576.tgz"
"version": "1.0.30000581",
"from": "caniuse-db@>=1.0.30000578 <2.0.0",
"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000581.tgz"
},
"caseless": {
"version": "0.11.0",
@@ -947,9 +1021,9 @@
"resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.5.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -1483,6 +1557,11 @@
"from": "expand-range@>=1.8.1 <2.0.0",
"resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz"
},
"expose-loader": {
"version": "0.7.1",
"from": "expose-loader@>=0.7.1 <0.8.0",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.1.tgz"
},
"express": {
"version": "2.5.11",
"from": "express@>=2.5.0 <2.6.0",
@@ -2343,9 +2422,9 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz"
},
"globals": {
"version": "9.12.0",
"version": "9.13.0",
"from": "globals@>=9.0.0 <10.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.12.0.tgz"
"resolved": "https://registry.npmjs.org/globals/-/globals-9.13.0.tgz"
},
"globule": {
"version": "1.1.0",
@@ -2369,6 +2448,33 @@
"from": "graceful-readlink@>=1.0.0",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
},
"grunt": {
"version": "1.0.1",
"from": "grunt@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.1.tgz",
"dependencies": {
"glob": {
"version": "7.0.6",
"from": "glob@>=7.0.0 <7.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz"
},
"js-yaml": {
"version": "3.5.5",
"from": "js-yaml@>=3.5.2 <3.6.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz"
},
"rimraf": {
"version": "2.2.8",
"from": "rimraf@>=2.2.8 <2.3.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
}
}
},
"grunt-angular-gettext": {
"version": "2.3.0",
"from": "grunt-angular-gettext@>=2.2.3 <3.0.0",
"resolved": "https://registry.npmjs.org/grunt-angular-gettext/-/grunt-angular-gettext-2.3.0.tgz"
},
"grunt-browser-sync": {
"version": "2.2.0",
"from": "grunt-browser-sync@>=2.2.0 <3.0.0",
@@ -2379,6 +2485,64 @@
"from": "grunt-cli@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz"
},
"grunt-concurrent": {
"version": "2.3.1",
"from": "grunt-concurrent@>=2.3.0 <3.0.0",
"resolved": "https://registry.npmjs.org/grunt-concurrent/-/grunt-concurrent-2.3.1.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
"from": "async@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
}
}
},
"grunt-contrib-clean": {
"version": "1.0.0",
"from": "grunt-contrib-clean@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.0.0.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
"from": "async@>=1.5.2 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
}
}
},
"grunt-contrib-concat": {
"version": "1.0.1",
"from": "grunt-contrib-concat@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz",
"dependencies": {
"source-map": {
"version": "0.5.6",
"from": "source-map@>=0.5.3 <0.6.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
}
}
},
"grunt-contrib-copy": {
"version": "1.0.0",
"from": "grunt-contrib-copy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz"
},
"grunt-contrib-jshint": {
"version": "1.0.0",
"from": "grunt-contrib-jshint@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-1.0.0.tgz"
},
"grunt-contrib-less": {
"version": "1.4.0",
"from": "grunt-contrib-less@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.0.tgz",
"dependencies": {
"lodash": {
"version": "4.17.1",
"from": "lodash@>=4.8.2 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
"grunt-contrib-watch": {
"version": "1.0.0",
"from": "grunt-contrib-watch@>=1.0.0 <2.0.0",
@@ -2391,6 +2555,11 @@
}
}
},
"grunt-extract-sourcemap": {
"version": "0.1.19",
"from": "grunt-extract-sourcemap@>=0.1.18 <0.2.0",
"resolved": "https://registry.npmjs.org/grunt-extract-sourcemap/-/grunt-extract-sourcemap-0.1.19.tgz"
},
"grunt-known-options": {
"version": "1.1.0",
"from": "grunt-known-options@>=1.1.0 <1.2.0",
@@ -2430,10 +2599,34 @@
}
}
},
"grunt-newer": {
"version": "1.2.0",
"from": "grunt-newer@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-newer/-/grunt-newer-1.2.0.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
"from": "async@>=1.5.2 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
}
}
},
"grunt-webpack": {
"version": "1.0.18",
"from": "grunt-webpack@>=1.0.11 <2.0.0",
"resolved": "https://registry.npmjs.org/grunt-webpack/-/grunt-webpack-1.0.18.tgz",
"dependencies": {
"lodash": {
"version": "4.17.1",
"from": "lodash@>=4.7.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
"handlebars": {
"version": "4.0.5",
"version": "4.0.6",
"from": "handlebars@>=4.0.0 <4.1.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.5.tgz",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.6.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
@@ -2579,9 +2772,9 @@
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz"
},
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.16.2 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
@@ -2620,6 +2813,18 @@
"from": "immutable@3.8.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz"
},
"imports-loader": {
"version": "0.6.5",
"from": "imports-loader@>=0.6.5 <0.7.0",
"resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.6.5.tgz",
"dependencies": {
"source-map": {
"version": "0.1.43",
"from": "source-map@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz"
}
}
},
"indent-string": {
"version": "2.1.0",
"from": "indent-string@>=2.1.0 <3.0.0",
@@ -2663,9 +2868,9 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz"
},
"invariant": {
"version": "2.2.1",
"version": "2.2.2",
"from": "invariant@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz"
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz"
},
"invert-kv": {
"version": "1.0.0",
@@ -2845,9 +3050,14 @@
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.0.tgz"
},
"istanbul-lib-instrument": {
"version": "1.2.0",
"version": "1.3.0",
"from": "istanbul-lib-instrument@>=1.1.4 <2.0.0",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.2.0.tgz"
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.3.0.tgz"
},
"jasmine-core": {
"version": "2.5.2",
"from": "jasmine-core@>=2.4.1 <3.0.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.5.2.tgz"
},
"javascript-detect-element-resize": {
"version": "0.5.3",
@@ -2885,9 +3095,9 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-2.0.0.tgz"
},
"js-yaml": {
"version": "3.6.1",
"version": "3.7.0",
"from": "js-yaml@>=3.2.7 <4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz"
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz"
},
"jsbn": {
"version": "0.1.0",
@@ -2904,6 +3114,35 @@
"from": "jsesc@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz"
},
"jshint": {
"version": "2.9.4",
"from": "jshint@>=2.9.4 <3.0.0",
"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.4.tgz",
"dependencies": {
"lodash": {
"version": "3.7.0",
"from": "lodash@>=3.7.0 <3.8.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz"
}
}
},
"jshint-loader": {
"version": "0.8.3",
"from": "jshint-loader@>=0.8.3 <0.9.0",
"resolved": "https://registry.npmjs.org/jshint-loader/-/jshint-loader-0.8.3.tgz",
"dependencies": {
"strip-json-comments": {
"version": "0.1.3",
"from": "strip-json-comments@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz"
}
}
},
"jshint-stylish": {
"version": "2.2.1",
"from": "jshint-stylish@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/jshint-stylish/-/jshint-stylish-2.2.1.tgz"
},
"json-schema": {
"version": "0.2.3",
"from": "json-schema@0.2.3",
@@ -2944,6 +3183,178 @@
"from": "jstimezonedetect@1.0.5",
"resolved": "https://registry.npmjs.org/jstimezonedetect/-/jstimezonedetect-1.0.5.tgz"
},
"karma": {
"version": "1.3.0",
"from": "karma@>=1.1.2 <2.0.0",
"resolved": "https://registry.npmjs.org/karma/-/karma-1.3.0.tgz",
"dependencies": {
"accepts": {
"version": "1.1.4",
"from": "accepts@1.1.4",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz"
},
"base64-arraybuffer": {
"version": "0.1.2",
"from": "base64-arraybuffer@0.1.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz"
},
"component-emitter": {
"version": "1.2.0",
"from": "component-emitter@1.2.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz"
},
"engine.io": {
"version": "1.6.10",
"from": "engine.io@1.6.10",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.10.tgz"
},
"engine.io-client": {
"version": "1.6.9",
"from": "engine.io-client@1.6.9",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz",
"dependencies": {
"component-emitter": {
"version": "1.1.2",
"from": "component-emitter@1.1.2",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
}
}
},
"engine.io-parser": {
"version": "1.2.4",
"from": "engine.io-parser@1.2.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz",
"dependencies": {
"has-binary": {
"version": "0.1.6",
"from": "has-binary@0.1.6",
"resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz"
}
}
},
"isarray": {
"version": "0.0.1",
"from": "isarray@0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
},
"mime": {
"version": "1.3.4",
"from": "mime@>=1.3.4 <2.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
},
"mime-db": {
"version": "1.12.0",
"from": "mime-db@>=1.12.0 <1.13.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz"
},
"mime-types": {
"version": "2.0.14",
"from": "mime-types@>=2.0.4 <2.1.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz"
},
"negotiator": {
"version": "0.4.9",
"from": "negotiator@0.4.9",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz"
},
"socket.io": {
"version": "1.4.7",
"from": "socket.io@1.4.7",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.7.tgz"
},
"socket.io-client": {
"version": "1.4.6",
"from": "socket.io-client@1.4.6",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz"
},
"source-map": {
"version": "0.5.6",
"from": "source-map@>=0.5.3 <0.6.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
},
"ws": {
"version": "1.0.1",
"from": "ws@1.0.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz"
}
}
},
"karma-chrome-launcher": {
"version": "1.0.1",
"from": "karma-chrome-launcher@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-1.0.1.tgz"
},
"karma-coverage": {
"version": "1.1.1",
"from": "karma-coverage@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.1.tgz",
"dependencies": {
"source-map": {
"version": "0.5.6",
"from": "source-map@>=0.5.1 <0.6.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
}
}
},
"karma-firefox-launcher": {
"version": "1.0.0",
"from": "karma-firefox-launcher@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.0.0.tgz"
},
"karma-html2js-preprocessor": {
"version": "1.1.0",
"from": "karma-html2js-preprocessor@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-html2js-preprocessor/-/karma-html2js-preprocessor-1.1.0.tgz"
},
"karma-jasmine": {
"version": "1.0.2",
"from": "karma-jasmine@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.0.2.tgz"
},
"karma-junit-reporter": {
"version": "1.1.0",
"from": "karma-junit-reporter@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.1.0.tgz"
},
"karma-phantomjs-launcher": {
"version": "1.0.2",
"from": "karma-phantomjs-launcher@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz",
"dependencies": {
"lodash": {
"version": "4.17.1",
"from": "lodash@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
},
"karma-sauce-launcher": {
"version": "1.1.0",
"from": "karma-sauce-launcher@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-sauce-launcher/-/karma-sauce-launcher-1.1.0.tgz"
},
"karma-sourcemap-loader": {
"version": "0.3.7",
"from": "karma-sourcemap-loader@>=0.3.7 <0.4.0",
"resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz"
},
"karma-webpack": {
"version": "1.8.0",
"from": "karma-webpack@>=1.8.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-1.8.0.tgz",
"dependencies": {
"async": {
"version": "0.9.2",
"from": "async@>=0.9.0 <0.10.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
},
"source-map": {
"version": "0.1.43",
"from": "source-map@>=0.1.41 <0.2.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz"
}
}
},
"kew": {
"version": "0.7.0",
"from": "kew@>=0.7.0 <0.8.0",
@@ -3011,6 +3422,11 @@
}
}
},
"less-plugin-autoprefix": {
"version": "1.5.1",
"from": "less-plugin-autoprefix@>=1.4.2 <2.0.0",
"resolved": "https://registry.npmjs.org/less-plugin-autoprefix/-/less-plugin-autoprefix-1.5.1.tgz"
},
"levn": {
"version": "0.3.0",
"from": "levn@>=0.3.0 <0.4.0",
@@ -3026,6 +3442,16 @@
"from": "livereload-js@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz"
},
"load-grunt-configs": {
"version": "1.0.0",
"from": "load-grunt-configs@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/load-grunt-configs/-/load-grunt-configs-1.0.0.tgz"
},
"load-grunt-tasks": {
"version": "3.5.2",
"from": "load-grunt-tasks@>=3.5.0 <4.0.0",
"resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-3.5.2.tgz"
},
"load-json-file": {
"version": "1.1.0",
"from": "load-json-file@>=1.0.0 <2.0.0",
@@ -3186,9 +3612,9 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
},
"moment": {
"version": "2.15.2",
"version": "2.16.0",
"from": "moment@>=2.10.2 <3.0.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.15.2.tgz"
"resolved": "https://registry.npmjs.org/moment/-/moment-2.16.0.tgz"
},
"ms": {
"version": "0.7.1",
@@ -3760,9 +4186,9 @@
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz"
},
"readable-stream": {
"version": "2.1.5",
"version": "2.2.2",
"from": "readable-stream@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz"
},
"readdirp": {
"version": "2.1.0",
@@ -3797,9 +4223,9 @@
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz"
},
"regenerate": {
"version": "1.3.1",
"version": "1.3.2",
"from": "regenerate@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.1.tgz"
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz"
},
"regenerator-runtime": {
"version": "0.9.6",
@@ -4174,9 +4600,9 @@
}
},
"statuses": {
"version": "1.3.0",
"version": "1.3.1",
"from": "statuses@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz"
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz"
},
"stream-browserify": {
"version": "1.0.0",
@@ -4302,6 +4728,11 @@
}
}
},
"time-grunt": {
"version": "1.4.0",
"from": "time-grunt@>=1.4.0 <2.0.0",
"resolved": "https://registry.npmjs.org/time-grunt/-/time-grunt-1.4.0.tgz"
},
"time-zone": {
"version": "0.1.0",
"from": "time-zone@>=0.1.0 <0.2.0",
@@ -4604,6 +5035,33 @@
}
}
},
"webpack": {
"version": "1.13.3",
"from": "webpack@>=1.13.1 <2.0.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-1.13.3.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
"from": "async@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
},
"minimist": {
"version": "0.0.8",
"from": "minimist@0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
},
"mkdirp": {
"version": "0.5.1",
"from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
},
"supports-color": {
"version": "3.1.2",
"from": "supports-color@>=3.1.0 <4.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz"
}
}
},
"webpack-core": {
"version": "0.6.8",
"from": "webpack-core@>=0.6.0 <0.7.0",
@@ -4666,9 +5124,9 @@
"resolved": "https://registry.npmjs.org/weinre/-/weinre-2.0.0-pre-I0Z7U9OV.tgz"
},
"which": {
"version": "1.2.11",
"version": "1.2.12",
"from": "which@>=1.2.0 <1.3.0",
"resolved": "https://registry.npmjs.org/which/-/which-1.2.11.tgz"
"resolved": "https://registry.npmjs.org/which/-/which-1.2.12.tgz"
},
"which-module": {
"version": "1.0.0",
@@ -4765,9 +5223,9 @@
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.1.0.tgz",
"dependencies": {
"lodash": {
"version": "4.16.6",
"version": "4.17.1",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz"
}
}
}

View File

@@ -84,6 +84,7 @@
"angular-codemirror": "chouseknecht/angular-codemirror#1.0.4",
"angular-cookies": "^1.4.3",
"angular-drag-and-drop-lists": "leigh-johnson/angular-drag-and-drop-lists#1.4.0",
"angular-duration-format": "^1.0.1",
"angular-gettext": "^2.3.5",
"angular-md5": "^0.1.8",
"angular-moment": "^0.10.1",

View File

@@ -0,0 +1,559 @@
'use strict';
describe('Controller: jobResultsController', () => {
// Setup
let jobResultsController;
let jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, eventResolve, populateResolve, $rScope, q;
jobData = {
related: {}
};
jobDataOptions = {
actions: {
get: {}
}
};
jobLabels = {};
jobFinished = true;
count = {
val: {},
countFinished: false
};
eventResolve = {
results: []
};
populateResolve = {};
let provideVals = () => {
angular.mock.module('jobResults', ($provide) => {
ParseTypeChange = jasmine.createSpy('ParseTypeChange');
ParseVariableString = jasmine.createSpy('ParseVariableString');
jobResultsService = jasmine.createSpyObj('jobResultsService', [
'deleteJob',
'cancelJob',
'relaunchJob',
'getEvents'
]);
eventQueue = jasmine.createSpyObj('eventQueue', [
'populate',
'markProcessed'
]);
$compile = jasmine.createSpy('$compile');
$provide.value('jobData', jobData);
$provide.value('jobDataOptions', jobDataOptions);
$provide.value('jobLabels', jobLabels);
$provide.value('jobFinished', jobFinished);
$provide.value('count', count);
$provide.value('ParseTypeChange', ParseTypeChange);
$provide.value('ParseVariableString', ParseVariableString);
$provide.value('jobResultsService', jobResultsService);
$provide.value('eventQueue', eventQueue);
$provide.value('$compile', $compile);
});
};
let injectVals = () => {
angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q, $httpBackend) => {
// when you call $scope.$apply() (which you need to do to
// to get inside of .then blocks to test), something is
// causing a request for all static files.
//
// this is a hack to just pass those requests through
//
// from googling this is probably due to angular-router
// weirdness
$httpBackend.when("GET", (url) => (url
.indexOf("/static/") !== -1))
.respond('');
$scope = $rootScope.$new();
$rScope = $rootScope;
q = $q;
jobData = _jobData_;
jobDataOptions = _jobDataOptions_;
jobLabels = _jobLabels_;
jobFinished = _jobFinished_;
count = _count_;
ParseTypeChange = _ParseTypeChange_;
ParseVariableString = _ParseVariableString_;
ParseVariableString.and.returnValue(jobData.extra_vars);
jobResultsService = _jobResultsService_;
eventQueue = _eventQueue_;
jobResultsService.getEvents.and
.returnValue($q.when(eventResolve));
eventQueue.populate.and
.returnValue($q.when(populateResolve));
$compile = _$compile_;
jobResultsController = $controller('jobResultsController', {
$scope: $scope,
jobData: jobData,
jobDataOptions: jobDataOptions,
jobLabels: jobLabels,
jobFinished: jobFinished,
count: count,
ParseTypeChange: ParseTypeChange,
jobResultsService: jobResultsService,
eventQueue: eventQueue,
$compile: $compile
});
});
};
beforeEach(angular.mock.module('Tower'));
let bootstrapTest = () => {
provideVals();
injectVals();
};
describe('bootstrap resolve values on scope', () => {
beforeEach(() => {
bootstrapTest();
});
it('should set values to scope based on resolve', () => {
expect($scope.job).toBe(jobData);
expect($scope.jobOptions).toBe(jobDataOptions.actions.GET);
expect($scope.labels).toBe(jobLabels);
});
});
describe('getTowerLinks()', () => {
beforeEach(() => {
jobData.related = {
"job_template": "api/v1/job_templates/12",
"created_by": "api/v1/users/12",
"inventory": "api/v1/inventories/12",
"project": "api/v1/projects/12",
"credential": "api/v1/credentials/12",
"cloud_credential": "api/v1/credentials/13",
"network_credential": "api/v1/credentials/14",
};
bootstrapTest();
});
it('should transform related links and set to scope var', () => {
expect($scope.job_template_link).toBe('/#/job_templates/12');
expect($scope.created_by_link).toBe('/#/users/12');
expect($scope.inventory_link).toBe('/#/inventories/12');
expect($scope.project_link).toBe('/#/projects/12');
expect($scope.machine_credential_link).toBe('/#/credentials/12');
expect($scope.cloud_credential_link).toBe('/#/credentials/13');
expect($scope.network_credential_link).toBe('/#/credentials/14');
});
});
describe('getTowerLabels()', () => {
beforeEach(() => {
jobDataOptions.actions.GET = {
status: {
choices: [
["new",
"New"]
]
},
job_type: {
choices: [
["job",
"Playbook Run"]
]
},
verbosity: {
choices: [
[0,
"0 (Normal)"]
]
}
};
jobData.status = "new";
jobData.job_type = "job";
jobData.verbosity = 0;
bootstrapTest();
});
it('should set scope variables based on options', () => {
expect($scope.status_label).toBe("New");
expect($scope.type_label).toBe("Playbook Run");
expect($scope.verbosity_label).toBe("0 (Normal)");
});
});
describe('extra vars stuff', () => {
let extraVars = "foo";
beforeEach(() => {
jobData.extra_vars = extraVars;
bootstrapTest();
});
it('should have extra vars on scope', () => {
expect($scope.job.extra_vars).toBe(extraVars);
});
it('should call ParseVariableString and set to scope', () => {
expect(ParseVariableString)
.toHaveBeenCalledWith(extraVars);
expect($scope.variables).toBe(extraVars);
});
it('should set the parse type to yaml', () => {
expect($scope.parseType).toBe('yaml');
});
it('should call ParseTypeChange with proper params', () => {
let params = {
scope: $scope,
field_id: 'pre-formatted-variables',
readOnly: true
};
expect(ParseTypeChange)
.toHaveBeenCalledWith(params);
});
});
describe('$scope.toggleStdoutFullscreen', () => {
beforeEach(() => {
bootstrapTest();
});
it('should toggle $scope.stdoutFullScreen', () => {
// essentially set to false
expect($scope.stdoutFullScreen).toBe(false);
// toggle once to true
$scope.toggleStdoutFullscreen();
expect($scope.stdoutFullScreen).toBe(true);
// toggle again to false
$scope.toggleStdoutFullscreen();
expect($scope.stdoutFullScreen).toBe(false);
});
});
describe('$scope.deleteJob', () => {
beforeEach(() => {
bootstrapTest();
});
it('should delete the job', () => {
let job = $scope.job;
$scope.deleteJob();
expect(jobResultsService.deleteJob).toHaveBeenCalledWith(job);
});
});
describe('$scope.cancelJob', () => {
beforeEach(() => {
bootstrapTest();
});
it('should cancel the job', () => {
let job = $scope.job;
$scope.cancelJob();
expect(jobResultsService.cancelJob).toHaveBeenCalledWith(job);
});
});
describe('$scope.relaunchJob', () => {
beforeEach(() => {
bootstrapTest();
});
it('should relaunch the job', () => {
let scope = $scope;
$scope.relaunchJob();
expect(jobResultsService.relaunchJob)
.toHaveBeenCalledWith(scope);
});
});
describe('count stuff', () => {
beforeEach(() => {
count = {
val: {
ok: 1,
skipped: 2,
unreachable: 3,
failures: 4,
changed: 5
},
countFinished: true
};
bootstrapTest();
});
it('should set count values to scope', () => {
expect($scope.count).toBe(count.val);
expect($scope.countFinished).toBe(true);
});
it('should find the hostCount based on the count', () => {
expect($scope.hostCount).toBe(15);
});
});
describe('follow stuff - incomplete', () => {
beforeEach(() => {
jobFinished = false;
bootstrapTest();
});
it('should set followEngaged based on jobFinished incomplete', () => {
expect($scope.followEngaged).toBe(true);
});
it('should set followTooltip based on jobFinished incomplete', () => {
expect($scope.followTooltip).toBe("Currently following standard out as it comes in. Click to unfollow.");
});
});
describe('follow stuff - finished', () => {
beforeEach(() => {
jobFinished = true;
bootstrapTest();
});
it('should set followEngaged based on jobFinished', () => {
expect($scope.followEngaged).toBe(false);
});
it('should set followTooltip based on jobFinished', () => {
expect($scope.followTooltip).toBe("Jump to last line of standard out.");
});
});
describe('event stuff', () => {
beforeEach(() => {
jobData.id = 1;
jobData.related.job_events = "url";
bootstrapTest();
});
it('should make a rest call to get already completed events', () => {
expect(jobResultsService.getEvents).toHaveBeenCalledWith("url");
});
it('should call processEvent when receiving message', () => {
let eventPayload = {"foo": "bar"};
$rScope.$broadcast('ws-job_events-1', eventPayload);
expect(eventQueue.populate).toHaveBeenCalledWith(eventPayload);
});
it('should set the job status on scope when receiving message', () => {
let eventPayload = {
unified_job_id: 1,
status: 'finished'
};
$rScope.$broadcast('ws-jobs', eventPayload);
expect($scope.job.status).toBe(eventPayload.status);
});
});
describe('getEvents and populate stuff', () => {
describe('getEvents', () => {
let event1 = {
event: 'foo'
};
let event2 = {
event_name: 'bar'
};
let event1Processed = {
event_name: 'foo'
};
beforeEach(() => {
eventResolve = {
results: [
event1,
event2
]
};
bootstrapTest();
$scope.$apply();
});
it('should change the event name to event_name', () => {
expect(eventQueue.populate)
.toHaveBeenCalledWith(event1Processed);
});
it('should pass through the event with event_name', () => {
expect(eventQueue.populate)
.toHaveBeenCalledWith(event2);
});
it('should have called populate twice', () => {
expect(eventQueue.populate.calls.count()).toEqual(2);
});
// TODO: can't figure out how to a test of events.next...
// if you set events.next to true it causes the tests to
// stop running
});
describe('populate - start time', () => {
beforeEach(() => {
jobData.start = "";
populateResolve = {
startTime: 'foo',
changes: ['startTime']
};
bootstrapTest();
$scope.$apply();
});
it('sets start time when passed as a change', () => {
expect($scope.job.start).toBe('foo');
});
});
describe('populate - start time already set', () => {
beforeEach(() => {
jobData.start = "bar";
populateResolve = {
startTime: 'foo',
changes: ['startTime']
};
bootstrapTest();
$scope.$apply();
});
it('does not set start time because already set', () => {
expect($scope.job.start).toBe('bar');
});
});
describe('populate - count already received', () => {
let receiveCount = {
ok: 2,
skipped: 2,
unreachable: 2,
failures: 2,
changed: 2
};
let alreadyCount = {
ok: 3,
skipped: 3,
unreachable: 3,
failures: 3,
changed: 3
};
beforeEach(() => {
count.countFinished = true;
count.val = alreadyCount;
populateResolve = {
count: receiveCount,
changes: ['count']
};
bootstrapTest();
$scope.$apply();
});
it('count does not change', () => {
expect($scope.count).toBe(alreadyCount);
expect($scope.hostCount).toBe(15);
});
});
describe('populate - playCount, taskCount and countFinished', () => {
beforeEach(() => {
populateResolve = {
playCount: 12,
taskCount: 13,
changes: ['playCount', 'taskCount', 'countFinished']
};
bootstrapTest();
$scope.$apply();
});
it('sets playCount', () => {
expect($scope.playCount).toBe(12);
});
it('sets taskCount', () => {
expect($scope.taskCount).toBe(13);
});
it('sets countFinished', () => {
expect($scope.countFinished).toBe(true);
});
});
describe('populate - finishedTime', () => {
beforeEach(() => {
jobData.finished = "";
populateResolve = {
finishedTime: "finished_time",
changes: ['finishedTime']
};
bootstrapTest();
$scope.$apply();
});
it('sets finished time and changes follow tooltip', () => {
expect($scope.job.finished).toBe('finished_time');
expect($scope.jobFinished).toBe(true);
expect($scope.followTooltip)
.toBe("Jump to last line of standard out.");
});
});
describe('populate - finishedTime when already finished', () => {
beforeEach(() => {
jobData.finished = "already_set";
populateResolve = {
finishedTime: "finished_time",
changes: ['finishedTime']
};
bootstrapTest();
$scope.$apply();
});
it('does not set finished time because already set', () => {
expect($scope.job.finished).toBe('already_set');
expect($scope.jobFinished).toBe(true);
expect($scope.followTooltip)
.toBe("Jump to last line of standard out.");
});
});
// TODO: stdout change tests
});
});

View File

@@ -0,0 +1,173 @@
'use strict';
describe('parseStdoutService', () => {
let parseStdoutService,
log;
beforeEach(angular.mock.module('Tower'));
beforeEach(angular.mock.module('jobResults',($provide) => {
log = jasmine.createSpyObj('$log', [
'error'
]);
$provide.value('$log', log);
}));
beforeEach(angular.mock.inject((_$log_, _parseStdoutService_) => {
parseStdoutService = _parseStdoutService_;
}));
describe('prettify()', () => {
it('returns lines of stdout with styling classes', () => {
let line = "[0;32mok: [host-00]",
styledLine = '<span class="ansi32">ok: [host-00]</span>';
expect(parseStdoutService.prettify(line).toBe(styledLine));
});
it('can return lines of stdout without styling classes', () => {
let line = "[0;32mok: [host-00]",
unstyled = "unstyled",
unstyledLine = 'ok: [host-00]';
expect(parseStdoutService.prettify(line, unstyled).toBe(unstyledLine));
});
});
describe('getLineClasses()', () => {
it('creates a string that is used as a class', () => {
let headerEvent = {
event_name: 'playbook_on_task_start',
event_data: {
task_uuid: '1da9012d-18e6-4562-85cd-83cf10a97f86'
}
};
let lineNum = 3;
let line = "TASK [setup] *******************************************************************";
let styledLine = "header_task header_task_80dd087c-268b-45e8-9aab-1083bcfd9364 play_0f667a23-d9ab-4128-a735-80566bcdbca0 line_num_3";
expect(parseStdoutService.getLineClasses(headerEvent, line, lineNum).toBe(styledLine));
});
});
describe('getCollapseIcon()', () => {
let emptySpan = `
<span class="JobResultsStdOut-lineExpander"></span>`;
it('returns empty expander for non-header event', () => {
let nonHeaderEvent = {
event_name: 'not_header',
start_line: 0,
end_line: 1,
stdout:"line1"
};
expect(parseStdoutService.getCollapseIcon(nonHeaderEvent))
.toBe(emptySpan);
});
it('returns collapse/decollapse icons for header events', () => {
let headerEvent = {
event_name: 'playbook_on_task_start',
start_line: 0,
end_line: 1,
stdout:"line1",
event_data: {
task_uuid: '1da9012d-18e6-4562-85cd-83cf10a97f86'
}
};
let line = "TASK [setup] *******************************************************************";
let expandSpan = `
<span class="JobResultsStdOut-lineExpander">
<i class="JobResultsStdOut-lineExpanderIcon fa fa-caret-down expanderizer
expanderizer--task expanded"
ng-click="toggleLine($event, '.task_1da9012d-18e6-4562-85cd-83cf10a97f86')"
data-uuid="task_1da9012d-18e6-4562-85cd-83cf10a97f86">
</i>
</span>`;
expect(parseStdoutService.getCollapseIcon(headerEvent, line))
.toBe(expandSpan);
});
});
describe('getLineArr()', () => {
it('returns stdout in array format', () => {
let mockEvent = {
start_line: 12,
end_line: 14,
stdout: "line1\r\nline2\r\n"
};
let expectedReturn = [[13, "line1"],[14, "line2"]];
let returnedEvent = parseStdoutService.getLineArr(mockEvent);
expect(returnedEvent).toEqual(expectedReturn);
});
});
describe('parseStdout()', () => {
let mockEvent = {"foo": "bar"};
it('calls functions', function() {
spyOn(parseStdoutService, 'getLineArr').and
.returnValue([[13, 'line1'], [14, 'line2']]);
spyOn(parseStdoutService, 'getLineClasses').and
.returnValue("");
spyOn(parseStdoutService, 'getCollapseIcon').and
.returnValue("");
spyOn(parseStdoutService, 'getAnchorTags').and
.returnValue("");
spyOn(parseStdoutService, 'prettify').and
.returnValue("prettified_line");
parseStdoutService.parseStdout(mockEvent);
expect(parseStdoutService.getLineArr)
.toHaveBeenCalledWith(mockEvent);
expect(parseStdoutService.getLineClasses)
.toHaveBeenCalledWith(mockEvent, 'line1', 13);
expect(parseStdoutService.getCollapseIcon)
.toHaveBeenCalledWith(mockEvent, 'line1');
expect(parseStdoutService.getAnchorTags)
.toHaveBeenCalledWith(mockEvent, "prettified_line");
expect(parseStdoutService.prettify)
.toHaveBeenCalledWith('line1');
// get line arr should be called once for the event
expect(parseStdoutService.getLineArr.calls.count())
.toBe(1);
// other functions should be called twice (once for each
// line)
expect(parseStdoutService.getLineClasses.calls.count())
.toBe(2);
expect(parseStdoutService.getCollapseIcon.calls.count())
.toBe(2);
expect(parseStdoutService.getAnchorTags.calls.count())
.toBe(2);
expect(parseStdoutService.prettify.calls.count())
.toBe(2);
});
it('returns dom-ified lines', function() {
spyOn(parseStdoutService, 'getLineArr').and
.returnValue([[13, 'line1']]);
spyOn(parseStdoutService, 'getLineClasses').and
.returnValue("line_classes");
spyOn(parseStdoutService, 'getCollapseIcon').and
.returnValue("collapse_icon_dom");
spyOn(parseStdoutService, 'getAnchorTags').and
.returnValue("anchor_tag_dom");
spyOn(parseStdoutService, 'prettify').and
.returnValue("prettified_line");
var returnedString = parseStdoutService.parseStdout(mockEvent);
var expectedString = `
<div class="JobResultsStdOut-aLineOfStdOutline_classes">
<div class="JobResultsStdOut-lineNumberColumn">collapse_icon_dom13</div>
<div class="JobResultsStdOut-stdoutColumn">anchor_tag_dom</div>
</div>`;
expect(returnedString).toBe(expectedString);
});
});
});

View File

@@ -18,6 +18,7 @@ var vendorPkgs = [
'angular-codemirror',
'angular-cookies',
'angular-drag-and-drop-lists',
'angular-duration-format',
'angular-gettext',
'angular-md5',
'angular-moment',