add event processing for stats and host status components

This commit is contained in:
Jake McDermott 2018-03-23 11:38:20 -04:00
parent faa33e0bec
commit 450eaeca96
No known key found for this signature in database
GPG Key ID: 3B02CAD476EECB35
19 changed files with 227 additions and 118 deletions

View File

@ -206,12 +206,11 @@
display: flex;
flex: 0 0 auto;
width: 100%;
margin-top: 10px;
}
.HostStatusBar-ok,
.HostStatusBar-changed,
.HostStatusBar-unreachable,
.HostStatusBar-dark,
.HostStatusBar-failed,
.HostStatusBar-skipped,
.HostStatusBar-noData {
@ -231,7 +230,7 @@
flex: 0 0 auto;
}
.HostStatusBar-unreachable {
.HostStatusBar-dark {
background-color: @default-unreachable;
flex: 0 0 auto;
}
@ -265,7 +264,7 @@
background-color: @default-succ;
}
.HostStatusBar-tooltipBadge--unreachable {
.HostStatusBar-tooltipBadge--dark {
background-color: @default-unreachable;
}

View File

@ -441,7 +441,7 @@ function AtDetailsController (
vm.init = _$scope_ => {
$scope = _$scope_;
resource = $scope.resource;
resource = $scope.resource; // eslint-disable-line prefer-destructuring
vm.status = getStatusDetails();
vm.started = getStartTimeDetails();
@ -466,27 +466,35 @@ function AtDetailsController (
vm.extraVars = getExtraVarsDetails();
vm.labels = getLabelDetails();
// Relaunch Component
vm.job = _.get(resource.model, 'model.GET', {});
// XX - Codemirror
if (vm.extraVars) {
const cm = {
parseType: 'yaml',
$apply: $scope.$apply,
variables: vm.extraVars.value,
};
ParseTypeChange({
field_id: 'cm-extra-vars',
readOnly: true,
scope: cm,
});
}
vm.cancelJob = cancelJob;
vm.deleteJob = deleteJob;
vm.toggleLabels = toggleLabels;
const observe = (key, transform) => {
$scope.$watch(key, value => { this[key] = transform(value); });
$scope.$watch(key, value => { vm[key] = transform(value); });
};
observe('finished', getFinishTimeDetails);
observe('status', getStatusDetails);
observe('started', getStartTimeDetails);
observe('finished', getFinishTimeDetails);
// relaunch component
$scope.job = _.get(resource.model, 'model.GET', {});
this.job = $scope.job;
// codemirror
if (this.extraVars) {
const cm = { parseType: 'yaml', variables: this.extraVars.value, $apply: $scope.$apply };
ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true });
}
};
}

View File

@ -1,11 +1,10 @@
<!-- LEFT PANE HEADER -->
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
<div class="JobResults-panelHeader">
<div class="JobResults-panelHeaderText" translate> DETAILS</div>
<!-- LEFT PANE HEADER ACTIONS -->
<div class="JobResults-panelHeaderButtonActions">
<!-- RELAUNCH ACTION -->
<at-relaunch state="vm.job"></at-relaunch>
<at-relaunch job="vm.job"></at-relaunch>
<!-- CANCEL ACTION -->
<button class="List-actionButton List-actionButton--delete"

View File

@ -1,16 +1,24 @@
let vm;
const JOB_START = 'playbook_on_start';
const JOB_END = 'playbook_on_stats';
const PLAY_START = 'playbook_on_play_start';
const TASK_START = 'playbook_on_task_start';
let $compile;
let $scope;
let $q;
let $scope;
let $state;
let moment;
let page;
let qs;
let render;
let resource;
let scroll;
let stream;
let resource;
let $state;
let qs;
let hack;
let vm;
let eventCounter;
let statsEvent;
function JobsIndexController (
_resource_,
@ -23,6 +31,7 @@ function JobsIndexController (
_$q_,
_$state_,
_qs_,
_moment_,
) {
vm = this || {};
@ -36,6 +45,8 @@ function JobsIndexController (
render = _render_;
stream = _stream_;
moment = _moment_;
// Development helper(s)
vm.clear = devClear;
@ -69,11 +80,19 @@ function JobsIndexController (
vm.removeSearchTag = removeSearchTag;
vm.searchTags = getSearchTags(getCurrentQueryset());
// Host Status Bar
// Events
eventCounter = null;
statsEvent = resource.stats;
// Status Bar
vm.status = {
stats: statsEvent,
elapsed: resource.model.get('elapsed'),
running: Boolean(resource.model.get('started')) && !resource.model.get('finished'),
stats: resource.stats,
}
title: resource.model.get('name'),
plays: null,
tasks: null,
};
// Details
vm.details = {
@ -83,33 +102,10 @@ function JobsIndexController (
status: resource.model.get('status'),
};
render.requestAnimationFrame(() => init());
render.requestAnimationFrame(() => init(!vm.status.running));
}
function onStreamStart (data) {
const status = _.get(data, 'summary_fields.job.status');
if (!hack) {
hack = true;
vm.details.status = status;
vm.details.started = data.created;
vm.status.running = true;
}
}
function onStreamFinish (data) {
const failed = _.get(data, 'summary_fields.job.failed');
vm.details.status = failed ? 'failed' : 'successful';
vm.details.finished = data.created;
vm.status = { stats: data, running: false };
};
function init (pageMode) {
hack = false;
page.init({
resource,
});
@ -123,26 +119,68 @@ function init (pageMode) {
scroll.init({
isAtRest: scrollIsAtRest,
previous,
next
next,
});
stream.init({
page,
scroll,
resource,
onStreamStart,
onStreamFinish,
render: events => shift().then(() => append(events, true)),
listen: (namespace, listener) => {
$scope.$on(namespace, (scope, data) => listener(data));
onEventFrame (events) {
return shift().then(() => append(events, true));
},
onStart () {
vm.status.plays = 0;
vm.status.tasks = 0;
vm.status.running = true;
},
onStop () {
vm.status.stats = statsEvent;
vm.status.running = false;
vm.details.status = statsEvent.failed ? 'failed' : 'successful';
vm.details.finished = statsEvent.created;
}
});
$scope.$on(resource.ws.namespace, handleSocketEvent);
if (pageMode) {
next();
}
}
function handleSocketEvent (scope, data) {
const isLatest = ((!eventCounter) || (data.counter > eventCounter));
if (isLatest) {
eventCounter = data.counter;
vm.details.status = _.get(data, 'summary_fields.job.status');
vm.status.elapsed = moment(data.created)
.diff(resource.model.get('created'), 'seconds');
}
if (data.event === JOB_START) {
vm.details.started = data.created;
}
if (data.event === PLAY_START) {
vm.status.plays++;
}
if (data.event === TASK_START) {
vm.status.tasks++;
}
if (data.event === JOB_END) {
statsEvent = data;
}
stream.pushEventData(data);
}
function devClear (pageMode) {
init(pageMode);
render.clear();
@ -158,9 +196,11 @@ function next () {
return shift()
.then(() => append(events))
.then(() => {
if(scroll.isMissing()) {
if (scroll.isMissing()) {
return next();
}
return $q.resolve();
});
});
}
@ -242,9 +282,11 @@ function scrollHome () {
scroll.resume();
})
.then(() => {
if(scroll.isMissing()) {
if (scroll.isMissing()) {
return next();
}
return $q.resolve();
});
});
}
@ -359,7 +401,7 @@ function toggleSearchKey () {
}
function getCurrentQueryset () {
const { job_event_search } = $state.params;
const { job_event_search } = $state.params; // eslint-disable-line camelcase
return qs.decodeArr(job_event_search);
}
@ -414,6 +456,7 @@ JobsIndexController.$inject = [
'$q',
'$state',
'QuerySet',
'moment',
];
module.exports = JobsIndexController;

View File

@ -10,7 +10,7 @@ import StreamService from '~features/output/stream.service';
import DetailsDirective from '~features/output/details.directive';
import SearchKeyDirective from '~features/output/search-key.directive';
import StatusDirective from '~features/output/status.directive';
import StatsDirective from '~features/output/stats.directive';
const Template = require('~features/output/index.view.html');
@ -31,7 +31,7 @@ function resolveResource (
qs,
Wait
) {
const { id, type, job_event_search } = $stateParams;
const { id, type, job_event_search } = $stateParams; // eslint-disable-line camelcase
let Resource;
let related = 'events';
@ -61,7 +61,7 @@ function resolveResource (
const params = { page_size: PAGE_SIZE, order_by: 'start_line' };
const config = { pageCache: PAGE_CACHE, pageLimit: PAGE_LIMIT, params };
if (job_event_search) {
if (job_event_search) { // eslint-disable-line camelcase
const queryParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
Object.assign(config.params, queryParams);
@ -211,7 +211,7 @@ angular
.service('JobStreamService', StreamService)
.directive('atDetails', DetailsDirective)
.directive('atSearchKey', SearchKeyDirective)
.directive('atStatus', StatusDirective)
.directive('atStats', StatsDirective)
.run(JobsRun);
export default MODULE_NAME;

View File

@ -13,7 +13,14 @@
<div class="col-md-8">
<at-panel class="at-Stdout">
<at-status running="vm.status.running" stats="vm.status.stats"></at-status>
<at-stats
elapsed="vm.status.elapsed"
running="vm.status.running"
stats="vm.status.stats"
title="vm.status.title"
plays="vm.status.plays"
tasks="vm.status.tasks">
</at-stats>
<!-- search ===================================================================================== -->
<form ng-submit="vm.search()">
<div class="input-group">

View File

@ -250,7 +250,6 @@ function JobRenderService ($q, $sce, $window) {
this.clear = () => {
const elements = this.el.children();
return this.remove(elements);
};

View File

@ -1,4 +1,4 @@
const templateUrl = require('~features/output/status.partial.html');
const templateUrl = require('~features/output/stats.partial.html');
const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped'];
@ -21,23 +21,25 @@ function getHostStatusCounts (statsEvent) {
});
});
counts.hosts = countedHostNames.length;
return counts;
}
function createStatusBarTooltip (key, count) {
function createStatsBarTooltip (key, count) {
const label = `<span class='HostStatusBar-tooltipLabel'>${key}</span>`;
const badge = `<span class='badge HostStatusBar-tooltipBadge HostStatusBar-tooltipBadge--${key}'>${count}</span>`;
return `${label}${badge}`;
}
function atStatusLink (scope, el, attrs, controllers) {
const [atStatusController] = controllers;
function atStatsLink (scope, el, attrs, controllers) {
const [atStatsController] = controllers;
atStatusController.init(scope);
atStatsController.init(scope);
}
function AtStatusController (strings) {
function AtStatsController (strings) {
const vm = this || {};
vm.tooltips = {
@ -46,12 +48,22 @@ function AtStatusController (strings) {
};
vm.init = scope => {
const { running, stats } = scope;
const { elapsed, running, stats, title, plays, tasks } = scope;
vm.title = title;
vm.plays = plays;
vm.tasks = tasks;
vm.elapsed = elapsed;
vm.running = running || false;
vm.setStats(stats);
scope.$watch('elapsed', value => { vm.elapsed = value; });
scope.$watch('running', value => { vm.running = value; });
scope.$watch('plays', value => { vm.plays = value; });
scope.$watch('tasks', value => { vm.tasks = value; });
scope.$watch('stats', vm.setStats);
};
@ -64,29 +76,34 @@ function AtStatusController (strings) {
statusBarElement.css('flex', `${count} 0 auto`);
vm.tooltips[key] = createStatusBarTooltip(key, count);
vm.tooltips[key] = createStatsBarTooltip(key, count);
});
vm.hosts = counts.hosts;
vm.statsAreAvailable = Boolean(stats);
};
}
function atStatus () {
function atStats () {
return {
templateUrl,
restrict: 'E',
require: ['atStatus'],
require: ['atStats'],
controllerAs: 'vm',
link: atStatusLink,
link: atStatsLink,
controller: [
'JobStrings',
AtStatusController
AtStatsController
],
scope: {
elapsed: '=',
running: '=',
stats: '=',
title: '=',
plays: '=',
tasks: '=',
},
};
}
export default atStatus;
export default atStats;

View File

@ -1,9 +1,8 @@
const JOB_START = 'playbook_on_start';
const JOB_END = 'playbook_on_stats';
const MAX_LAG = 120;
function JobStreamService ($q) {
this.init = ({ resource, scroll, page, onStreamStart, onStreamFinish, render, listen }) => {
this.init = ({ resource, scroll, page, onEventFrame, onStart, onStop }) => {
this.resource = resource;
this.scroll = scroll;
this.page = page;
@ -13,20 +12,21 @@ function JobStreamService ($q) {
this.pageCount = 0;
this.chain = $q.resolve();
this.factors = this.getBatchFactors(this.resource.page.size);
this.state = {
started: false,
paused: false,
pausing: false,
resuming: false,
ending: false,
ended: false
ended: false,
counting: false,
};
this.hooks = {
onStreamStart,
onStreamFinish,
render,
listen,
onEventFrame,
onStart,
onStop,
};
this.lines = {
@ -36,8 +36,6 @@ function JobStreamService ($q) {
min: 0,
max: 0
};
this.hooks.listen(resource.ws.namespace, this.listener);
};
this.getBatchFactors = size => {
@ -91,7 +89,7 @@ function JobStreamService ($q) {
this.lines.used.push(i);
}
let missing = [];
const missing = [];
for (let i = this.lines.min; i < this.lines.max; i++) {
if (this.lines.used.indexOf(i) === -1) {
missing.push(i);
@ -107,25 +105,19 @@ function JobStreamService ($q) {
}
};
this.listener = data => {
this.pushEventData = data => {
this.lag++;
this.chain = this.chain
.then(() => {
// console.log(data);
if (!this.isActive()) {
this.start();
if (!this.isEnding()) {
this.hooks.onStreamStart(data);
}
} else if (data.event === JOB_END) {
if (this.isPaused()) {
this.end(true);
} else {
this.end();
}
this.hooks.onStreamFinish(data);
}
this.checkLines(data);
@ -142,9 +134,11 @@ function JobStreamService ($q) {
return this.renderFrame(events);
})
.then(() => --this.lag);
return this.chain;
};
this.renderFrame = events => this.hooks.render(events)
this.renderFrame = events => this.hooks.onEventFrame(events)
.then(() => {
if (this.scroll.isLocked()) {
this.scroll.scrollToBottom();
@ -190,9 +184,13 @@ function JobStreamService ($q) {
};
this.start = () => {
this.state.started = true;
this.scroll.pause();
this.scroll.lock();
if (!this.state.ending && !this.state.ended) {
this.state.started = true;
this.scroll.pause();
this.scroll.lock();
this.hooks.onStart();
}
};
this.end = done => {
@ -202,13 +200,15 @@ function JobStreamService ($q) {
this.scroll.unlock();
this.scroll.resume();
this.hooks.onStop();
return;
}
this.state.ending = true;
};
this.isReadyToRender = () => this.isEnding() ||
this.isReadyToRender = () => this.isDone() ||
(!this.isPaused() && this.hasAllLines() && this.isBatchFull());
this.hasAllLines = () => this.lines.ready;
this.isBatchFull = () => this.count % this.framesPerRender === 0;

View File

@ -44,3 +44,15 @@
text-align: center;
margin-left: 5px;
}
.at-Panel-label {
text-transform: uppercase;
color: @default-interface-txt;
font-size: 12px;
font-weight: normal!important;
width: 30%;
@media screen and (max-width: @breakpoint-md) {
flex: 2.5 0 auto;
}
}

View File

@ -114,7 +114,8 @@ function atRelaunchCtrl (
jobObj.postRelaunch(launchParams)
.then((launchRes) => {
if (!$state.includes('jobs')) {
$state.go('jobResult', { id: launchRes.data.id }, { reload: true });
const relaunchType = launchRes.data.type === 'job' ? 'playbook' : launchRes.data.type;
$state.go('jobz', { id: launchRes.data.id, type: relaunchType }, { reload: true });
}
});
}

View File

@ -354,7 +354,6 @@ function has (method, keys) {
}
function extend (related, config = {}) {
const req = this.parseRequestConfig('GET', config);
if (_.get(config, 'params.page_size')) {
@ -614,7 +613,11 @@ function create (method, resource, config) {
}
function setEndpoint (resource) {
this.endpoint = `${this.path}${resource}/`;
if (Array.isArray(resource)) {
this.endpoint = `${this.path}${resource[0]}/`;
} else {
this.endpoint = `${this.path}${resource}/`;
}
}
function parseRequestConfig (method, resource, config) {

View File

@ -48,7 +48,6 @@ function getStats () {
});
}
function JobModel (method, resource, config) {
BaseModel.call(this, 'jobs');

View File

@ -10,8 +10,6 @@ function getStats () {
return Promise.reject(new Error('No related property, events, exists'));
}
const req = {
method: 'GET',
url: `${this.path}${this.get('id')}/events/`,
@ -20,19 +18,18 @@ function getStats () {
return $http(req)
.then(({ data }) => {
console.log(data);
if (data.results.length > 0) {
return data.results[0];
}
return null;
})
});
}
function ProjectUpdateModel (method, resource, config) {
BaseModel.call(this, 'project_updates');
this.getStats = getStats;
this.getStats = getStats.bind(this);
this.Constructor = ProjectUpdateModel;
@ -40,7 +37,7 @@ function ProjectUpdateModel (method, resource, config) {
}
function ProjectUpdateModelLoader (_$http_, _BaseModel_) {
$http = _$http_;
$http = _$http_;
BaseModel = _BaseModel_;
return ProjectUpdateModel;

View File

@ -50,6 +50,17 @@
font-size: @at-font-size-body;
}
.at-ButtonIcon-noborder {
padding: 4px @at-padding-button-horizontal;
font-size: @at-font-size-body;
.at-mixin-Button();
.at-mixin-ButtonHollow(
'at-color-default',
'at-color-default',
'at-color-button-text-default'
);
}
.at-Button--expand {
width: 100%;
}

View File

@ -20,3 +20,11 @@
.at-u-clear {
clear: both;
}
.at-u-noBorder {
border: none;
}
.at-u-floatRight {
float: right
}

View File

@ -2,7 +2,6 @@
display: flex;
flex: 0 0 auto;
width: 100%;
margin-top: 10px;
}
.HostStatusBar-ok,
@ -32,6 +31,11 @@
flex: 0 0 auto;
}
.HostStatusBar-dark {
background-color: @default-unreachable;
flex: 0 0 auto;
}
.HostStatusBar-failures {
background-color: @default-err;
flex: 0 0 auto;
@ -65,6 +69,10 @@
background-color: @default-unreachable;
}
.HostStatusBar-tooltipBadge--dark {
background-color: @default-unreachable;
}
.HostStatusBar-tooltipBadge--skipped {
background-color: @default-link;
}

View File

@ -126,9 +126,8 @@ standard-out-log {
.StandardOut-panelHeaderActions {
justify-content: flex-end;
display: flex;
margin-left: 10px;
font-size: 20px;
font-size: 12px;
}
.StandardOut-actions {
@ -137,12 +136,11 @@ standard-out-log {
.StandardOut-actionButton {
font-size: 16px;
height: 30px;
height: 20px;
min-width: 30px;
color: @list-action-icon;
background-color: inherit;
border: none;
border-radius: 50%;
}
.StandardOut-actionButton:hover {