event processing for details panel and initial stats bar integration

This commit is contained in:
Jake McDermott 2018-03-19 12:01:17 -04:00
parent f65d170cab
commit b577f50930
No known key found for this signature in database
GPG Key ID: 3B02CAD476EECB35
12 changed files with 447 additions and 82 deletions

View File

@ -199,3 +199,85 @@
width: 100%;
flex-wrap: wrap;
}
// Status Bar -----------------------------------------------------------------------------
.HostStatusBar {
display: flex;
flex: 0 0 auto;
width: 100%;
margin-top: 10px;
}
.HostStatusBar-ok,
.HostStatusBar-changed,
.HostStatusBar-unreachable,
.HostStatusBar-failed,
.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-failed {
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;
border: 1px solid @default-bg;
}
.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--failed {
background-color: @default-err;
}

View File

@ -2,6 +2,7 @@ const templateUrl = require('~features/output/details.partial.html');
let $http;
let $filter;
let $scope;
let $state;
let error;
@ -12,68 +13,86 @@ let strings;
let wait;
function mapChoices (choices) {
return Object.assign(...choices.map(([k, v]) => ({[k]: v})));
if (!choices) return {};
return Object.assign(...choices.map(([k, v]) => ({ [k]: v })));
}
function getStatusDetails (status) {
const value = status || resource.model.get('status');
const label = 'Status';
const unmapped = status || resource.model.get('status');
if (!unmapped) {
return null;
}
const choices = mapChoices(resource.model.options('actions.GET.status.choices'));
const displayValue = choices[value];
const label = 'Status';
const icon = `fa icon-job-${unmapped}`;
const value = choices[unmapped];
return { displayValue, label, value };
return { label, icon, value };
}
function getStartTimeDetails (started) {
const value = started || resource.model.get('started');
const unfiltered = started || resource.model.get('started');
const label = 'Started';
let displayValue;
let value;
if (value) {
displayValue = $filter('longDate')(value);
if (unfiltered) {
value = $filter('longDate')(unfiltered);
} else {
displayValue = 'Not Started';
value = 'Not Started';
}
return { displayValue, label, value };
return { label, value };
}
function getFinishTimeDetails (finished) {
const value = finished || resource.model.get('finished');
const unfiltered = finished || resource.model.get('finished');
const label = 'Finished';
let displayValue;
let value;
if (value) {
displayValue = $filter('longDate')(value);
if (unfiltered) {
value = $filter('longDate')(unfiltered);
} else {
displayValue = 'Not Finished';
value = 'Not Finished';
}
return { displayValue, label, value };
return { label, value };
}
function getJobTypeDetails () {
const value = resource.model.get('job_type');
const label = 'Job Type';
const unmapped = resource.model.get('job_type');
if (!unmapped) {
return null;
}
const choices = mapChoices(resource.model.options('actions.GET.job_type.choices'));
const displayValue = choices[value];
const label = 'Job Type';
const value = choices[unmapped];
return { displayValue, label, value };
return { label, value };
}
function getVerbosityDetails () {
const value = resource.model.get('verbosity');
const verbosity = resource.model.get('verbosity');
if (!verbosity) {
return null;
}
const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices'));
const displayValue = choices[value];
const label = 'Verbosity';
const value = choices[value];
return { displayValue, label, value };
return { label, value };
}
function getSourceWorkflowJobDetails () {
@ -273,7 +292,6 @@ function getLimitDetails () {
}
function getInstanceGroupDetails () {
const instanceGroup = resource.model.get('summary_fields.instance_group');
if (!instanceGroup) {
@ -336,9 +354,9 @@ function getLabelDetails () {
}
const label = 'Labels';
const value = jobLabels.map(({ name }) => name).map($filter('sanitize'));
const more = false;
let more = false;
const value = jobLabels.map(({ name }) => name).map($filter('sanitize'));
return { label, more, value };
}
@ -396,9 +414,7 @@ function cancelJob () {
prompt({ hdr, resourceName, body, actionText, action });
}
function deleteJob () {
return;
}
function deleteJob () {}
function AtDetailsController (
_$http_,
@ -418,21 +434,18 @@ function AtDetailsController (
$state = _$state_;
error = _error_;
// resource = _resource_;
parse = ParseVariableString;
prompt = _prompt_;
strings = _strings_;
wait = _wait_;
// statusChoices = mapChoices(resource.options('status.choices'));
vm.init = _$scope_ => {
$scope = _$scope_;
resource = $scope.resource;
vm.init = scope => {
vm.job = scope.job || {};
resource = scope.resource;
vm.status = getStatusDetails(scope.status);
vm.startTime = getStartTimeDetails();
vm.finishTime = getFinishTimeDetails();
vm.status = getStatusDetails();
vm.started = getStartTimeDetails();
vm.finished = getFinishTimeDetails();
vm.jobType = getJobTypeDetails();
vm.jobTemplate = getJobTemplateDetails();
vm.sourceWorkflowJob = getSourceWorkflowJobDetails();
@ -457,12 +470,24 @@ function AtDetailsController (
vm.deleteJob = deleteJob;
vm.toggleLabels = toggleLabels;
// codemirror
const cm = { parseType: 'yaml', variables: vm.extraVars.value, $apply: scope.$apply };
ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true });
const observe = (key, transform) => {
$scope.$watch(key, value => { this[key] = transform(value); });
};
scope.$watch('status', value => { vm.status = getStatusDetails(value); });
}
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 });
}
};
}
AtDetailsController.$inject = [
@ -492,9 +517,10 @@ function atDetails () {
link: atDetailsLink,
controller: AtDetailsController,
scope: {
job: '=',
status: '=',
resource: '=',
status: '=',
started: '=',
finished: '=',
},
};
}

View File

@ -36,25 +36,25 @@
<div class="JobResults-resultRow">
<label class="JobResults-resultRowLabel">{{ vm.status.label}}</label>
<div class="JobResults-resultRowText">
<i class="JobResults-statusResultIcon fa icon-job-{{ vm.status.value }}"></i>
{{ vm.status.displayValue | translate }}
<i class="JobResults-statusResultIcon {{ vm.status.icon }}"></i>
{{ vm.status.value }}
</div>
</div>
<!-- START TIME DETAIL -->
<div class="JobResults-resultRow" ng-if="vm.startTime">
<label class="JobResults-resultRowLabel">{{ vm.startTime.label }}</label>
<div class="JobResults-resultRow" ng-if="vm.started">
<label class="JobResults-resultRowLabel">{{ vm.started.label }}</label>
<div class="JobResults-resultRowText">
{{ vm.startTime.displayValue }}
{{ vm.started.value }}
</div>
</div>
<!-- FINISHED TIME DETAIL -->
<div class="JobResults-resultRow" ng-show="vm.startTime">
<label class="JobResults-resultRowLabel">{{ vm.finishTime.label }}</label>
<div class="JobResults-resultRow" ng-show="vm.started">
<label class="JobResults-resultRowLabel">{{ vm.finished.label }}</label>
<div class="JobResults-resultRowText">
{{ vm.finishTime.displayValue }}
{{ vm.finished.value }}
</div>
</div>
@ -81,7 +81,7 @@
<!-- JOB TYPE DETAIL -->
<div class="JobResults-resultRow" ng-if="vm.jobType">
<label class="JobResults-resultRowLabel">{{ vm.jobType.label }}</label>
<div class="JobResults-resultRowText">{{ vm.jobType.displayValue }}</div>
<div class="JobResults-resultRowText">{{ vm.jobType.value }}</div>
</div>
<!-- LAUNCHED BY DETAIL -->
@ -164,7 +164,7 @@
<!-- VERBOSITY DETAIL -->
<div class="JobResults-resultRow" ng-if="vm.verbosity">
<label class="JobResults-resultRowLabel">{{ vm.verbosity.label }}</label>
<div class="JobResults-resultRowText">{{ vm.verbosity.displayValue }}</div>
<div class="JobResults-resultRowText">{{ vm.verbosity.value }}</div>
</div>
<!-- IG DETAIL -->

View File

@ -10,6 +10,8 @@ let resource;
let $state;
let qs;
let hack;
function JobsIndexController (
_resource_,
_page_,
@ -51,7 +53,7 @@ function JobsIndexController (
vm.expand = expand;
vm.isExpanded = true;
// search
// Search
$state = _$state_;
qs = _qs_;
@ -67,17 +69,47 @@ function JobsIndexController (
vm.removeSearchTag = removeSearchTag;
vm.searchTags = getSearchTags(getCurrentQueryset());
// details
// Host Status Bar
vm.status = {
running: Boolean(resource.model.get('started')) && !resource.model.get('finished'),
stats: resource.stats,
}
// Details
vm.details = {
job: resource.model.model.GET,
status: resource.model.model.GET.status,
resource,
started: resource.model.get('started'),
finished: resource.model.get('finished'),
status: resource.model.get('status'),
};
render.requestAnimationFrame(() => init());
}
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,
});
@ -98,10 +130,12 @@ function init (pageMode) {
page,
scroll,
resource,
onStreamStart,
onStreamFinish,
render: events => shift().then(() => append(events, true)),
listen: (namespace, listener) => {
$scope.$on(namespace, (scope, data) => listener(data));
}
},
});
if (pageMode) {

View File

@ -6,13 +6,16 @@ import Controller from '~features/output/index.controller';
import PageService from '~features/output/page.service';
import RenderService from '~features/output/render.service';
import ScrollService from '~features/output/scroll.service';
import SearchKeyDirective from '~features/output/search-key.directive';
import StreamService from '~features/output/stream.service';
import DetailsDirective from '~features/output/details.directive.js';
import DetailsDirective from '~features/output/details.directive';
import SearchKeyDirective from '~features/output/search-key.directive';
import StatusDirective from '~features/output/status.directive';
const Template = require('~features/output/index.view.html');
const MODULE_NAME = 'at.features.output';
const PAGE_CACHE = true;
const PAGE_LIMIT = 5;
const PAGE_SIZE = 50;
@ -66,13 +69,21 @@ function resolveResource (
Wait('start');
return new Resource(['get', 'options'], [id, id])
.then(model => Promise.all([
model.extend('labels'),
model.extend(related, config)
]))
.then(([ model ]) => ({
.then(model => {
const promises = [model.getStats()];
if (model.has('related.labels')) {
promises.push(model.extend('labels'));
}
promises.push(model.extend(related, config));
return Promise.all(promises);
})
.then(([stats, model]) => ({
id,
type,
stats,
model,
related,
ws: {
@ -200,6 +211,7 @@ angular
.service('JobStreamService', StreamService)
.directive('atDetails', DetailsDirective)
.directive('atSearchKey', SearchKeyDirective)
.directive('atStatus', StatusDirective)
.run(JobsRun);
export default MODULE_NAME;

View File

@ -1,14 +1,20 @@
<div class="container-fluid">
<div class="col-md-4">
<at-panel>
<at-details job="vm.details.job" status="vm.details.status" resource="vm.details.resource"></at-details>
<p><button class="btn" ng-click="vm.clear(true)">Page Mode</button></p>
<at-details
resource="vm.details.resource"
status="vm.details.status"
started="vm.details.started"
finished="vm.details.finished">
</at-details>
<!-- --><p><button class="btn" ng-click="vm.clear(true)">Page Mode</button></p>
</at-panel>
</div>
<div class="col-md-8">
<at-panel class="at-Stdout">
<!-- search ============================================================================================================= -->
<at-status running="vm.status.running" stats="vm.status.stats"></at-status>
<!-- search ===================================================================================== -->
<form ng-submit="vm.search()">
<div class="input-group">
<input type="text"
@ -52,7 +58,7 @@
<at-search-key ng-show="vm.searchKey" fields="vm.searchKeyFields" examples="vm.searchKeyExamples"></at-search-key>
<!-- ==================================================================================================================== -->
<!-- ============================================================================================ -->
<div class="at-Stdout-menuTop">
<div class="pull-left" ng-click="vm.expand()">
<i class="at-Stdout-menuIcon fa"

View File

@ -12,6 +12,11 @@ function JobsStrings (BaseString) {
CANCEL_BODY: t.s('Are you sure you want to cancel this job?'),
CANCEL_HEADER: t.s('Cancel Job'),
};
ns.status = {
RUNNING: t.s('The host status bar will update when the job is complete.'),
UNAVAILABLE: t.s('Host status information for this job unavailable.'),
};
}
JobsStrings.$inject = ['BaseStringService'];

View File

@ -0,0 +1,92 @@
const templateUrl = require('~features/output/status.partial.html');
const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped'];
function getHostStatusCounts (statsEvent) {
const countedHostNames = [];
const counts = Object.assign(...HOST_STATUS_KEYS.map(key => ({ [key]: 0 })));
HOST_STATUS_KEYS.forEach(key => {
const hostData = _.get(statsEvent, ['event_data', key], {});
Object.keys(hostData).forEach(hostName => {
const isAlreadyCounted = (countedHostNames.indexOf(hostName) > -1);
const shouldBeCounted = ((!isAlreadyCounted) && hostData[hostName] > 0);
if (shouldBeCounted) {
countedHostNames.push(hostName);
counts[key]++;
}
});
});
return counts;
}
function createStatusBarTooltip (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;
atStatusController.init(scope);
}
function AtStatusController (strings) {
const vm = this || {};
vm.tooltips = {
running: strings.get('status.RUNNING'),
unavailable: strings.get('status.UNAVAILABLE'),
};
vm.init = scope => {
const { running, stats } = scope;
vm.running = running || false;
vm.setStats(stats);
scope.$watch('running', value => { vm.running = value; });
scope.$watch('stats', vm.setStats);
};
vm.setStats = stats => {
const counts = getHostStatusCounts(stats);
HOST_STATUS_KEYS.forEach(key => {
const count = counts[key];
const statusBarElement = $(`.HostStatusBar-${key}`);
statusBarElement.css('flex', `${count} 0 auto`);
vm.tooltips[key] = createStatusBarTooltip(key, count);
});
vm.statsAreAvailable = Boolean(stats);
};
}
function atStatus () {
return {
templateUrl,
restrict: 'E',
require: ['atStatus'],
controllerAs: 'vm',
link: atStatusLink,
controller: [
'JobStrings',
AtStatusController
],
scope: {
running: '=',
stats: '=',
},
};
}
export default atStatus;

View File

@ -0,0 +1,37 @@
<div class="HostStatusBar">
<div class="HostStatusBar-ok"
data-placement="top"
aw-tool-tip="{{ vm.tooltips.ok }}"
data-tip-watch="vm.tooltips.ok">
</div>
<div class="HostStatusBar-skipped"
data-placement="top"
aw-tool-tip="{{ vm.tooltips.skipped }}"
data-tip-watch="vm.tooltips.skipped">
</div>
<div class="HostStatusBar-changed"
data-placement="top"
aw-tool-tip="{{ vm.tooltips.changed }}"
data-tip-watch="vm.tooltips.changed">
</div>
<div class="HostStatusBar-failures"
data-placement="top"
aw-tool-tip="{{ vm.tooltips.failures }}"
data-tip-watch="vm.tooltips.failures">
</div>
<div class="HostStatusBar-unreachable"
data-placement="top"
aw-tool-tip="{{ vm.tooltips.unreachable }}"
data-tip-watch="vm.tooltips.unreachable">
</div>
<div class="HostStatusBar-noData"
ng-show="vm.running"
data-placement="top"
aw-tool-tip="{{:: vm.tooltips.running }}">
</div>
<div class="HostStatusBar-noData"
ng-show="!vm.running && !vm.statsAreAvailable"
data-placement="top"
aw-tool-tip="{{:: vm.tooltips.unavailable }}">
</div>
</div>

View File

@ -3,7 +3,7 @@ const JOB_END = 'playbook_on_stats';
const MAX_LAG = 120;
function JobStreamService ($q) {
this.init = ({ resource, scroll, page, render, listen }) => {
this.init = ({ resource, scroll, page, onStreamStart, onStreamFinish, render, listen }) => {
this.resource = resource;
this.scroll = scroll;
this.page = page;
@ -23,8 +23,10 @@ function JobStreamService ($q) {
};
this.hooks = {
onStreamStart,
onStreamFinish,
render,
listen
listen,
};
this.lines = {
@ -35,7 +37,7 @@ function JobStreamService ($q) {
max: 0
};
this.hooks.listen(resource.ws.namespace, this.listen);
this.hooks.listen(resource.ws.namespace, this.listener);
};
this.getBatchFactors = size => {
@ -105,19 +107,25 @@ function JobStreamService ($q) {
}
};
this.listen = data => {
this.listener = 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);

View File

@ -23,26 +23,54 @@ function postRelaunch (params) {
return $http(req);
}
function getStats () {
if (!this.has('GET', 'id')) {
return Promise.reject(new Error('No property, id, exists'));
}
if (!this.has('GET', 'related.job_events')) {
return Promise.reject(new Error('No related property, job_events, exists'));
}
const req = {
method: 'GET',
url: `${this.path}${this.get('id')}/job_events/`,
params: { event: 'playbook_on_stats' },
};
return $http(req)
.then(({ data }) => {
if (data.results.length > 0) {
return data.results[0];
}
return null;
});
}
function JobModel (method, resource, config) {
BaseModel.call(this, 'jobs');
this.Constructor = JobModel;
this.postRelaunch = postRelaunch.bind(this);
this.getRelaunch = getRelaunch.bind(this);
this.getStats = getStats.bind(this);
return this.create(method, resource, config);
}
function JobModelLoader (_BaseModel_, _$http_) {
BaseModel = _BaseModel_;
function JobModelLoader (_$http_, _BaseModel_) {
$http = _$http_;
BaseModel = _BaseModel_;
return JobModel;
}
JobModelLoader.$inject = [
'$http',
'BaseModel',
'$http'
];
export default JobModelLoader;

View File

@ -1,19 +1,54 @@
let $http;
let BaseModel;
function getStats () {
if (!this.has('GET', 'id')) {
return Promise.reject(new Error('No property, id, exists'));
}
if (!this.has('GET', 'related.events')) {
return Promise.reject(new Error('No related property, events, exists'));
}
const req = {
method: 'GET',
url: `${this.path}${this.get('id')}/events/`,
params: { event: 'playbook_on_stats' },
};
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.Constructor = ProjectUpdateModel;
return this.create(method, resource, config);
}
function ProjectUpdateModelLoader (_BaseModel_) {
function ProjectUpdateModelLoader (_$http_, _BaseModel_) {
$http = _$http_;
BaseModel = _BaseModel_;
return ProjectUpdateModel;
}
ProjectUpdateModelLoader.$inject = ['BaseModel'];
ProjectUpdateModelLoader.$inject = [
'$http',
'BaseModel'
];
export default ProjectUpdateModelLoader;