mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
Refactor scroll handling into independent service
This commit is contained in:
committed by
Jake McDermott
parent
b16d9a89e3
commit
0c09447f2d
@@ -1,83 +1,41 @@
|
||||
import Ansi from 'ansi-to-html';
|
||||
import hasAnsi from 'has-ansi';
|
||||
|
||||
let vm;
|
||||
let ansi;
|
||||
let model;
|
||||
let resource;
|
||||
let page;
|
||||
let container;
|
||||
let $timeout;
|
||||
let $sce;
|
||||
let $compile;
|
||||
let $scope;
|
||||
let $q;
|
||||
|
||||
const record = {};
|
||||
|
||||
let parent = null;
|
||||
|
||||
const SCROLL_THRESHOLD = 0.1;
|
||||
const SCROLL_DELAY = 1000;
|
||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
const JOB_START = 'playbook_on_start';
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
|
||||
const EVENT_GROUPS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY
|
||||
];
|
||||
|
||||
const TIME_EVENTS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY,
|
||||
EVENT_STATS_PLAY
|
||||
];
|
||||
let vm;
|
||||
let $compile;
|
||||
let $scope;
|
||||
let $q;
|
||||
let page;
|
||||
let render;
|
||||
let scroll;
|
||||
let resource;
|
||||
|
||||
function JobsIndexController (
|
||||
_resource_,
|
||||
_page_,
|
||||
_$sce_,
|
||||
_$timeout_,
|
||||
_scroll_,
|
||||
_render_,
|
||||
_$scope_,
|
||||
_$compile_,
|
||||
_$q_
|
||||
) {
|
||||
vm = this || {};
|
||||
|
||||
$timeout = _$timeout_;
|
||||
$sce = _$sce_;
|
||||
$compile = _$compile_;
|
||||
$scope = _$scope_;
|
||||
$q = _$q_;
|
||||
resource = _resource_;
|
||||
|
||||
page = _page_;
|
||||
model = resource.model;
|
||||
|
||||
ansi = new Ansi();
|
||||
|
||||
const events = model.get(`related.${resource.related}.results`);
|
||||
const parsed = parseEvents(events);
|
||||
const html = $sce.trustAsHtml(parsed.html);
|
||||
|
||||
page.init(resource);
|
||||
|
||||
page.add({ number: 1, lines: parsed.lines });
|
||||
scroll = _scroll_;
|
||||
render = _render_;
|
||||
|
||||
// Development helper(s)
|
||||
vm.clear = devClear;
|
||||
|
||||
// Stdout Navigation
|
||||
vm.scroll = {
|
||||
isLocked: false,
|
||||
showBackToTop: false,
|
||||
isActive: false,
|
||||
position: 0,
|
||||
time: 0,
|
||||
home: scrollHome,
|
||||
end: scrollEnd,
|
||||
down: scrollPageDown,
|
||||
@@ -90,87 +48,93 @@ function JobsIndexController (
|
||||
vm.isExpanded = true;
|
||||
|
||||
// Real-time (active between JOB_START and JOB_END events only)
|
||||
$scope.$on(resource.ws.namespace, processWebSocketEvents);
|
||||
vm.stream = {
|
||||
isActive: false,
|
||||
isRendering: false,
|
||||
isPaused: false,
|
||||
buffered: 0,
|
||||
count: 0,
|
||||
page: 1
|
||||
active: false,
|
||||
rendering: false,
|
||||
paused: false
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const table = $(ELEMENT_TBODY);
|
||||
container = $(ELEMENT_CONTAINER);
|
||||
const stream = false; // TODO: Set in route
|
||||
|
||||
table.html($sce.getTrustedHtml(html));
|
||||
$compile(table.contents())($scope);
|
||||
|
||||
container.scroll(onScroll);
|
||||
});
|
||||
render.requestAnimationFrame(() => init());
|
||||
}
|
||||
|
||||
function processWebSocketEvents (scope, data) {
|
||||
let done;
|
||||
function init (stream) {
|
||||
page.init(resource);
|
||||
|
||||
render.init({
|
||||
get: () => resource.model.get(`related.${resource.related}.results`),
|
||||
compile: html => $compile(html)($scope)
|
||||
});
|
||||
|
||||
scroll.init({
|
||||
isAtRest: scrollIsAtRest,
|
||||
previous,
|
||||
next
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
$scope.$on(resource.ws.namespace, process);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function process (scope, data) {
|
||||
if (data.event === JOB_START) {
|
||||
vm.scroll.isActive = true;
|
||||
vm.stream.isActive = true;
|
||||
vm.scroll.isLocked = true;
|
||||
vm.stream.active = true;
|
||||
scroll.lock();
|
||||
} else if (data.event === JOB_END) {
|
||||
vm.stream.isActive = false;
|
||||
vm.stream.active = false;
|
||||
}
|
||||
|
||||
const pageAdded = page.addToBuffer(data);
|
||||
|
||||
if (pageAdded && !vm.scroll.isLocked) {
|
||||
vm.stream.isPaused = true;
|
||||
if (pageAdded && !scroll.isLocked()) {
|
||||
vm.stream.paused = true;
|
||||
}
|
||||
|
||||
if (vm.stream.isPaused && vm.scroll.isLocked) {
|
||||
vm.stream.isPaused = false;
|
||||
if (vm.stream.paused && scroll.isLocked()) {
|
||||
vm.stream.paused = false;
|
||||
}
|
||||
|
||||
if (vm.stream.isRendering || vm.stream.isPaused) {
|
||||
if (vm.stream.rendering || vm.stream.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
const events = page.emptyBuffer();
|
||||
|
||||
return render(events);
|
||||
return renderStream(events);
|
||||
}
|
||||
|
||||
function render (events) {
|
||||
vm.stream.isRendering = true;
|
||||
function renderStream (events) {
|
||||
vm.stream.rendering = true;
|
||||
|
||||
return shift()
|
||||
.then(() => append(events))
|
||||
.then(() => {
|
||||
if (vm.scroll.isLocked) {
|
||||
const height = container[0].scrollHeight;
|
||||
container[0].scrollTop = height;
|
||||
if (scroll.isLocked()) {
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
}
|
||||
|
||||
if (!vm.stream.isActive) {
|
||||
if (!vm.stream.active) {
|
||||
const buffer = page.emptyBuffer();
|
||||
|
||||
if (buffer.length) {
|
||||
return render(buffer);
|
||||
return renderStream(buffer);
|
||||
} else {
|
||||
vm.stream.rendering = false;
|
||||
scroll.unlock();
|
||||
}
|
||||
|
||||
vm.stream.isRendering = false;
|
||||
vm.scroll.isLocked = false;
|
||||
vm.scroll.isActive = false;
|
||||
} else {
|
||||
vm.stream.isRendering = false;
|
||||
vm.stream.rendering = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function devClear () {
|
||||
page.init(resource);
|
||||
clear();
|
||||
init(true);
|
||||
render.clear();
|
||||
}
|
||||
|
||||
function next () {
|
||||
@@ -182,13 +146,12 @@ function next () {
|
||||
|
||||
return shift()
|
||||
.then(() => append(events));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
const container = $(ELEMENT_CONTAINER)[0];
|
||||
|
||||
let previousHeight;
|
||||
let initialPosition = scroll.getScrollPosition();
|
||||
let postPopHeight;
|
||||
|
||||
return page.previous()
|
||||
.then(events => {
|
||||
@@ -198,296 +161,56 @@ function previous () {
|
||||
|
||||
return pop()
|
||||
.then(() => {
|
||||
previousHeight = container.scrollHeight;
|
||||
postPopHeight = scroll.getScrollHeight();
|
||||
|
||||
return prepend(events);
|
||||
})
|
||||
.then(() => {
|
||||
const currentHeight = container.scrollHeight;
|
||||
container.scrollTop = currentHeight - previousHeight;
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
|
||||
scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function append (events) {
|
||||
return $q(resolve => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const parsed = parseEvents(events);
|
||||
const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html)));
|
||||
const table = $(ELEMENT_TBODY);
|
||||
|
||||
page.updateLineCount('current', parsed.lines);
|
||||
|
||||
table.append(rows);
|
||||
$compile(rows.contents())($scope);
|
||||
|
||||
return resolve();
|
||||
return render.append(events)
|
||||
.then(count => {
|
||||
page.updateLineCount('current', count);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prepend (events) {
|
||||
return $q(resolve => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const parsed = parseEvents(events);
|
||||
const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html)));
|
||||
const table = $(ELEMENT_TBODY);
|
||||
|
||||
page.updateLineCount('current', parsed.lines);
|
||||
|
||||
table.prepend(rows);
|
||||
$compile(rows.contents())($scope);
|
||||
|
||||
$scope.$apply(() => {
|
||||
return resolve(parsed.lines);
|
||||
});
|
||||
return render.prepend(events)
|
||||
.then(count => {
|
||||
page.updateLineCount('current', count);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pop () {
|
||||
return $q(resolve => {
|
||||
if (!page.isOverCapacity()) {
|
||||
return resolve();
|
||||
}
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const lines = page.trim('right');
|
||||
const rows = $(ELEMENT_TBODY).children().slice(-lines);
|
||||
const lines = page.trim('right');
|
||||
|
||||
rows.empty();
|
||||
rows.remove();
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
return render.pop(lines);
|
||||
}
|
||||
|
||||
function shift () {
|
||||
return $q(resolve => {
|
||||
if (!page.isOverCapacity()) {
|
||||
return resolve();
|
||||
}
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const lines = page.trim('left');
|
||||
const rows = $(ELEMENT_TBODY).children().slice(0, lines);
|
||||
const lines = page.trim('left');
|
||||
|
||||
rows.empty();
|
||||
rows.remove();
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clear () {
|
||||
return $q(resolve => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const rows = $(ELEMENT_TBODY).children();
|
||||
|
||||
rows.empty();
|
||||
rows.remove();
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
return render.shift(lines);
|
||||
}
|
||||
|
||||
function expand () {
|
||||
vm.toggle(parent, true);
|
||||
}
|
||||
|
||||
function parseEvents (events) {
|
||||
let lines = 0;
|
||||
let html = '';
|
||||
|
||||
events.sort(orderByLineNumber);
|
||||
|
||||
events.forEach(event => {
|
||||
const line = parseLine(event);
|
||||
|
||||
html += line.html;
|
||||
lines += line.count;
|
||||
});
|
||||
|
||||
return {
|
||||
html,
|
||||
lines
|
||||
};
|
||||
}
|
||||
|
||||
function orderByLineNumber (a, b) {
|
||||
if (a.start_line > b.start_line) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.start_line < b.start_line) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseLine (event) {
|
||||
if (!event || !event.stdout) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const { stdout } = event;
|
||||
const lines = stdout.split('\r\n');
|
||||
|
||||
let count = lines.length;
|
||||
let ln = event.start_line;
|
||||
|
||||
const current = createRecord(ln, lines, event);
|
||||
|
||||
const html = lines.reduce((html, line, i) => {
|
||||
ln++;
|
||||
|
||||
const isLastLine = i === lines.length - 1;
|
||||
let row = createRow(current, ln, line);
|
||||
|
||||
if (current && current.isTruncated && isLastLine) {
|
||||
row += createRow(current);
|
||||
count++;
|
||||
}
|
||||
|
||||
return `${html}${row}`;
|
||||
}, '');
|
||||
|
||||
return { html, count };
|
||||
}
|
||||
|
||||
function createRecord (ln, lines, event) {
|
||||
if (!event.uuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = {
|
||||
id: event.id,
|
||||
line: ln + 1,
|
||||
uuid: event.uuid,
|
||||
level: event.event_level,
|
||||
start: event.start_line,
|
||||
end: event.end_line,
|
||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||
isHost: typeof event.host === 'number'
|
||||
};
|
||||
|
||||
if (event.parent_uuid) {
|
||||
info.parents = getParentEvents(event.parent_uuid);
|
||||
}
|
||||
|
||||
if (info.isTruncated) {
|
||||
info.truncatedAt = event.start_line + lines.length;
|
||||
}
|
||||
|
||||
if (EVENT_GROUPS.includes(event.event)) {
|
||||
info.isParent = true;
|
||||
|
||||
if (event.event_level === 1) {
|
||||
parent = event.uuid;
|
||||
}
|
||||
|
||||
if (event.parent_uuid) {
|
||||
if (record[event.parent_uuid]) {
|
||||
if (record[event.parent_uuid].children &&
|
||||
!record[event.parent_uuid].children.includes(event.uuid)) {
|
||||
record[event.parent_uuid].children.push(event.uuid);
|
||||
} else {
|
||||
record[event.parent_uuid].children = [event.uuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TIME_EVENTS.includes(event.event)) {
|
||||
info.time = getTime(event.created);
|
||||
info.line++;
|
||||
}
|
||||
|
||||
record[event.uuid] = info;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function getParentEvents (uuid, list) {
|
||||
list = list || [];
|
||||
|
||||
if (record[uuid]) {
|
||||
list.push(uuid);
|
||||
|
||||
if (record[uuid].parents) {
|
||||
list = list.concat(record[uuid].parents);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function createRow (current, ln, content) {
|
||||
let id = '';
|
||||
let timestamp = '';
|
||||
let tdToggle = '';
|
||||
let tdEvent = '';
|
||||
let classList = '';
|
||||
|
||||
content = content || '';
|
||||
|
||||
if (hasAnsi(content)) {
|
||||
content = ansi.toHtml(content);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
if (current.isParent && current.line === ln) {
|
||||
id = current.uuid;
|
||||
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
|
||||
}
|
||||
|
||||
if (current.isHost) {
|
||||
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}')">${content}</td>`;
|
||||
}
|
||||
|
||||
if (current.time && current.line === ln) {
|
||||
timestamp = `<span>${current.time}</span>`;
|
||||
}
|
||||
|
||||
if (current.parents) {
|
||||
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!tdEvent) {
|
||||
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
|
||||
}
|
||||
|
||||
if (!tdToggle) {
|
||||
tdToggle = '<td class="at-Stdout-toggle"></td>';
|
||||
}
|
||||
|
||||
if (!ln) {
|
||||
ln = '...';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr id="${id}" class="${classList}">
|
||||
${tdToggle}
|
||||
<td class="at-Stdout-line">${ln}</td>
|
||||
${tdEvent}
|
||||
<td class="at-Stdout-time">${timestamp}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function getTime (created) {
|
||||
const date = new Date(created);
|
||||
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
||||
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
|
||||
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
function showHostDetails (id) {
|
||||
jobEvent.request('get', id)
|
||||
.then(() => {
|
||||
@@ -527,100 +250,38 @@ function toggle (uuid, menu) {
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll () {
|
||||
if (vm.scroll.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vm.scroll.register) {
|
||||
$timeout.cancel(vm.scroll.register);
|
||||
}
|
||||
|
||||
vm.scroll.register = $timeout(registerScrollEvent, SCROLL_DELAY);
|
||||
}
|
||||
|
||||
function registerScrollEvent () {
|
||||
vm.scroll.isActive = true;
|
||||
|
||||
const position = container[0].scrollTop;
|
||||
const height = container[0].offsetHeight;
|
||||
const downward = position > vm.scroll.position;
|
||||
|
||||
let promise;
|
||||
|
||||
if (position !== 0 ) {
|
||||
vm.scroll.showBackToTop = true;
|
||||
} else {
|
||||
vm.scroll.showBackToTop = false;
|
||||
}
|
||||
|
||||
|
||||
console.log('downward', downward);
|
||||
if (downward) {
|
||||
if (((height - position) / height) < SCROLL_THRESHOLD) {
|
||||
promise = next;
|
||||
}
|
||||
} else {
|
||||
if ((position / height) < SCROLL_THRESHOLD) {
|
||||
console.log('previous');
|
||||
promise = previous;
|
||||
}
|
||||
}
|
||||
|
||||
vm.scroll.position = position;
|
||||
|
||||
if (!promise) {
|
||||
vm.scroll.isActive = false;
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return promise()
|
||||
.then(() => {
|
||||
console.log('done');
|
||||
vm.scroll.isActive = false;
|
||||
/*
|
||||
*$timeout(() => {
|
||||
* vm.scroll.isActive = false;
|
||||
*}, SCROLL_DELAY);
|
||||
*/
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function scrollHome () {
|
||||
scroll.pause();
|
||||
|
||||
return page.first()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return;
|
||||
}
|
||||
|
||||
return clear()
|
||||
.then(() => prepend(events))
|
||||
return render.clear()
|
||||
.then(() => render.prepend(events))
|
||||
.then(() => {
|
||||
vm.scroll.isActive = false;
|
||||
scroll.setScrollPosition(0);
|
||||
scroll.resume();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollEnd () {
|
||||
if (vm.scroll.isLocked) {
|
||||
if (scroll.isLocked()) {
|
||||
page.bookmark();
|
||||
|
||||
vm.scroll.isLocked = false;
|
||||
vm.scroll.isActive = false;
|
||||
scroll.unlock();
|
||||
|
||||
return;
|
||||
} else if (!vm.scroll.isLocked && vm.stream.isActive) {
|
||||
} else if (!scroll.isLocked() && vm.stream.active) {
|
||||
page.bookmark();
|
||||
|
||||
vm.scroll.isActive = true;
|
||||
vm.scroll.isLocked = true;
|
||||
scroll.lock();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
vm.scroll.isActive = true;
|
||||
scroll.pause();
|
||||
|
||||
return page.last()
|
||||
.then(events => {
|
||||
@@ -628,36 +289,32 @@ function scrollEnd () {
|
||||
return;
|
||||
}
|
||||
|
||||
return clear()
|
||||
.then(() => append(events))
|
||||
return render.clear()
|
||||
.then(() => render.append(events))
|
||||
.then(() => {
|
||||
const container = $(ELEMENT_CONTAINER)[0];
|
||||
|
||||
container.scrollTop = container.scrollHeight;
|
||||
vm.scroll.isActive = false;
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
scroll.resume();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollPageUp () {
|
||||
const container = $(ELEMENT_CONTAINER)[0];
|
||||
const jump = container.scrollTop - container.offsetHeight;
|
||||
|
||||
container.scrollTop = jump;
|
||||
scroll.pageUp();
|
||||
}
|
||||
|
||||
function scrollPageDown () {
|
||||
const container = $(ELEMENT_CONTAINER)[0];
|
||||
const jump = container.scrollTop + container.offsetHeight;
|
||||
scroll.pageDown();
|
||||
}
|
||||
|
||||
container.scrollTop = jump;
|
||||
function scrollIsAtRest (isAtRest) {
|
||||
vm.scroll.showBackToTop = !isAtRest;
|
||||
}
|
||||
|
||||
JobsIndexController.$inject = [
|
||||
'resource',
|
||||
'JobPageService',
|
||||
'$sce',
|
||||
'$timeout',
|
||||
'JobScrollService',
|
||||
'JobRenderService',
|
||||
'$scope',
|
||||
'$compile',
|
||||
'$q'
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import JobsStrings from '~features/output/jobs.strings';
|
||||
import IndexController from '~features/output/index.controller';
|
||||
import atLibModels from '~models';
|
||||
import atLibComponents from '~components';
|
||||
|
||||
import Strings from '~features/output/jobs.strings';
|
||||
import Controller from '~features/output/index.controller';
|
||||
import PageService from '~features/output/page.service';
|
||||
import ScrollService from '~features/output/scroll.service';
|
||||
|
||||
const Template = require('~features/output/index.view.html');
|
||||
|
||||
@@ -178,6 +177,7 @@ angular
|
||||
])
|
||||
.service('JobStrings', Strings)
|
||||
.service('JobPageService', PageService)
|
||||
.service('JobScrollService', ScrollService)
|
||||
.run(JobsRun);
|
||||
|
||||
export default MODULE_NAME;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
function JobPageService ($q) {
|
||||
this.page = null;
|
||||
this.resource = null;
|
||||
this.result = null;
|
||||
this.buffer = null;
|
||||
this.cache = null;
|
||||
|
||||
this.init = resource => {
|
||||
this.resource = resource;
|
||||
|
||||
this.page = {
|
||||
limit: resource.page.pageLimit,
|
||||
size: resource.page.size,
|
||||
limit: this.resource.page.pageLimit,
|
||||
size: this.resource.page.size,
|
||||
current: 0,
|
||||
index: -1,
|
||||
count: 0,
|
||||
@@ -153,13 +147,10 @@ function JobPageService ($q) {
|
||||
}
|
||||
|
||||
this.bookmark = () => {
|
||||
console.log('b,current', this.page.current);
|
||||
if (!this.page.bookmark.active) {
|
||||
this.page.bookmark.first = this.page.first;
|
||||
this.page.bookmark.last = this.page.last;
|
||||
this.page.bookmark.current = this.page.current;
|
||||
|
||||
console.log('b,bookmark', this.page.bookmark.current);
|
||||
this.page.bookmark.active = true;
|
||||
} else {
|
||||
this.page.bookmark.active = false;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import Ansi from 'ansi-to-html';
|
||||
import hasAnsi from 'has-ansi';
|
||||
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||
const JOB_START = 'playbook_on_start';
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
|
||||
const EVENT_GROUPS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY
|
||||
];
|
||||
|
||||
const TIME_EVENTS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY,
|
||||
EVENT_STATS_PLAY
|
||||
];
|
||||
|
||||
const ansi = new Ansi();
|
||||
|
||||
function JobRenderService ($q, $sce, $window) {
|
||||
this.init = ({ compile, apply, get }) => {
|
||||
this.parent = null;
|
||||
this.record = {};
|
||||
this.el = $(ELEMENT_TBODY);
|
||||
this.hooks = { get, compile, apply };
|
||||
};
|
||||
|
||||
this.sortByLineNumber = (a, b) => {
|
||||
if (a.start_line > b.start_line) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.start_line < b.start_line) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
this.transformEventGroup = events => {
|
||||
let lines = 0;
|
||||
let html = '';
|
||||
|
||||
events.sort(this.sortByLineNumber);
|
||||
|
||||
events.forEach(event => {
|
||||
const line = this.transformEvent(event);
|
||||
|
||||
html += line.html;
|
||||
lines += line.count;
|
||||
});
|
||||
|
||||
return { html, lines };
|
||||
};
|
||||
|
||||
this.transformEvent = event => {
|
||||
if (!event || !event.stdout) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const { stdout } = event;
|
||||
const lines = stdout.split('\r\n');
|
||||
|
||||
let count = lines.length;
|
||||
let ln = event.start_line;
|
||||
|
||||
const current = this.createRecord(ln, lines, event);
|
||||
|
||||
const html = lines.reduce((html, line, i) => {
|
||||
ln++;
|
||||
|
||||
const isLastLine = i === lines.length - 1;
|
||||
let row = this.createRow(current, ln, line);
|
||||
|
||||
if (current && current.isTruncated && isLastLine) {
|
||||
row += this.createRow(current);
|
||||
count++;
|
||||
}
|
||||
|
||||
return `${html}${row}`;
|
||||
}, '');
|
||||
|
||||
return { html, count };
|
||||
};
|
||||
|
||||
this.createRecord = event => {
|
||||
if (!event.uuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = {
|
||||
id: event.id,
|
||||
line: ln + 1,
|
||||
uuid: event.uuid,
|
||||
level: event.event_level,
|
||||
start: event.start_line,
|
||||
end: event.end_line,
|
||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||
isHost: typeof event.host === 'number'
|
||||
};
|
||||
|
||||
if (event.parent_uuid) {
|
||||
info.parents = getParentEvents(event.parent_uuid);
|
||||
}
|
||||
|
||||
if (info.isTruncated) {
|
||||
info.truncatedAt = event.start_line + lines.length;
|
||||
}
|
||||
|
||||
if (EVENT_GROUPS.includes(event.event)) {
|
||||
info.isParent = true;
|
||||
|
||||
if (event.event_level === 1) {
|
||||
this.parent = event.uuid;
|
||||
}
|
||||
|
||||
if (event.parent_uuid) {
|
||||
if (this.record[event.parent_uuid]) {
|
||||
if (this.record[event.parent_uuid].children &&
|
||||
!this.record[event.parent_uuid].children.includes(event.uuid)) {
|
||||
this.record[event.parent_uuid].children.push(event.uuid);
|
||||
} else {
|
||||
this.record[event.parent_uuid].children = [event.uuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TIME_EVENTS.includes(event.event)) {
|
||||
info.time = this.getTimestamp(event.created);
|
||||
info.line++;
|
||||
}
|
||||
|
||||
this.record[event.uuid] = info;
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
this.createRow = (current, ln, content) => {
|
||||
let id = '';
|
||||
let timestamp = '';
|
||||
let tdToggle = '';
|
||||
let tdEvent = '';
|
||||
let classList = '';
|
||||
|
||||
content = content || '';
|
||||
|
||||
if (hasAnsi(content)) {
|
||||
content = ansi.toHtml(content);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
if (current.isParent && current.line === ln) {
|
||||
id = current.uuid;
|
||||
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
|
||||
}
|
||||
|
||||
if (current.isHost) {
|
||||
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}')">${content}</td>`;
|
||||
}
|
||||
|
||||
if (current.time && current.line === ln) {
|
||||
timestamp = `<span>${current.time}</span>`;
|
||||
}
|
||||
|
||||
if (current.parents) {
|
||||
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!tdEvent) {
|
||||
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
|
||||
}
|
||||
|
||||
if (!tdToggle) {
|
||||
tdToggle = '<td class="at-Stdout-toggle"></td>';
|
||||
}
|
||||
|
||||
if (!ln) {
|
||||
ln = '...';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr id="${id}" class="${classList}">
|
||||
${tdToggle}
|
||||
<td class="at-Stdout-line">${ln}</td>
|
||||
${tdEvent}
|
||||
<td class="at-Stdout-time">${timestamp}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
this.getTimestamp = (created) => {
|
||||
const date = new Date(created);
|
||||
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
||||
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
|
||||
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
this.getParentEvents = (uuid, list) => {
|
||||
list = list || [];
|
||||
|
||||
if (this.record[uuid]) {
|
||||
list.push(uuid);
|
||||
|
||||
if (this.record[uuid].parents) {
|
||||
list = list.concat(record[uuid].parents);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
this.getEvents = () => {
|
||||
return this.hooks.get();
|
||||
};
|
||||
|
||||
this.insert = (events, insert) => {
|
||||
const result = this.transformEventGroup(events);
|
||||
const html = this.sanitize(result.html);
|
||||
|
||||
return this.requestAnimationFrame(() => insert(html))
|
||||
.then(() => this.compile(html))
|
||||
.then(() => result.lines);
|
||||
};
|
||||
|
||||
this.remove = elements => {
|
||||
return this.requestAnimationFrame(() => {
|
||||
elements.empty();
|
||||
elements.remove();
|
||||
});
|
||||
};
|
||||
|
||||
this.requestAnimationFrame = fn => {
|
||||
return $q(resolve => {
|
||||
$window.requestAnimationFrame(() => {
|
||||
if (fn) {
|
||||
fn();
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.compile = html => {
|
||||
this.hooks.compile(html);
|
||||
|
||||
return this.requestAnimationFrame();
|
||||
};
|
||||
|
||||
this.build = () => {
|
||||
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const elements = this.el.children();
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.shift = lines => {
|
||||
const elements = this.el.children().slice(0, lines);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.pop = lines => {
|
||||
const elements = this.el.children().slice(-lines);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.prepend = events => {
|
||||
return this.insert(events, html => this.el.prepend(html))
|
||||
};
|
||||
|
||||
this.append = events => {
|
||||
return this.insert(events, html => this.el.append(html))
|
||||
};
|
||||
|
||||
// TODO: stdout from the API should not be trusted.
|
||||
this.sanitize = html => {
|
||||
html = $sce.trustAsHtml(html);
|
||||
|
||||
return $sce.getTrustedHtml(html);
|
||||
};
|
||||
}
|
||||
|
||||
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
||||
|
||||
export default JobRenderService;
|
||||
|
||||
172
awx/ui/client/features/output/scroll.service.js
Normal file
172
awx/ui/client/features/output/scroll.service.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
const DELAY = 100;
|
||||
const THRESHOLD = 0.1;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = (hooks) => {
|
||||
this.el = $(ELEMENT_CONTAINER);
|
||||
this.timer = null;
|
||||
|
||||
this.position = {
|
||||
previous: 0,
|
||||
current: 0
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
isAtRest: hooks.isAtRest,
|
||||
next: hooks.next,
|
||||
previous: hooks.previous
|
||||
};
|
||||
|
||||
this.state = {
|
||||
locked: false,
|
||||
paused: false,
|
||||
top: true
|
||||
};
|
||||
|
||||
this.el.scroll(this.listen);
|
||||
};
|
||||
|
||||
this.listen = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timer) {
|
||||
$timeout.cancel(this.timer);
|
||||
}
|
||||
|
||||
this.timer = $timeout(this.register, DELAY);
|
||||
};
|
||||
|
||||
this.register = () => {
|
||||
this.pause();
|
||||
|
||||
const height = this.getScrollHeight();
|
||||
const current = this.getScrollPosition();
|
||||
const downward = current > this.position.previous;
|
||||
|
||||
let promise;
|
||||
|
||||
if (downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.next;
|
||||
} else if (!downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.previous;
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
this.setScrollPosition(current);
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return promise()
|
||||
.then(() => {
|
||||
this.setScrollPosition(this.getScrollPosition());
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
});
|
||||
};
|
||||
|
||||
this.isBeyondThreshold = (downward, current) => {
|
||||
const previous = this.position.previous;
|
||||
const height = this.getScrollHeight();
|
||||
|
||||
if (downward) {
|
||||
current += this.getViewableHeight();
|
||||
|
||||
if (current >= height || ((height - current) / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (current <= 0 || (current / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.pageUp = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top - height);
|
||||
};
|
||||
|
||||
this.pageDown = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top + height);
|
||||
};
|
||||
|
||||
this.getScrollHeight = () => {
|
||||
return this.el[0].scrollHeight;
|
||||
};
|
||||
|
||||
this.getViewableHeight = () => {
|
||||
return this.el[0].offsetHeight;
|
||||
};
|
||||
|
||||
this.getScrollPosition = () => {
|
||||
return this.el[0].scrollTop;
|
||||
};
|
||||
|
||||
this.setScrollPosition = position => {
|
||||
this.position.previous = this.position.current;
|
||||
this.position.current = position;
|
||||
this.el[0].scrollTop = position;
|
||||
this.isAtRest();
|
||||
};
|
||||
|
||||
this.isAtRest = () => {
|
||||
if (this.position.current === 0 && !this.state.top) {
|
||||
this.state.top = true;
|
||||
this.hooks.isAtRest(true);
|
||||
} else if (this.position.current > 0 && this.state.top) {
|
||||
this.state.top = false;
|
||||
this.hooks.isAtRest(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.isPaused = () => {
|
||||
return this.state.paused;
|
||||
};
|
||||
|
||||
this.lock = () => {
|
||||
this.state.locked = true;
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.unlock = () => {
|
||||
this.state.locked = false;
|
||||
this.state.paused = false;
|
||||
};
|
||||
|
||||
this.isLocked = () => {
|
||||
return this.state.locked;
|
||||
};
|
||||
}
|
||||
|
||||
JobScrollService.$inject = ['$q', '$timeout'];
|
||||
|
||||
export default JobScrollService;
|
||||
Reference in New Issue
Block a user