mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 23:17:32 -02:30
Merge pull request #4046 from ansible/jobDetailsRework
Job details rework
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
// // };
|
||||
|
||||
@@ -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);
|
||||
// }]);
|
||||
|
||||
251
awx/ui/client/src/job-results/event-queue.service.js
Normal file
251
awx/ui/client/src/job-results/event-queue.service.js
Normal 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;
|
||||
}];
|
||||
@@ -0,0 +1,2 @@
|
||||
<textarea id="HostEvent-codemirror" class="HostEvent-codemirror">
|
||||
</textarea>
|
||||
@@ -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>
|
||||
152
awx/ui/client/src/job-results/host-event/host-event.block.less
Normal file
152
awx/ui/client/src/job-results/host-event/host-event.block.less
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}];
|
||||
58
awx/ui/client/src/job-results/host-event/host-event.route.js
Normal file
58
awx/ui/client/src/job-results/host-event/host-event.route.js
Normal 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 };
|
||||
20
awx/ui/client/src/job-results/host-event/main.js
Normal file
20
awx/ui/client/src/job-results/host-event/main.js
Normal 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);
|
||||
}]);
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
@@ -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>
|
||||
11
awx/ui/client/src/job-results/host-status-bar/main.js
Normal file
11
awx/ui/client/src/job-results/host-status-bar/main.js
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
@@ -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>
|
||||
11
awx/ui/client/src/job-results/job-results-stdout/main.js
Normal file
11
awx/ui/client/src/job-results/job-results-stdout/main.js
Normal 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);
|
||||
125
awx/ui/client/src/job-results/job-results.block.less
Normal file
125
awx/ui/client/src/job-results/job-results.block.less
Normal 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;
|
||||
}
|
||||
}
|
||||
202
awx/ui/client/src/job-results/job-results.controller.js
Normal file
202
awx/ui/client/src/job-results/job-results.controller.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}];
|
||||
522
awx/ui/client/src/job-results/job-results.partial.html
Normal file
522
awx/ui/client/src/job-results/job-results.partial.html
Normal 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>
|
||||
158
awx/ui/client/src/job-results/job-results.route.js
Normal file
158
awx/ui/client/src/job-results/job-results.route.js
Normal 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'
|
||||
};
|
||||
197
awx/ui/client/src/job-results/job-results.service.js
Normal file
197
awx/ui/client/src/job-results/job-results.service.js
Normal 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;
|
||||
}];
|
||||
27
awx/ui/client/src/job-results/main.js
Normal file
27
awx/ui/client/src/job-results/main.js
Normal 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);
|
||||
208
awx/ui/client/src/job-results/parse-stdout.service.js
Normal file
208
awx/ui/client/src/job-results/parse-stdout.service.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
// 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;
|
||||
}];
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
586
awx/ui/npm-shrinkwrap.json
generated
586
awx/ui/npm-shrinkwrap.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
559
awx/ui/tests/spec/job-results/job-results.controller-test.js
Normal file
559
awx/ui/tests/spec/job-results/job-results.controller-test.js
Normal 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
|
||||
});
|
||||
});
|
||||
173
awx/ui/tests/spec/job-results/parse-stdout.service-test.js
Normal file
173
awx/ui/tests/spec/job-results/parse-stdout.service-test.js
Normal 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][0m",
|
||||
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][0m",
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ var vendorPkgs = [
|
||||
'angular-codemirror',
|
||||
'angular-cookies',
|
||||
'angular-drag-and-drop-lists',
|
||||
'angular-duration-format',
|
||||
'angular-gettext',
|
||||
'angular-md5',
|
||||
'angular-moment',
|
||||
|
||||
Reference in New Issue
Block a user