Adds the host event modal to the standard out feature

Removes old host modal code
This commit is contained in:
Jared Tabor
2018-04-02 11:21:11 -07:00
committed by Jake McDermott
parent 18dc0e9066
commit fe58b74d1e
18 changed files with 352 additions and 240 deletions

View File

@@ -1,3 +1,4 @@
@import 'host-event/_index';
.at-Stdout {
&-menuTop {
color: @at-gray-848992;

View File

@@ -1,4 +1,5 @@
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
<div ui-view></div>
<div class="JobResults-panelHeader">
<div class="JobResults-panelHeaderText" translate> DETAILS</div>
<!-- LEFT PANE HEADER ACTIONS -->

View File

@@ -0,0 +1,190 @@
.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;
max-height: none!important;
}
.HostEvent-close:hover{
color: @btn-txt;
background-color: @btn-bg-hov;
}
.HostEvent-body{
margin-bottom: 20px;
}
.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{
padding: 0px!important;
overflow-y: auto;
}
.HostEvent-nav{
padding-top: 12px;
padding-bottom: 20px;
}
.HostEvent-field{
margin-bottom: 8px;
flex: 0 1 12em;
}
.HostEvent-field--label{
text-transform: uppercase;
flex: 0 1 80px;
max-width: 80px;
min-width: 80px;
font-size: 12px;
word-wrap: break-word;
}
.HostEvent-field{
.OnePlusTwo-left--detailsRow;
}
.HostEvent-field--content{
word-wrap: break-word;
}
.HostEvent-field--monospaceContent{
font-family: monospace;
}
.HostEvent-button:disabled {
pointer-events: all!important;
}
.HostEvent-stdout{
height:200px;
width:100%
}
.HostEvent-stdoutContainer {
height:200px;
overflow-y: scroll;
overflow-x: hidden;
border-radius: 5px;
border: 1px solid #ccc;
font-style: normal;
background-color: @default-no-items-bord;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
.HostEvent-numberColumnPreload {
background-color: @default-list-header-bg;
height: 198px;
border-right: 1px solid #ccc;
width: 30px;
position: fixed;
}
.HostEvent-numberColumn {
background-color: @default-list-header-bg;
border-right: 1px solid #ccc;
border-bottom-left-radius: 5px;
color: #999;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
position: fixed;
padding: 4px 3px 0 5px;
text-align: right;
white-space: nowrap;
width: 30px;
}
.HostEvent-numberColumn--second{
padding-top:0px;
}
.HostEvent-stdoutColumn{
white-space: pre;
overflow-y: scroll;
margin-left: 46px;
padding-top: 4px;
}
.HostEvent-noJson{
align-items: center;
background-color: @default-no-items-bord;
border: 1px solid @default-icon-hov;
border-radius: 5px;
color: @b7grey;
display: flex;
height: 200px;
justify-content: center;
text-transform: uppercase;
width: 100%;
}

View File

@@ -0,0 +1,3 @@
<textarea ng-hide="no_json" id="HostEvent-codemirror" class="HostEvent-codemirror">
</textarea>
<div ng-if="no_json" class="HostEvent-noJson" translate>No JSON data returned by the module</div>

View File

@@ -0,0 +1,72 @@
<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 ng-click="closeHostEvent()" type="button" class="close">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div class="HostEvent-details">
<div class="HostEvent-field">
<span class="HostEvent-field--label">CREATED</span>
<span class="HostEvent-field--content">{{(event.created | longDate) || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">ID</span>
<span class="HostEvent-field--content">{{event.id || "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 HostEvent-field--monospaceContent">{{module_name}}</span>
</div>
</div>
<div class="HostEvent-nav">
<!-- view navigation buttons -->
<button ui-sref="jobz.host-event.json" type="button"
class="btn btn-sm btn-default HostEvent-tab"
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.host-event.json')}">
JSON
</button>
<button ng-if="stdout" ui-sref="jobz.host-event.stdout"
type="button" class="btn btn-sm btn-default HostEvent-tab"
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.host-event.stdout')}">
Standard Out
</button>
<button ng-if="stderr" ui-sref="jobz.host-event.stderr"
type="button" class="btn btn-sm btn-default HostEvent-tab"
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.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 ng-click="closeHostEvent()" class="btn btn-sm btn-default HostEvent-close">Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="HostEvent-stdout">
<div class="HostEvent-stdoutContainer">
<div class="HostEvent-numberColumnPreload"></div>
<div class="HostEvent-numberColumn">1</div>
<div class="HostEvent-stdoutColumn" ng-bind-html="stderr"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div id="HostEvent-stdout" class="HostEvent-stdout">
<div class="HostEvent-stdoutContainer">
<div class="HostEvent-numberColumnPreload"></div>
<div class="HostEvent-numberColumn">1</div>
<div class="HostEvent-stdoutColumn" ng-bind-html="stdout"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,170 @@
function HostEventsController (
$scope,
$state,
HostEventService,
hostEvent
) {
$scope.processEventStatus = HostEventService.processEventStatus;
$scope.processResults = processResults;
$scope.isActiveState = isActiveState;
$scope.getActiveHostIndex = getActiveHostIndex;
$scope.closeHostEvent = closeHostEvent;
function init () {
hostEvent.event_name = hostEvent.event;
$scope.event = _.cloneDeep(hostEvent);
// grab standard out & standard error if present from the host
// event's 'res' object, for things like Ansible modules. Small
// wrinkle in this implementation is that the stdout/stderr tabs
// should be shown if the `res` object has stdout/stderr keys, even
// if they're a blank string. The presence of these keys is
// potentially significant to a user.
if (_.has(hostEvent.event_data, 'task_action')) {
$scope.module_name = hostEvent.event_data.task_action;
} else if (!_.has(hostEvent.event_data, 'task_action')) {
$scope.module_name = 'No result found';
}
if (_.has(hostEvent.event_data, 'res.result.stdout')) {
if (hostEvent.event_data.res.stdout === '') {
$scope.stdout = ' ';
} else {
$scope.stdout = hostEvent.event_data.res.stdout;
}
}
if (_.has(hostEvent.event_data, 'res.result.stderr')) {
if (hostEvent.event_data.res.stderr === '') {
$scope.stderr = ' ';
} else {
$scope.stderr = hostEvent.event_data.res.stderr;
}
}
if (_.has(hostEvent.event_data, 'res')) {
$scope.json = hostEvent.event_data.res;
}
if ($scope.module_name === 'debug' &&
_.has(hostEvent.event_data, 'res.result.stdout')) {
$scope.stdout = hostEvent.event_data.res.result.stdout;
}
if ($scope.module_name === 'yum' &&
_.has(hostEvent.event_data, 'res.results') &&
_.isArray(hostEvent.event_data.res.results)) {
const event = hostEvent.event_data.res.results;
$scope.stdout = event[0];// eslint-disable-line prefer-destructuring
}
// instantiate Codemirror
if ($state.current.name === 'jobz.host-event.json') {
try {
if (_.has(hostEvent.event_data, 'res')) {
initCodeMirror(
'HostEvent-codemirror',
JSON.stringify($scope.json, null, 4),
{ name: 'javascript', json: true }
);
resize();
} else {
$scope.no_json = true;
}
} catch (err) {
// element with id HostEvent-codemirror is not the view
// controlled by this instance of HostEventController
}
} else if ($state.current.name === 'jobz.host-event.stdout') {
try {
resize();
} catch (err) {
// element with id HostEvent-codemirror is not the view
// controlled by this instance of HostEventController
}
} else if ($state.current.name === 'jobz.host-event.stderr') {
try {
resize();
} catch (err) {
// element with id HostEvent-codemirror is not the view
// controlled by this instance of HostEventController
}
}
$('#HostEvent').modal('show');
$('.modal-content').resizable({
minHeight: 523,
minWidth: 600
});
$('.modal-dialog').draggable({
cancel: '.HostEvent-view--container'
});
function resize () {
if ($state.current.name === 'jobz.host-event.json') {
const editor = $('.CodeMirror')[0].CodeMirror;
const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
editor.setSize('100%', height);
} else if ($state.current.name === 'jobz.host-event.stdout' || $state.current.name === 'jobz.host-event.stderr') {
const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
$('.HostEvent-stdout').width('100%');
$('.HostEvent-stdout').height(height);
$('.HostEvent-stdoutContainer').height(height);
$('.HostEvent-numberColumnPreload').height(height);
}
}
$('.modal-dialog').on('resize', resize);
$('#HostEvent').on('hidden.bs.modal', $scope.closeHostEvent);
}
function processResults (value) {
if (typeof value === 'object') {
return false;
}
return true;
}
function initCodeMirror (el, data, mode) {
const container = document.getElementById(el);
const options = {};
options.lineNumbers = true;
options.mode = mode;
options.readOnly = true;
options.scrollbarStyle = null;
const editor = CodeMirror.fromTextArea(// eslint-disable-line no-undef
container,
options
);
editor.setSize('100%', 200);
editor.getDoc().setValue(data);
}
function isActiveState (name) {
return $state.current.name === name;
}
function getActiveHostIndex () {
function hostResultfilter (obj) {
return obj.id === $scope.event.id;
}
const result = $scope.hostResults.filter(hostResultfilter);
return $scope.hostResults.indexOf(result[0]);
}
function closeHostEvent () {
// Unbind the listener so it doesn't fire when we close the modal via navigation
$('#HostEvent').off('hidden.bs.modal');
$('#HostEvent').modal('hide');
$state.go('jobz');
}
$scope.init = init;
$scope.init();
}
HostEventsController.$inject = [
'$scope',
'$state',
'HostEventService',
'hostEvent',
];
module.exports = HostEventsController;

View File

@@ -0,0 +1,72 @@
const HostEventModalTemplate = require('~features/output/host-event/host-event-modal.partial.html');
const HostEventCodeMirrorTemplate = require('~features/output/host-event/host-event-codemirror.partial.html');
const HostEventStdoutTemplate = require('~features/output/host-event/host-event-stdout.partial.html');
const HostEventStderrTemplate = require('~features/output/host-event/host-event-stderr.partial.html');
function exit () {
// 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');
}
function HostEventResolve (HostEventService, $stateParams) {
return HostEventService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId
}).then((response) => response.data.results[0]);
}
HostEventResolve.$inject = [
'HostEventService',
'$stateParams',
];
const hostEventModal = {
name: 'jobz.host-event',
url: '/host-event/:eventId',
controller: 'HostEventsController',
templateUrl: HostEventModalTemplate,
abstract: false,
ncyBreadcrumb: {
skip: true
},
resolve: {
hostEvent: HostEventResolve
},
onExit: exit
};
const hostEventJson = {
name: 'jobz.host-event.json',
url: '/json',
controller: 'HostEventsController',
templateUrl: HostEventCodeMirrorTemplate,
ncyBreadcrumb: {
skip: true
},
};
const hostEventStdout = {
name: 'jobz.host-event.stdout',
url: '/stdout',
controller: 'HostEventsController',
templateUrl: HostEventStdoutTemplate,
ncyBreadcrumb: {
skip: true
},
};
const hostEventStderr = {
name: 'jobz.host-event.stderr',
url: '/stderr',
controller: 'HostEventsController',
templateUrl: HostEventStderrTemplate,
ncyBreadcrumb: {
skip: true
},
};
export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr };

View File

@@ -0,0 +1,69 @@
function HostEventService (
Rest,
ProcessErrors,
GetBasePath,
$rootScope
) {
// GET events related to a job run
// e.g.
// ?event=playbook_on_stats
// ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter
this.getRelatedJobEvents = (id, params) => {
let url = GetBasePath('jobs');
url = `${url}${id}/job_events/?${this.stringifyParams(params)}`;
Rest.setUrl(url);
return Rest.get()
.then(response => response)
.catch(({ data, status }) => {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: `Call to ${url}. GET returned: ${status}` });
});
};
this.stringifyParams = params => {
function reduceFunction (result, value, key) {
return `${result}${key}=${value}&`;
}
return _.reduce(params, reduceFunction, '');
};
// Generate a helper class for job_event statuses
// the stack for which status to display is
// unreachable > failed > changed > ok
// uses the API's runner events and convenience properties .failed .changed to determine status.
// see: job_event_callback.py for more filters to support
this.processEventStatus = event => {
const obj = {};
if (event.event === 'runner_on_unreachable') {
obj.class = 'HostEvent-status--unreachable';
obj.status = 'unreachable';
}
// equiv to 'runner_on_error' && 'runner on failed'
if (event.failed) {
obj.class = 'HostEvent-status--failed';
obj.status = 'failed';
}
// catch the changed case before ok, because both can be true
if (event.changed) {
obj.class = 'HostEvent-status--changed';
obj.status = 'changed';
}
if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok') {
obj.class = 'HostEvent-status--ok';
obj.status = 'ok';
}
if (event.event === 'runner_on_skipped') {
obj.class = 'HostEvent-status--skipped';
obj.status = 'skipped';
}
return obj;
};
}
HostEventService.$inject = [
'Rest',
'ProcessErrors',
'GetBasePath',
'$rootScope'
];
export default HostEventService;

View File

@@ -0,0 +1,26 @@
import {
hostEventModal,
hostEventJson,
hostEventStdout,
hostEventStderr
} from './host-event.route';
import controller from './host-event.controller';
import service from './host-event.service';
const MODULE_NAME = 'hostEvents';
function hostEventRun ($stateExtender) {
$stateExtender.addState(hostEventModal);
$stateExtender.addState(hostEventJson);
$stateExtender.addState(hostEventStdout);
$stateExtender.addState(hostEventStderr);
}
hostEventRun.$inject = [
'$stateExtender'
];
angular.module(MODULE_NAME, [])
.controller('HostEventsController', controller)
.service('HostEventService', service)
.run(hostEventRun);
export default MODULE_NAME;

View File

@@ -12,6 +12,7 @@ import StatusService from '~features/output/status.service';
import DetailsDirective from '~features/output/details.directive';
import SearchDirective from '~features/output/search.directive';
import StatsDirective from '~features/output/stats.directive';
import HostEvent from './host-event/index';
const Template = require('~features/output/index.view.html');
@@ -211,7 +212,8 @@ JobsRun.$inject = ['$stateRegistry'];
angular
.module(MODULE_NAME, [
atLibModels,
atLibComponents
atLibComponents,
HostEvent
])
.service('JobStrings', Strings)
.service('JobPageService', PageService)

View File

@@ -169,7 +169,7 @@ function JobRenderService ($q, $sce, $window) {
}
if (current.isHost) {
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}')">${content}</td>`;
tdEvent = `<td class="at-Stdout-event--host" ui-sref="jobz.host-event.json({eventId: ${current.id}, taskUuid: '${current.uuid}' })">${content}</td>`;
}
if (current.time && current.line === ln) {
@@ -251,6 +251,7 @@ function JobRenderService ($q, $sce, $window) {
});
this.compile = html => {
html = $(this.el);
this.hooks.compile(html);
return this.requestAnimationFrame();