mirror of
https://github.com/ansible/awx.git
synced 2026-01-17 04:31:21 -03:30
Merge pull request #2057 from jakemcdermott/job-results/slide
fix memory leak in output render service, rewrite output data storage
This commit is contained in:
commit
f78f179789
@ -1,144 +1,94 @@
|
||||
const PAGE_LIMIT = 5;
|
||||
const API_PAGE_SIZE = 200;
|
||||
const PAGE_SIZE = 50;
|
||||
const ORDER_BY = 'counter';
|
||||
|
||||
const BASE_PARAMS = {
|
||||
order_by: 'start_line',
|
||||
page_size: PAGE_SIZE,
|
||||
order_by: ORDER_BY,
|
||||
};
|
||||
|
||||
const merge = (...objs) => _.merge({}, ...objs);
|
||||
|
||||
const getInitialState = params => ({
|
||||
results: [],
|
||||
count: 0,
|
||||
previous: 1,
|
||||
page: 1,
|
||||
next: 1,
|
||||
last: 1,
|
||||
params: merge(BASE_PARAMS, params),
|
||||
});
|
||||
|
||||
function JobEventsApiService ($http, $q) {
|
||||
this.init = (endpoint, params) => {
|
||||
this.keys = [];
|
||||
this.cache = {};
|
||||
this.pageSizes = {};
|
||||
this.endpoint = endpoint;
|
||||
this.state = getInitialState(params);
|
||||
this.params = merge(BASE_PARAMS, params);
|
||||
|
||||
this.state = { current: 0, count: 0 };
|
||||
};
|
||||
|
||||
this.getLastPage = count => Math.ceil(count / this.state.params.page_size);
|
||||
this.fetch = () => this.getFirst().then(() => this);
|
||||
|
||||
this.clearCache = () => {
|
||||
delete this.cache;
|
||||
delete this.keys;
|
||||
delete this.pageSizes;
|
||||
this.getFirst = () => {
|
||||
const page = 1;
|
||||
const params = merge(this.params, { page });
|
||||
|
||||
this.cache = {};
|
||||
this.keys = [];
|
||||
this.pageSizes = {};
|
||||
};
|
||||
|
||||
this.fetch = () => this.first().then(() => this);
|
||||
|
||||
this.getPage = number => {
|
||||
if (number < 1 || number > this.state.last) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (this.cache[number]) {
|
||||
if (this.pageSizes[number] === PAGE_SIZE) {
|
||||
return this.cache[number];
|
||||
}
|
||||
|
||||
delete this.pageSizes[number];
|
||||
delete this.cache[number];
|
||||
|
||||
this.keys.splice(this.keys.indexOf(number));
|
||||
}
|
||||
|
||||
const { params } = this.state;
|
||||
|
||||
delete params.page;
|
||||
|
||||
params.page = number;
|
||||
|
||||
const promise = $http.get(this.endpoint, { params })
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results, count } = data;
|
||||
|
||||
this.state.results = results;
|
||||
this.state.count = count;
|
||||
this.state.page = number;
|
||||
this.state.last = this.getLastPage(count);
|
||||
this.state.previous = Math.max(1, number - 1);
|
||||
this.state.next = Math.min(this.state.last, number + 1);
|
||||
this.state.current = page;
|
||||
|
||||
this.pageSizes[number] = results.length;
|
||||
|
||||
return { results, page: number };
|
||||
return results;
|
||||
});
|
||||
|
||||
if (number === 1) {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
this.cache[number] = promise;
|
||||
this.keys.push(number);
|
||||
|
||||
if (this.keys.length > PAGE_LIMIT) {
|
||||
const remove = this.keys.shift();
|
||||
|
||||
delete this.cache[remove];
|
||||
delete this.pageSizes[remove];
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
this.first = () => this.getPage(1);
|
||||
this.next = () => this.getPage(this.state.next);
|
||||
this.previous = () => this.getPage(this.state.previous);
|
||||
this.getRange = range => {
|
||||
if (!range) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
this.last = () => {
|
||||
const params = merge({}, this.state.params);
|
||||
const [low, high] = range;
|
||||
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
|
||||
|
||||
delete params.page;
|
||||
delete params.order_by;
|
||||
params.page_size = API_PAGE_SIZE;
|
||||
|
||||
params.page = 1;
|
||||
params.order_by = '-start_line';
|
||||
|
||||
const promise = $http.get(this.endpoint, { params })
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results, count } = data;
|
||||
const lastPage = this.getLastPage(count);
|
||||
const { results } = data;
|
||||
const maxCounter = Math.max(results.map(({ counter }) => counter));
|
||||
|
||||
this.state.current = Math.ceil(maxCounter / PAGE_SIZE);
|
||||
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
this.getLast = () => {
|
||||
const params = merge(this.params, { page: 1, order_by: `-${ORDER_BY}` });
|
||||
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results } = data;
|
||||
const count = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
let rotated = results;
|
||||
|
||||
if (count > PAGE_SIZE) {
|
||||
results.splice(count % PAGE_SIZE);
|
||||
rotated = results.splice(count % PAGE_SIZE);
|
||||
|
||||
if (results.length > 0) {
|
||||
rotated = results;
|
||||
}
|
||||
}
|
||||
|
||||
results.reverse();
|
||||
|
||||
this.state.results = results;
|
||||
this.state.count = count;
|
||||
this.state.page = lastPage;
|
||||
this.state.next = lastPage;
|
||||
this.state.last = lastPage;
|
||||
this.state.previous = Math.max(1, this.state.page - 1);
|
||||
this.state.current = Math.ceil(count / PAGE_SIZE);
|
||||
|
||||
this.clearCache();
|
||||
|
||||
return { results, page: lastPage };
|
||||
return rotated;
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
this.getCurrentPageNumber = () => this.state.current;
|
||||
this.getLastPageNumber = () => Math.ceil(this.state.count / PAGE_SIZE);
|
||||
this.getPreviousPageNumber = () => Math.max(1, this.state.current - 1);
|
||||
this.getNextPageNumber = () => Math.min(this.state.current + 1, this.getLastPageNumber());
|
||||
this.getMaxCounter = () => this.state.count;
|
||||
|
||||
this.getNext = () => this.getPage(this.getNextPageNumber());
|
||||
this.getPrevious = () => this.getPage(this.getPreviousPageNumber());
|
||||
}
|
||||
|
||||
JobEventsApiService.$inject = [
|
||||
'$http',
|
||||
'$q'
|
||||
];
|
||||
JobEventsApiService.$inject = ['$http', '$q'];
|
||||
|
||||
export default JobEventsApiService;
|
||||
|
||||
@ -729,7 +729,7 @@ JobDetailsController.$inject = [
|
||||
'OutputStrings',
|
||||
'Wait',
|
||||
'ParseVariableString',
|
||||
'JobStatusService',
|
||||
'OutputStatusService',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<!-- todo: further componentization -->
|
||||
<div class="JobResults-panelHeader">
|
||||
<div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div>
|
||||
<!-- LEFT PANE HEADER ACTIONS -->
|
||||
|
||||
@ -1,235 +0,0 @@
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
const MAX_LAG = 120;
|
||||
|
||||
function JobEventEngine ($q) {
|
||||
this.init = ({ resource, scroll, page, onEventFrame, onStart, onStop }) => {
|
||||
this.resource = resource;
|
||||
this.scroll = scroll;
|
||||
this.page = page;
|
||||
|
||||
this.lag = 0;
|
||||
this.count = 0;
|
||||
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,
|
||||
counting: false,
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
onEventFrame,
|
||||
onStart,
|
||||
onStop,
|
||||
};
|
||||
|
||||
this.lines = {
|
||||
used: [],
|
||||
missing: [],
|
||||
ready: false,
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
};
|
||||
|
||||
this.setMinLine = min => {
|
||||
if (min > this.lines.min) {
|
||||
this.lines.min = min;
|
||||
}
|
||||
};
|
||||
|
||||
this.getBatchFactors = size => {
|
||||
const factors = [1];
|
||||
|
||||
for (let i = 2; i <= size / 2; i++) {
|
||||
if (size % i === 0) {
|
||||
factors.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
factors.push(size);
|
||||
|
||||
return factors;
|
||||
};
|
||||
|
||||
this.getBatchFactorIndex = () => {
|
||||
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
|
||||
|
||||
return index > this.factors.length - 1 ? this.factors.length - 1 : index;
|
||||
};
|
||||
|
||||
this.setBatchFrameCount = () => {
|
||||
const index = this.getBatchFactorIndex();
|
||||
|
||||
this.framesPerRender = this.factors[index];
|
||||
};
|
||||
|
||||
this.buffer = data => {
|
||||
const pageAdded = this.page.addToBuffer(data);
|
||||
|
||||
if (pageAdded) {
|
||||
this.pageCount++;
|
||||
this.setBatchFrameCount();
|
||||
|
||||
if (this.isPausing()) {
|
||||
this.pause(true);
|
||||
} else if (this.isResuming()) {
|
||||
this.resume(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.checkLines = data => {
|
||||
for (let i = data.start_line; i < data.end_line; i++) {
|
||||
if (i > this.lines.max) {
|
||||
this.lines.max = i;
|
||||
}
|
||||
|
||||
this.lines.used.push(i);
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
for (let i = this.lines.min; i < this.lines.max; i++) {
|
||||
if (this.lines.used.indexOf(i) === -1) {
|
||||
missing.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length === 0) {
|
||||
this.lines.ready = true;
|
||||
this.lines.min = this.lines.max + 1;
|
||||
this.lines.used = [];
|
||||
} else {
|
||||
this.lines.ready = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.pushJobEvent = data => {
|
||||
this.lag++;
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
if (data.end_line < this.lines.min) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (!this.isActive()) {
|
||||
this.start();
|
||||
} else if (data.event === JOB_END) {
|
||||
if (this.isPaused()) {
|
||||
this.end(true);
|
||||
} else {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
this.checkLines(data);
|
||||
this.buffer(data);
|
||||
this.count++;
|
||||
|
||||
if (!this.isReadyToRender()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const events = this.page.emptyBuffer();
|
||||
this.count -= events.length;
|
||||
|
||||
return this.renderFrame(events);
|
||||
})
|
||||
.then(() => --this.lag);
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.renderFrame = events => this.hooks.onEventFrame(events)
|
||||
.then(() => {
|
||||
if (this.scroll.isLocked()) {
|
||||
this.scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
if (this.isEnding()) {
|
||||
const lastEvents = this.page.emptyBuffer();
|
||||
|
||||
if (lastEvents.length) {
|
||||
return this.renderFrame(lastEvents);
|
||||
}
|
||||
|
||||
this.end(true);
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.resume = done => {
|
||||
if (done) {
|
||||
this.state.resuming = false;
|
||||
this.state.paused = false;
|
||||
} else if (!this.isTransitioning()) {
|
||||
this.scroll.pause();
|
||||
this.scroll.lock();
|
||||
this.scroll.scrollToBottom();
|
||||
this.state.resuming = true;
|
||||
this.page.removeBookmark();
|
||||
}
|
||||
};
|
||||
|
||||
this.pause = done => {
|
||||
if (done) {
|
||||
this.state.pausing = false;
|
||||
this.state.paused = true;
|
||||
this.scroll.resume();
|
||||
} else if (!this.isTransitioning()) {
|
||||
this.scroll.pause();
|
||||
this.scroll.unlock();
|
||||
this.state.pausing = true;
|
||||
this.page.setBookmark();
|
||||
}
|
||||
};
|
||||
|
||||
this.start = () => {
|
||||
if (!this.state.ending && !this.state.ended) {
|
||||
this.state.started = true;
|
||||
this.scroll.pause();
|
||||
this.scroll.lock();
|
||||
|
||||
this.hooks.onStart();
|
||||
}
|
||||
};
|
||||
|
||||
this.end = done => {
|
||||
if (done) {
|
||||
this.state.ending = false;
|
||||
this.state.ended = true;
|
||||
this.scroll.unlock();
|
||||
this.scroll.resume();
|
||||
|
||||
this.hooks.onStop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.ending = true;
|
||||
};
|
||||
|
||||
this.isReadyToRender = () => this.isDone() ||
|
||||
(!this.isPaused() && this.hasAllLines() && this.isBatchFull());
|
||||
this.hasAllLines = () => this.lines.ready;
|
||||
this.isBatchFull = () => this.count % this.framesPerRender === 0;
|
||||
this.isPaused = () => this.state.paused;
|
||||
this.isPausing = () => this.state.pausing;
|
||||
this.isResuming = () => this.state.resuming;
|
||||
this.isTransitioning = () => this.isActive() && (this.state.pausing || this.state.resuming);
|
||||
this.isActive = () => this.state.started && !this.state.ended;
|
||||
this.isEnding = () => this.state.ending;
|
||||
this.isDone = () => this.state.ended;
|
||||
}
|
||||
|
||||
JobEventEngine.$inject = ['$q'];
|
||||
|
||||
export default JobEventEngine;
|
||||
@ -1,126 +1,156 @@
|
||||
/* eslint camelcase: 0 */
|
||||
let $compile;
|
||||
let $filter;
|
||||
let $q;
|
||||
let $scope;
|
||||
let $state;
|
||||
|
||||
let page;
|
||||
let render;
|
||||
let resource;
|
||||
let render;
|
||||
let scroll;
|
||||
let engine;
|
||||
let status;
|
||||
let slide;
|
||||
let stream;
|
||||
|
||||
let vm;
|
||||
let streaming;
|
||||
let listeners = [];
|
||||
|
||||
function JobsIndexController (
|
||||
_$compile_,
|
||||
_$filter_,
|
||||
_$q_,
|
||||
_$scope_,
|
||||
_resource_,
|
||||
_page_,
|
||||
_scroll_,
|
||||
_render_,
|
||||
_engine_,
|
||||
_status_,
|
||||
_strings_,
|
||||
) {
|
||||
vm = this || {};
|
||||
const bufferState = [0, 0]; // [length, count]
|
||||
const listeners = [];
|
||||
const rx = [];
|
||||
|
||||
$compile = _$compile_;
|
||||
$filter = _$filter_;
|
||||
$q = _$q_;
|
||||
$scope = _$scope_;
|
||||
let following = false;
|
||||
|
||||
resource = _resource_;
|
||||
page = _page_;
|
||||
scroll = _scroll_;
|
||||
render = _render_;
|
||||
engine = _engine_;
|
||||
status = _status_;
|
||||
function bufferInit () {
|
||||
rx.length = 0;
|
||||
|
||||
vm.strings = _strings_;
|
||||
|
||||
// Development helper(s)
|
||||
vm.clear = devClear;
|
||||
|
||||
// Expand/collapse
|
||||
vm.expanded = false;
|
||||
vm.toggleExpanded = toggleExpanded;
|
||||
|
||||
// Panel
|
||||
vm.resource = resource;
|
||||
vm.title = $filter('sanitize')(resource.model.get('name'));
|
||||
|
||||
// Stdout Navigation
|
||||
vm.scroll = {
|
||||
showBackToTop: false,
|
||||
home: scrollFirst,
|
||||
end: scrollLast,
|
||||
down: scrollPageDown,
|
||||
up: scrollPageUp
|
||||
};
|
||||
|
||||
render.requestAnimationFrame(() => init());
|
||||
bufferState[0] = 0;
|
||||
bufferState[1] = 0;
|
||||
}
|
||||
|
||||
function init () {
|
||||
status.init({
|
||||
resource,
|
||||
});
|
||||
function bufferAdd (event) {
|
||||
rx.push(event);
|
||||
|
||||
page.init({
|
||||
resource,
|
||||
});
|
||||
bufferState[0] += 1;
|
||||
bufferState[1] += 1;
|
||||
|
||||
render.init({
|
||||
compile: html => $compile(html)($scope),
|
||||
isStreamActive: engine.isActive,
|
||||
});
|
||||
return bufferState;
|
||||
}
|
||||
|
||||
scroll.init({
|
||||
isAtRest: scrollIsAtRest,
|
||||
previous,
|
||||
next,
|
||||
});
|
||||
function bufferEmpty () {
|
||||
bufferState[0] = 0;
|
||||
|
||||
engine.init({
|
||||
page,
|
||||
scroll,
|
||||
resource,
|
||||
onEventFrame (events) {
|
||||
return shift().then(() => append(events, true));
|
||||
},
|
||||
onStart () {
|
||||
status.setJobStatus('running');
|
||||
},
|
||||
onStop () {
|
||||
stopListening();
|
||||
status.updateStats();
|
||||
status.dispatch();
|
||||
return rx.splice(0, rx.length);
|
||||
}
|
||||
|
||||
function onFrames (events) {
|
||||
if (!following) {
|
||||
const minCounter = Math.min(...events.map(({ counter }) => counter));
|
||||
// attachment range
|
||||
const max = slide.getTailCounter() + 1;
|
||||
const min = Math.max(1, slide.getHeadCounter(), max - 50);
|
||||
|
||||
if (minCounter > max || minCounter < min) {
|
||||
return $q.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
streaming = false;
|
||||
|
||||
if (status.state.running) {
|
||||
return scrollLast().then(() => startListening());
|
||||
} else if (!status.state.finished) {
|
||||
return scrollFirst().then(() => startListening());
|
||||
follow();
|
||||
}
|
||||
|
||||
return scrollLast();
|
||||
const capacity = slide.getCapacity();
|
||||
|
||||
if (capacity >= events.length) {
|
||||
return slide.pushFront(events);
|
||||
}
|
||||
|
||||
delete render.record;
|
||||
|
||||
render.record = {};
|
||||
|
||||
return slide.popBack(events.length - capacity)
|
||||
.then(() => slide.pushFront(events))
|
||||
.then(() => {
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function first () {
|
||||
unfollow();
|
||||
scroll.pause();
|
||||
|
||||
return slide.getFirst()
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function next () {
|
||||
return slide.slideDown();
|
||||
}
|
||||
|
||||
function previous () {
|
||||
unfollow();
|
||||
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
|
||||
return slide.slideUp()
|
||||
.then(changed => {
|
||||
if (changed[0] !== 0 || changed[1] !== 0) {
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
scroll.setScrollPosition((currentHeight / 4) - initialPosition);
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function last () {
|
||||
scroll.pause();
|
||||
|
||||
return slide.getLast()
|
||||
.then(() => {
|
||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function compile (html) {
|
||||
return $compile(html)($scope);
|
||||
}
|
||||
|
||||
function follow () {
|
||||
scroll.pause();
|
||||
// scroll.hide();
|
||||
|
||||
following = true;
|
||||
}
|
||||
|
||||
function unfollow () {
|
||||
following = false;
|
||||
|
||||
// scroll.unhide();
|
||||
scroll.resume();
|
||||
}
|
||||
|
||||
function showHostDetails (id, uuid) {
|
||||
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
||||
}
|
||||
|
||||
function stopListening () {
|
||||
listeners.forEach(deregister => deregister());
|
||||
listeners = [];
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
function startListening () {
|
||||
stopListening();
|
||||
|
||||
listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
|
||||
listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));
|
||||
}
|
||||
@ -130,280 +160,95 @@ function handleStatusEvent (data) {
|
||||
}
|
||||
|
||||
function handleJobEvent (data) {
|
||||
streaming = streaming || attachToRunningJob();
|
||||
stream.pushJobEvent(data);
|
||||
status.pushJobEvent(data);
|
||||
}
|
||||
|
||||
streaming.then(() => {
|
||||
engine.pushJobEvent(data);
|
||||
status.pushJobEvent(data);
|
||||
function OutputIndexController (
|
||||
_$compile_,
|
||||
_$q_,
|
||||
_$scope_,
|
||||
_$state_,
|
||||
_resource_,
|
||||
_scroll_,
|
||||
_render_,
|
||||
_status_,
|
||||
_slide_,
|
||||
_stream_,
|
||||
$filter,
|
||||
strings,
|
||||
) {
|
||||
$compile = _$compile_;
|
||||
$q = _$q_;
|
||||
$scope = _$scope_;
|
||||
$state = _$state_;
|
||||
|
||||
resource = _resource_;
|
||||
scroll = _scroll_;
|
||||
render = _render_;
|
||||
slide = _slide_;
|
||||
status = _status_;
|
||||
stream = _stream_;
|
||||
|
||||
vm = this || {};
|
||||
|
||||
// Panel
|
||||
vm.strings = strings;
|
||||
vm.resource = resource;
|
||||
vm.title = $filter('sanitize')(resource.model.get('name'));
|
||||
|
||||
vm.expanded = false;
|
||||
vm.showHostDetails = showHostDetails;
|
||||
vm.toggleExpanded = () => { vm.expanded = !vm.expanded; };
|
||||
|
||||
// Stdout Navigation
|
||||
vm.menu = {
|
||||
end: last,
|
||||
home: first,
|
||||
up: previous,
|
||||
down: next,
|
||||
};
|
||||
|
||||
render.requestAnimationFrame(() => {
|
||||
bufferInit();
|
||||
|
||||
status.init(resource);
|
||||
slide.init(render, resource.events);
|
||||
|
||||
render.init({ compile });
|
||||
scroll.init({ previous, next });
|
||||
|
||||
stream.init({
|
||||
bufferAdd,
|
||||
bufferEmpty,
|
||||
onFrames,
|
||||
onStop () {
|
||||
stopListening();
|
||||
status.updateStats();
|
||||
status.dispatch();
|
||||
unfollow();
|
||||
}
|
||||
});
|
||||
|
||||
startListening();
|
||||
|
||||
return last();
|
||||
});
|
||||
}
|
||||
|
||||
function next () {
|
||||
return page.next()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return shift()
|
||||
.then(() => append(events))
|
||||
.then(() => {
|
||||
if (scroll.isMissing()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
let postPopHeight;
|
||||
|
||||
return page.previous()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return pop()
|
||||
.then(() => {
|
||||
postPopHeight = scroll.getScrollHeight();
|
||||
|
||||
return prepend(events);
|
||||
})
|
||||
.then(() => {
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function append (events, eng) {
|
||||
return render.append(events)
|
||||
.then(count => {
|
||||
page.updateLineCount(count, eng);
|
||||
});
|
||||
}
|
||||
|
||||
function prepend (events) {
|
||||
return render.prepend(events)
|
||||
.then(count => {
|
||||
page.updateLineCount(count);
|
||||
});
|
||||
}
|
||||
|
||||
function pop () {
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const lines = page.trim();
|
||||
|
||||
return render.pop(lines);
|
||||
}
|
||||
|
||||
function shift () {
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const lines = page.trim(true);
|
||||
|
||||
return render.shift(lines);
|
||||
}
|
||||
|
||||
function scrollFirst () {
|
||||
if (engine.isActive()) {
|
||||
if (engine.isTransitioning()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (!engine.isPaused()) {
|
||||
engine.pause(true);
|
||||
}
|
||||
} else if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return page.first()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return render.clear()
|
||||
.then(() => prepend(events))
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
scroll.resume();
|
||||
})
|
||||
.then(() => {
|
||||
if (scroll.isMissing()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollLast () {
|
||||
if (engine.isActive()) {
|
||||
if (engine.isTransitioning()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (engine.isPaused()) {
|
||||
engine.resume(true);
|
||||
}
|
||||
} else if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return render.clear()
|
||||
.then(() => page.last())
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const minLine = 1 + Math.max(...events.map(event => event.end_line));
|
||||
engine.setMinLine(minLine);
|
||||
|
||||
return append(events);
|
||||
})
|
||||
.then(() => {
|
||||
if (!engine.isActive()) {
|
||||
scroll.resume();
|
||||
}
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
})
|
||||
.then(() => {
|
||||
if (!engine.isActive() && scroll.isMissing()) {
|
||||
return previous();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function attachToRunningJob () {
|
||||
if (engine.isActive()) {
|
||||
if (engine.isTransitioning()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (engine.isPaused()) {
|
||||
engine.resume(true);
|
||||
}
|
||||
} else if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return page.last()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const minLine = 1 + Math.max(...events.map(event => event.end_line));
|
||||
engine.setMinLine(minLine);
|
||||
|
||||
return append(events);
|
||||
})
|
||||
.then(() => {
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
});
|
||||
}
|
||||
|
||||
function scrollPageUp () {
|
||||
if (scroll.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.pageUp();
|
||||
}
|
||||
|
||||
function scrollPageDown () {
|
||||
if (scroll.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.pageDown();
|
||||
}
|
||||
|
||||
function scrollIsAtRest (isAtRest) {
|
||||
vm.scroll.showBackToTop = !isAtRest;
|
||||
}
|
||||
|
||||
function toggleExpanded () {
|
||||
vm.expanded = !vm.expanded;
|
||||
}
|
||||
|
||||
function devClear () {
|
||||
render.clear().then(() => init());
|
||||
}
|
||||
|
||||
// function showHostDetails (id) {
|
||||
// jobEvent.request('get', id)
|
||||
// .then(() => {
|
||||
// const title = jobEvent.get('host_name');
|
||||
|
||||
// vm.host = {
|
||||
// menu: true,
|
||||
// stdout: jobEvent.get('stdout')
|
||||
// };
|
||||
|
||||
// $scope.jobs.modal.show(title);
|
||||
// });
|
||||
// }
|
||||
|
||||
// function toggle (uuid, menu) {
|
||||
// const lines = $(`.child-of-${uuid}`);
|
||||
// let icon = $(`#${uuid} .at-Stdout-toggle > i`);
|
||||
|
||||
// if (menu || record[uuid].level === 1) {
|
||||
// vm.isExpanded = !vm.isExpanded;
|
||||
// }
|
||||
|
||||
// if (record[uuid].children) {
|
||||
// icon = icon.add($(`#${record[uuid].children.join(', #')}`)
|
||||
// .find('.at-Stdout-toggle > i'));
|
||||
// }
|
||||
|
||||
// if (icon.hasClass('fa-angle-down')) {
|
||||
// icon.addClass('fa-angle-right');
|
||||
// icon.removeClass('fa-angle-down');
|
||||
|
||||
// lines.addClass('hidden');
|
||||
// } else {
|
||||
// icon.addClass('fa-angle-down');
|
||||
// icon.removeClass('fa-angle-right');
|
||||
|
||||
// lines.removeClass('hidden');
|
||||
// }
|
||||
// }
|
||||
|
||||
JobsIndexController.$inject = [
|
||||
OutputIndexController.$inject = [
|
||||
'$compile',
|
||||
'$filter',
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'resource',
|
||||
'JobPageService',
|
||||
'JobScrollService',
|
||||
'JobRenderService',
|
||||
'JobEventEngine',
|
||||
'JobStatusService',
|
||||
'OutputScrollService',
|
||||
'OutputRenderService',
|
||||
'OutputStatusService',
|
||||
'OutputSlideService',
|
||||
'OutputStreamService',
|
||||
'$filter',
|
||||
'OutputStrings',
|
||||
];
|
||||
|
||||
module.exports = JobsIndexController;
|
||||
module.exports = OutputIndexController;
|
||||
|
||||
@ -3,13 +3,13 @@ import atLibComponents from '~components';
|
||||
|
||||
import Strings from '~features/output/output.strings';
|
||||
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 EngineService from '~features/output/engine.service';
|
||||
import StreamService from '~features/output/stream.service';
|
||||
import StatusService from '~features/output/status.service';
|
||||
import MessageService from '~features/output/message.service';
|
||||
import EventsApiService from '~features/output/api.events.service';
|
||||
import SlideService from '~features/output/slide.service';
|
||||
import LegacyRedirect from '~features/output/legacy.route';
|
||||
|
||||
import DetailsComponent from '~features/output/details.component';
|
||||
@ -24,6 +24,7 @@ const MODULE_NAME = 'at.features.output';
|
||||
const PAGE_CACHE = true;
|
||||
const PAGE_LIMIT = 5;
|
||||
const PAGE_SIZE = 50;
|
||||
const ORDER_BY = 'counter';
|
||||
const WS_PREFIX = 'ws';
|
||||
|
||||
function resolveResource (
|
||||
@ -74,7 +75,7 @@ function resolveResource (
|
||||
|
||||
const params = {
|
||||
page_size: PAGE_SIZE,
|
||||
order_by: 'start_line',
|
||||
order_by: ORDER_BY,
|
||||
};
|
||||
|
||||
const config = {
|
||||
@ -250,13 +251,13 @@ angular
|
||||
HostEvent
|
||||
])
|
||||
.service('OutputStrings', Strings)
|
||||
.service('JobPageService', PageService)
|
||||
.service('JobScrollService', ScrollService)
|
||||
.service('JobRenderService', RenderService)
|
||||
.service('JobEventEngine', EngineService)
|
||||
.service('JobStatusService', StatusService)
|
||||
.service('JobMessageService', MessageService)
|
||||
.service('OutputScrollService', ScrollService)
|
||||
.service('OutputRenderService', RenderService)
|
||||
.service('OutputStreamService', StreamService)
|
||||
.service('OutputStatusService', StatusService)
|
||||
.service('OutputMessageService', MessageService)
|
||||
.service('JobEventsApiService', EventsApiService)
|
||||
.service('OutputSlideService', SlideService)
|
||||
.component('atJobSearch', SearchComponent)
|
||||
.component('atJobStats', StatsComponent)
|
||||
.component('atJobDetails', DetailsComponent)
|
||||
|
||||
@ -22,17 +22,17 @@
|
||||
ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" ng-click="vm.scroll.end()">
|
||||
<div class="pull-right" ng-click="vm.menu.end()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
ng-class=" { 'at-Stdout-menuIcon--active': vm.scroll.isLocked }"></i>
|
||||
ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.home()">
|
||||
<div class="pull-right" ng-click="vm.menu.home()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.down()">
|
||||
<div class="pull-right" ng-click="vm.menu.down()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.up()">
|
||||
<div class="pull-right" ng-click="vm.menu.up()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
|
||||
</div>
|
||||
|
||||
@ -52,8 +52,8 @@
|
||||
</table>
|
||||
</pre>
|
||||
|
||||
<div ng-show="vm.scroll.showBackToTop" class="at-Stdout-menuBottom">
|
||||
<div class="at-Stdout-menuIconGroup" ng-click="vm.scroll.home()">
|
||||
<div ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
|
||||
<div class="at-Stdout-menuIconGroup" ng-click="vm.menu.home()">
|
||||
<p class="pull-left"><i class="fa fa-angle-double-up"></i></p>
|
||||
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
|
||||
</div>
|
||||
|
||||
@ -1,283 +0,0 @@
|
||||
function JobPageService ($q) {
|
||||
this.init = ({ resource }) => {
|
||||
this.resource = resource;
|
||||
this.api = this.resource.events;
|
||||
|
||||
this.page = {
|
||||
limit: this.resource.page.pageLimit,
|
||||
size: this.resource.page.size,
|
||||
cache: [],
|
||||
state: {
|
||||
count: 0,
|
||||
current: 0,
|
||||
first: 0,
|
||||
last: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.bookmark = {
|
||||
pending: false,
|
||||
set: true,
|
||||
cache: [],
|
||||
state: {
|
||||
count: 0,
|
||||
first: 0,
|
||||
last: 0,
|
||||
current: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.result = {
|
||||
limit: this.page.limit * this.page.size,
|
||||
count: 0
|
||||
};
|
||||
|
||||
this.buffer = {
|
||||
count: 0
|
||||
};
|
||||
};
|
||||
|
||||
this.addPage = (number, events, push, reference) => {
|
||||
const page = { number, events, lines: 0 };
|
||||
reference = reference || this.getActiveReference();
|
||||
|
||||
if (push) {
|
||||
reference.cache.push(page);
|
||||
reference.state.last = page.number;
|
||||
reference.state.first = reference.cache[0].number;
|
||||
} else {
|
||||
reference.cache.unshift(page);
|
||||
reference.state.first = page.number;
|
||||
reference.state.last = reference.cache[reference.cache.length - 1].number;
|
||||
}
|
||||
|
||||
reference.state.current = page.number;
|
||||
reference.state.count++;
|
||||
};
|
||||
|
||||
this.addToBuffer = event => {
|
||||
const reference = this.getReference();
|
||||
const index = reference.cache.length - 1;
|
||||
let pageAdded = false;
|
||||
|
||||
if (this.result.count % this.page.size === 0) {
|
||||
this.addPage(reference.state.current + 1, [event], true, reference);
|
||||
|
||||
if (this.isBookmarkPending()) {
|
||||
this.setBookmark();
|
||||
}
|
||||
|
||||
this.trimBuffer();
|
||||
|
||||
pageAdded = true;
|
||||
} else {
|
||||
reference.cache[index].events.push(event);
|
||||
}
|
||||
|
||||
this.buffer.count++;
|
||||
this.result.count++;
|
||||
|
||||
return pageAdded;
|
||||
};
|
||||
|
||||
this.trimBuffer = () => {
|
||||
const reference = this.getReference();
|
||||
const diff = reference.cache.length - this.page.limit;
|
||||
|
||||
if (diff <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < diff; i++) {
|
||||
if (reference.cache[i].events) {
|
||||
this.buffer.count -= reference.cache[i].events.length;
|
||||
reference.cache[i].events.splice(0, reference.cache[i].events.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.isBufferFull = () => {
|
||||
if (this.buffer.count === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.emptyBuffer = () => {
|
||||
const reference = this.getReference();
|
||||
let data = [];
|
||||
|
||||
for (let i = 0; i < reference.cache.length; i++) {
|
||||
const count = reference.cache[i].events.length;
|
||||
|
||||
if (count > 0) {
|
||||
this.buffer.count -= count;
|
||||
data = data.concat(reference.cache[i].events.splice(0, count));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.emptyCache = number => {
|
||||
const reference = this.getActiveReference();
|
||||
|
||||
number = number || reference.state.current;
|
||||
|
||||
reference.state.first = number;
|
||||
reference.state.current = number;
|
||||
reference.state.last = number;
|
||||
|
||||
reference.cache.splice(0, reference.cache.length);
|
||||
};
|
||||
|
||||
this.isOverCapacity = () => {
|
||||
const reference = this.getActiveReference();
|
||||
|
||||
return (reference.cache.length - this.page.limit) > 0;
|
||||
};
|
||||
|
||||
this.trim = left => {
|
||||
const reference = this.getActiveReference();
|
||||
const excess = reference.cache.length - this.page.limit;
|
||||
|
||||
let ejected;
|
||||
|
||||
if (left) {
|
||||
ejected = reference.cache.splice(0, excess);
|
||||
reference.state.first = reference.cache[0].number;
|
||||
} else {
|
||||
ejected = reference.cache.splice(-excess);
|
||||
reference.state.last = reference.cache[reference.cache.length - 1].number;
|
||||
}
|
||||
|
||||
return ejected.reduce((total, page) => total + page.lines, 0);
|
||||
};
|
||||
|
||||
this.isPageBookmarked = number => number >= this.page.bookmark.first &&
|
||||
number <= this.page.bookmark.last;
|
||||
|
||||
this.updateLineCount = (lines, engine) => {
|
||||
let reference;
|
||||
|
||||
if (engine) {
|
||||
reference = this.getReference();
|
||||
} else {
|
||||
reference = this.getActiveReference();
|
||||
}
|
||||
|
||||
const index = reference.cache.findIndex(item => item.number === reference.state.current);
|
||||
|
||||
reference.cache[index].lines += lines;
|
||||
};
|
||||
|
||||
this.isBookmarkPending = () => this.bookmark.pending;
|
||||
this.isBookmarkSet = () => this.bookmark.set;
|
||||
|
||||
this.setBookmark = () => {
|
||||
if (this.isBookmarkSet()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isBookmarkPending()) {
|
||||
this.bookmark.pending = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.bookmark.state.first = this.page.state.first;
|
||||
this.bookmark.state.last = this.page.state.last - 1;
|
||||
this.bookmark.state.current = this.page.state.current - 1;
|
||||
this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache));
|
||||
this.bookmark.set = true;
|
||||
this.bookmark.pending = false;
|
||||
};
|
||||
|
||||
this.removeBookmark = () => {
|
||||
this.bookmark.set = false;
|
||||
this.bookmark.pending = false;
|
||||
this.bookmark.cache.splice(0, this.bookmark.cache.length);
|
||||
this.bookmark.state.first = 0;
|
||||
this.bookmark.state.last = 0;
|
||||
this.bookmark.state.current = 0;
|
||||
};
|
||||
|
||||
this.next = () => {
|
||||
const reference = this.getActiveReference();
|
||||
const number = reference.state.last + 1;
|
||||
|
||||
return this.api.getPage(number)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.addPage(data.page, [], true);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.previous = () => {
|
||||
const reference = this.getActiveReference();
|
||||
|
||||
return this.api.getPage(reference.state.first - 1)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.addPage(data.page, [], false);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.last = () => this.api.last()
|
||||
.then(data => {
|
||||
if (!data || !data.results || !data.results.length > 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.emptyCache(data.page);
|
||||
this.addPage(data.page, [], true);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
|
||||
this.first = () => this.api.first()
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.emptyCache(data.page);
|
||||
this.addPage(data.page, [], false);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
|
||||
this.getActiveReference = () => (this.isBookmarkSet() ?
|
||||
this.getReference(true) : this.getReference());
|
||||
|
||||
this.getReference = (bookmark) => {
|
||||
if (bookmark) {
|
||||
return {
|
||||
bookmark: true,
|
||||
cache: this.bookmark.cache,
|
||||
state: this.bookmark.state
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bookmark: false,
|
||||
cache: this.page.cache,
|
||||
state: this.page.state
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
JobPageService.$inject = ['$q'];
|
||||
|
||||
export default JobPageService;
|
||||
@ -30,11 +30,13 @@ const re = new RegExp(pattern);
|
||||
const hasAnsi = input => re.test(input);
|
||||
|
||||
function JobRenderService ($q, $sce, $window) {
|
||||
this.init = ({ compile, isStreamActive }) => {
|
||||
this.init = ({ compile }) => {
|
||||
this.parent = null;
|
||||
this.record = {};
|
||||
this.el = $(ELEMENT_TBODY);
|
||||
this.hooks = { isStreamActive, compile };
|
||||
this.hooks = { compile };
|
||||
|
||||
this.createToggles = false;
|
||||
};
|
||||
|
||||
this.sortByLineNumber = (a, b) => {
|
||||
@ -55,12 +57,11 @@ function JobRenderService ($q, $sce, $window) {
|
||||
|
||||
events.sort(this.sortByLineNumber);
|
||||
|
||||
events.forEach(event => {
|
||||
const line = this.transformEvent(event);
|
||||
|
||||
for (let i = 0; i < events.length; ++i) {
|
||||
const line = this.transformEvent(events[i]);
|
||||
html += line.html;
|
||||
lines += line.count;
|
||||
});
|
||||
}
|
||||
|
||||
return { html, lines };
|
||||
};
|
||||
@ -177,13 +178,13 @@ function JobRenderService ($q, $sce, $window) {
|
||||
}
|
||||
|
||||
if (current) {
|
||||
if (!this.hooks.isStreamActive() && current.isParent && current.line === ln) {
|
||||
if (this.createToggles && 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" ui-sref="output.host-event.json({eventId: ${current.id}, taskUuid: '${current.uuid}' })"><span ng-non-bindable>${content}</span></td>`;
|
||||
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}', '${current.uuid}')">${content}</td>`;
|
||||
}
|
||||
|
||||
if (current.time && current.line === ln) {
|
||||
@ -239,18 +240,7 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return list;
|
||||
};
|
||||
|
||||
this.insert = (events, insert) => {
|
||||
const result = this.transformEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
return this.requestAnimationFrame(() => insert(html))
|
||||
.then(() => this.compile(html))
|
||||
.then(() => result.lines);
|
||||
};
|
||||
|
||||
this.remove = elements => this.requestAnimationFrame(() => {
|
||||
elements.remove();
|
||||
});
|
||||
this.remove = elements => this.requestAnimationFrame(() => elements.remove());
|
||||
|
||||
this.requestAnimationFrame = fn => $q(resolve => {
|
||||
$window.requestAnimationFrame(() => {
|
||||
@ -262,9 +252,8 @@ function JobRenderService ($q, $sce, $window) {
|
||||
});
|
||||
});
|
||||
|
||||
this.compile = html => {
|
||||
html = $(this.el);
|
||||
this.hooks.compile(html);
|
||||
this.compile = content => {
|
||||
this.hooks.compile(content);
|
||||
|
||||
return this.requestAnimationFrame();
|
||||
};
|
||||
@ -286,9 +275,35 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.prepend = events => this.insert(events, html => this.el.prepend(html));
|
||||
this.prepend = events => {
|
||||
if (events.length < 1) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.append = events => this.insert(events, html => this.el.append(html));
|
||||
const result = this.transformEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
const newElements = angular.element(html);
|
||||
|
||||
return this.requestAnimationFrame(() => this.el.prepend(newElements))
|
||||
.then(() => this.compile(newElements))
|
||||
.then(() => result.lines);
|
||||
};
|
||||
|
||||
this.append = events => {
|
||||
if (events.length < 1) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const result = this.transformEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
const newElements = angular.element(html);
|
||||
|
||||
return this.requestAnimationFrame(() => this.el.append(newElements))
|
||||
.then(() => this.compile(newElements))
|
||||
.then(() => result.lines);
|
||||
};
|
||||
|
||||
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ const DELAY = 100;
|
||||
const THRESHOLD = 0.1;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = (hooks) => {
|
||||
this.init = ({ next, previous }) => {
|
||||
this.el = $(ELEMENT_CONTAINER);
|
||||
this.timer = null;
|
||||
|
||||
@ -14,15 +14,15 @@ function JobScrollService ($q, $timeout) {
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
isAtRest: hooks.isAtRest,
|
||||
next: hooks.next,
|
||||
previous: hooks.previous
|
||||
next,
|
||||
previous,
|
||||
isAtRest: () => $q.resolve()
|
||||
};
|
||||
|
||||
this.state = {
|
||||
locked: false,
|
||||
hidden: false,
|
||||
paused: false,
|
||||
top: true
|
||||
top: true,
|
||||
};
|
||||
|
||||
this.el.scroll(this.listen);
|
||||
@ -158,6 +158,20 @@ function JobScrollService ($q, $timeout) {
|
||||
this.state.locked = false;
|
||||
};
|
||||
|
||||
this.hide = () => {
|
||||
if (!this.state.hidden) {
|
||||
this.el.css('overflow', 'hidden');
|
||||
this.state.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.unhide = () => {
|
||||
if (this.state.hidden) {
|
||||
this.el.css('overflow', 'auto');
|
||||
this.state.hidden = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.isLocked = () => this.state.locked;
|
||||
this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ JobSearchController.$inject = [
|
||||
'$state',
|
||||
'QuerySet',
|
||||
'OutputStrings',
|
||||
'JobStatusService',
|
||||
'OutputStatusService',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,39 +1,37 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<!-- todo: markup, styling, css etc. - disposition according to project lib conventions -->
|
||||
<!-- todo: further componentization -->
|
||||
<form ng-submit="vm.submitSearch()">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': vm.rejected }"
|
||||
ng-model="vm.value"
|
||||
ng-attr-placeholder="{{ vm.running ? vm.strings.get('search.PLACEHOLDER_RUNNING') :
|
||||
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}"
|
||||
ng-disabled="vm.disabled">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control at-Input"
|
||||
ng-disabled="vm.disabled"
|
||||
ng-class="{ 'at-Input--rejected': vm.rejected }"
|
||||
ng-model="vm.value"
|
||||
ng-attr-placeholder="{{ vm.running ?
|
||||
vm.strings.get('search.PLACEHOLDER_RUNNING') :
|
||||
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
ng-click="vm.submitSearch()"
|
||||
ng-disabled="vm.disabled"
|
||||
type="button">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button class="btn jobz-Button-searchKey"
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button class="btn jobz-Button-searchKey"
|
||||
ng-if="vm.key"
|
||||
ng-disabled="vm.disabled"
|
||||
ng-click="vm.toggleSearchKey()"
|
||||
type="button">
|
||||
{{:: vm.strings.get('search.KEY') }}
|
||||
</button>
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
type="button"> {{:: vm.strings.get('search.KEY') }}
|
||||
</button>
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
ng-if="!vm.key"
|
||||
ng-disabled="vm.disabled"
|
||||
ng-click="vm.toggleSearchKey()"
|
||||
type="button">
|
||||
{{:: vm.strings.get('search.KEY') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<p ng-if="vm.rejected" class="at-InputMessage--rejected">
|
||||
{{ vm.message }}
|
||||
</p>
|
||||
type="button"> {{:: vm.strings.get('search.KEY') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<p ng-if="vm.rejected" class="at-InputMessage--rejected">{{ vm.message }}</p>
|
||||
</form>
|
||||
|
||||
<div class="jobz-tagz">
|
||||
@ -41,19 +39,25 @@
|
||||
<at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag>
|
||||
</div>
|
||||
<div class="jobz-searchClearAllContainer">
|
||||
<a href class="jobz-searchClearAll" ng-click="vm.clearSearch()" ng-show="!(vm.tags | isEmpty)">{{:: vm.strings.get('search.CLEAR_ALL') }}</a>
|
||||
<a class="jobz-searchClearAll"
|
||||
ng-click="vm.clearSearch()"
|
||||
ng-show="!(vm.tags | isEmpty)"
|
||||
href> {{:: vm.strings.get('search.CLEAR_ALL') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jobz-searchKeyPaneContainer" ng-show="vm.key">
|
||||
<div class="jobz-searchKeyPane">
|
||||
<div class="SmartSearch-keyRow">
|
||||
<div class="SmartSearch-examples">
|
||||
<div class="SmartSearch-examples--title">
|
||||
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
|
||||
<div class="SmartSearch-examples">
|
||||
<div class="SmartSearch-examples--title">
|
||||
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
|
||||
</div>
|
||||
<div class="SmartSearch-examples--search"
|
||||
ng-repeat="tag in vm.examples">{{ tag }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="SmartSearch-examples--search" ng-repeat="tag in vm.examples">{{ tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="SmartSearch-keyRow">
|
||||
<b>{{:: vm.strings.get('search.FIELDS') }}:</b>
|
||||
|
||||
322
awx/ui/client/features/output/slide.service.js
Normal file
322
awx/ui/client/features/output/slide.service.js
Normal file
@ -0,0 +1,322 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const PAGE_SIZE = 50;
|
||||
const PAGE_LIMIT = 5;
|
||||
const EVENT_LIMIT = PAGE_LIMIT * PAGE_SIZE;
|
||||
|
||||
/**
|
||||
* Check if a range overlaps another range
|
||||
*
|
||||
* @arg {Array} range - A [low, high] range array.
|
||||
* @arg {Array} other - A [low, high] range array to be compared with the first.
|
||||
*
|
||||
* @returns {Boolean} - Indicating that the ranges overlap.
|
||||
*/
|
||||
function checkRangeOverlap (range, other) {
|
||||
const span = Math.max(range[1], other[1]) - Math.min(range[0], other[0]);
|
||||
|
||||
return (range[1] - range[0]) + (other[1] - other[0]) >= span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array that describes the overlap of two ranges.
|
||||
*
|
||||
* @arg {Array} range - A [low, high] range array.
|
||||
* @arg {Array} other - A [low, high] range array to be compared with the first.
|
||||
*
|
||||
* @returns {(Array|Boolean)} - Returns false if the ranges aren't overlapping.
|
||||
* For overlapping ranges, a length-2 array describing the nature of the overlap
|
||||
* is returned. The overlap array describes the position of the second range in
|
||||
* terms of how many steps inward (negative) or outward (positive) its sides are
|
||||
* relative to the first range.
|
||||
*
|
||||
* ++45678
|
||||
* 234---- => getOverlapArray([4, 8], [2, 4]) = [2, -4]
|
||||
*
|
||||
* 45678
|
||||
* 45--- => getOverlapArray([4, 8], [4, 5]) = [0, -3]
|
||||
*
|
||||
* 45678
|
||||
* -56-- => getOverlapArray([4, 8], [5, 6]) = [-1, -2]
|
||||
*
|
||||
* 45678
|
||||
* --678 => getOverlapArray([4, 8], [6, 8]) = [-2, 0]
|
||||
*
|
||||
* 456++
|
||||
* --678 => getOverlapArray([4, 6], [6, 8]) = [-2, 2]
|
||||
*
|
||||
* +++456++
|
||||
* 12345678 => getOverlapArray([4, 6], [1, 8]) = [3, 2]
|
||||
^
|
||||
* 12345678
|
||||
* ---456-- => getOverlapArray([1, 8], [4, 6]) = [-3, -2]
|
||||
*/
|
||||
function getOverlapArray (range, other) {
|
||||
if (!checkRangeOverlap(range, other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [range[0] - other[0], other[1] - range[1]];
|
||||
}
|
||||
|
||||
function SlidingWindowService ($q) {
|
||||
this.init = (storage, api) => {
|
||||
const { prepend, append, shift, pop } = storage;
|
||||
const { getMaxCounter, getRange, getFirst, getLast } = api;
|
||||
|
||||
this.api = {
|
||||
getMaxCounter,
|
||||
getRange,
|
||||
getFirst,
|
||||
getLast
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
prepend,
|
||||
append,
|
||||
shift,
|
||||
pop
|
||||
};
|
||||
|
||||
this.records = {};
|
||||
this.chain = $q.resolve();
|
||||
};
|
||||
|
||||
this.pushFront = events => {
|
||||
const tail = this.getTailCounter();
|
||||
const newEvents = events.filter(({ counter }) => counter > tail);
|
||||
|
||||
return this.storage.append(newEvents)
|
||||
.then(() => {
|
||||
newEvents.forEach(({ counter, start_line, end_line }) => {
|
||||
this.records[counter] = { start_line, end_line };
|
||||
});
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.pushBack = events => {
|
||||
const [head, tail] = this.getRange();
|
||||
const newEvents = events
|
||||
.filter(({ counter }) => counter < head || counter > tail);
|
||||
|
||||
return this.storage.prepend(newEvents)
|
||||
.then(() => {
|
||||
newEvents.forEach(({ counter, start_line, end_line }) => {
|
||||
this.records[counter] = { start_line, end_line };
|
||||
});
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.popFront = count => {
|
||||
if (!count || count <= 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const max = this.getTailCounter();
|
||||
const min = max - count;
|
||||
|
||||
let lines = 0;
|
||||
|
||||
for (let i = max; i >= min; --i) {
|
||||
if (this.records[i]) {
|
||||
lines += (this.records[i].end_line - this.records[i].start_line);
|
||||
}
|
||||
}
|
||||
|
||||
return this.storage.pop(lines)
|
||||
.then(() => {
|
||||
for (let i = max; i >= min; --i) {
|
||||
delete this.records[i];
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.popBack = count => {
|
||||
if (!count || count <= 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const min = this.getHeadCounter();
|
||||
const max = min + count;
|
||||
|
||||
let lines = 0;
|
||||
|
||||
for (let i = min; i <= min + count; ++i) {
|
||||
if (this.records[i]) {
|
||||
lines += (this.records[i].end_line - this.records[i].start_line);
|
||||
}
|
||||
}
|
||||
|
||||
return this.storage.shift(lines)
|
||||
.then(() => {
|
||||
for (let i = min; i <= max; ++i) {
|
||||
delete this.records[i];
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.getBoundedRange = ([low, high]) => {
|
||||
const bounds = [1, this.getMaxCounter()];
|
||||
|
||||
return [Math.max(low, bounds[0]), Math.min(high, bounds[1])];
|
||||
};
|
||||
|
||||
this.move = ([low, high]) => {
|
||||
const [head, tail] = this.getRange();
|
||||
const [newHead, newTail] = this.getBoundedRange([low, high]);
|
||||
|
||||
if (newHead > newTail) {
|
||||
return $q.resolve([0, 0]);
|
||||
}
|
||||
|
||||
if (!Number.isFinite(newHead) || !Number.isFinite(newTail)) {
|
||||
return $q.resolve([0, 0]);
|
||||
}
|
||||
|
||||
const overlap = getOverlapArray([head, tail], [newHead, newTail]);
|
||||
|
||||
if (!overlap) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.popBack(this.getRecordCount()))
|
||||
.then(() => this.api.getRange([newHead, newTail]))
|
||||
.then(events => this.pushFront(events));
|
||||
}
|
||||
|
||||
if (overlap && overlap[0] > 0) {
|
||||
const pushBackRange = [head - overlap[0], head];
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getRange(pushBackRange))
|
||||
.then(events => this.pushBack(events));
|
||||
}
|
||||
|
||||
if (overlap && overlap[1] > 0) {
|
||||
const pushFrontRange = [tail, tail + overlap[1]];
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getRange(pushFrontRange))
|
||||
.then(events => this.pushFront(events));
|
||||
}
|
||||
|
||||
if (overlap && overlap[0] < 0) {
|
||||
this.chain = this.chain.then(() => this.popBack(Math.abs(overlap[0])));
|
||||
}
|
||||
|
||||
if (overlap && overlap[1] < 0) {
|
||||
this.chain = this.chain.then(() => this.popFront(Math.abs(overlap[1])));
|
||||
}
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
const range = this.getRange();
|
||||
const displacement = [range[0] - head, range[1] - tail];
|
||||
|
||||
return $q.resolve(displacement);
|
||||
});
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.slideDown = (displacement = PAGE_SIZE) => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const tailRoom = this.getMaxCounter() - tail;
|
||||
const tailDisplacement = Math.min(tailRoom, displacement);
|
||||
|
||||
const newTail = tail + tailDisplacement;
|
||||
|
||||
let headDisplacement = 0;
|
||||
|
||||
if (newTail - head > EVENT_LIMIT) {
|
||||
headDisplacement = (newTail - EVENT_LIMIT) - head;
|
||||
}
|
||||
|
||||
return this.move([head + headDisplacement, tail + tailDisplacement]);
|
||||
};
|
||||
|
||||
this.slideUp = (displacement = PAGE_SIZE) => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const headRoom = head - 1;
|
||||
const headDisplacement = Math.min(headRoom, displacement);
|
||||
|
||||
const newHead = head - headDisplacement;
|
||||
|
||||
let tailDisplacement = 0;
|
||||
|
||||
if (tail - newHead > EVENT_LIMIT) {
|
||||
tailDisplacement = tail - (newHead + EVENT_LIMIT);
|
||||
}
|
||||
|
||||
return this.move([newHead, tail - tailDisplacement]);
|
||||
};
|
||||
|
||||
this.moveHead = displacement => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const headRoom = head - 1;
|
||||
const headDisplacement = Math.min(headRoom, displacement);
|
||||
|
||||
return this.move([head + headDisplacement, tail]);
|
||||
};
|
||||
|
||||
this.moveTail = displacement => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const tailRoom = this.getMaxCounter() - tail;
|
||||
const tailDisplacement = Math.max(tailRoom, displacement);
|
||||
|
||||
return this.move([head, tail + tailDisplacement]);
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const count = this.getRecordCount();
|
||||
|
||||
if (count > 0) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.popBack(count));
|
||||
}
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.getFirst = () => this.clear()
|
||||
.then(() => this.api.getFirst())
|
||||
.then(events => this.pushFront(events))
|
||||
.then(() => this.moveTail(PAGE_SIZE));
|
||||
|
||||
this.getLast = () => this.clear()
|
||||
.then(() => this.api.getLast())
|
||||
.then(events => this.pushBack(events))
|
||||
.then(() => this.moveHead(-PAGE_SIZE));
|
||||
|
||||
this.getTailCounter = () => {
|
||||
const tail = Math.max(...Object.keys(this.records));
|
||||
|
||||
return Number.isFinite(tail) ? tail : 0;
|
||||
};
|
||||
|
||||
this.getHeadCounter = () => {
|
||||
const head = Math.min(...Object.keys(this.records));
|
||||
|
||||
return Number.isFinite(head) ? head : 0;
|
||||
};
|
||||
|
||||
this.compareRange = (a, b) => a[0] === b[0] && a[1] === b[1];
|
||||
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
|
||||
|
||||
this.getMaxCounter = () => this.api.getMaxCounter();
|
||||
this.getRecordCount = () => Object.keys(this.records).length;
|
||||
this.getCapacity = () => EVENT_LIMIT - this.getRecordCount();
|
||||
}
|
||||
|
||||
SlidingWindowService.$inject = ['$q'];
|
||||
|
||||
export default SlidingWindowService;
|
||||
@ -63,7 +63,7 @@ function JobStatsController (strings, { subscribe }) {
|
||||
|
||||
JobStatsController.$inject = [
|
||||
'OutputStrings',
|
||||
'JobStatusService',
|
||||
'OutputStatusService',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<!-- todo: styling, markup, css etc. - disposition according to project lib conventions -->
|
||||
<div class="at-u-floatRight">
|
||||
<span class="at-Panel-label">plays</span>
|
||||
<span ng-show="!vm.plays" class="at-Panel-headingTitleBadge">...</span>
|
||||
|
||||
@ -12,9 +12,7 @@ function JobStatusService (moment, message) {
|
||||
this.dispatch = () => message.dispatch('status', this.state);
|
||||
this.subscribe = listener => message.subscribe('status', listener);
|
||||
|
||||
this.init = ({ resource }) => {
|
||||
const { model } = resource;
|
||||
|
||||
this.init = ({ model, stats }) => {
|
||||
this.created = model.get('created');
|
||||
this.job = model.get('id');
|
||||
this.jobType = model.get('type');
|
||||
@ -43,7 +41,7 @@ function JobStatusService (moment, message) {
|
||||
},
|
||||
};
|
||||
|
||||
this.setStatsEvent(resource.stats);
|
||||
this.setStatsEvent(stats);
|
||||
this.updateStats();
|
||||
this.updateRunningState();
|
||||
|
||||
@ -213,7 +211,7 @@ function JobStatusService (moment, message) {
|
||||
|
||||
JobStatusService.$inject = [
|
||||
'moment',
|
||||
'JobMessageService',
|
||||
'OutputMessageService',
|
||||
];
|
||||
|
||||
export default JobStatusService;
|
||||
|
||||
146
awx/ui/client/features/output/stream.service.js
Normal file
146
awx/ui/client/features/output/stream.service.js
Normal file
@ -0,0 +1,146 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const PAGE_SIZE = 50;
|
||||
const MAX_LAG = 120;
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
|
||||
function OutputStream ($q) {
|
||||
this.init = ({ bufferAdd, bufferEmpty, onFrames, onStop }) => {
|
||||
this.hooks = {
|
||||
bufferAdd,
|
||||
bufferEmpty,
|
||||
onFrames,
|
||||
onStop,
|
||||
};
|
||||
|
||||
this.counters = {
|
||||
used: [],
|
||||
min: 1,
|
||||
max: 0,
|
||||
ready: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
ending: false,
|
||||
ended: false
|
||||
};
|
||||
|
||||
this.lag = 0;
|
||||
this.chain = $q.resolve();
|
||||
|
||||
this.factors = this.calcFactors(PAGE_SIZE);
|
||||
this.setFramesPerRender();
|
||||
};
|
||||
|
||||
this.calcFactors = size => {
|
||||
const factors = [1];
|
||||
|
||||
for (let i = 2; i <= size / 2; i++) {
|
||||
if (size % i === 0) {
|
||||
factors.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
factors.push(size);
|
||||
|
||||
return factors;
|
||||
};
|
||||
|
||||
this.setFramesPerRender = () => {
|
||||
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
|
||||
const boundedIndex = Math.min(this.factors.length - 1, index);
|
||||
|
||||
this.framesPerRender = this.factors[boundedIndex];
|
||||
};
|
||||
|
||||
this.setMissingCounterThreshold = counter => {
|
||||
if (counter > this.counters.min) {
|
||||
this.counters.min = counter;
|
||||
}
|
||||
};
|
||||
|
||||
this.updateCounterState = ({ counter }) => {
|
||||
this.counters.used.push(counter);
|
||||
|
||||
if (counter > this.counters.max) {
|
||||
this.counters.max = counter;
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
const ready = [];
|
||||
|
||||
for (let i = this.counters.min; i < this.counters.max; i++) {
|
||||
if (this.counters.used.indexOf(i) === -1) {
|
||||
missing.push(i);
|
||||
} else if (missing.length === 0) {
|
||||
ready.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length === 0) {
|
||||
this.counters.ready = true;
|
||||
this.counters.min = this.counters.max + 1;
|
||||
this.counters.used = [];
|
||||
} else {
|
||||
this.counters.ready = false;
|
||||
}
|
||||
|
||||
this.counters.missing = missing;
|
||||
this.counters.readyLines = ready;
|
||||
|
||||
return this.counters.ready;
|
||||
};
|
||||
|
||||
this.pushJobEvent = data => {
|
||||
this.lag++;
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
if (data.event === JOB_END) {
|
||||
this.state.ending = true;
|
||||
}
|
||||
|
||||
const isMissingCounters = !this.updateCounterState(data);
|
||||
const [length, count] = this.hooks.bufferAdd(data);
|
||||
|
||||
if (count % PAGE_SIZE === 0) {
|
||||
this.setFramesPerRender();
|
||||
}
|
||||
|
||||
const isBatchReady = length % this.framesPerRender === 0;
|
||||
const isReady = this.state.ended || (!isMissingCounters && isBatchReady);
|
||||
|
||||
if (!isReady) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const events = this.hooks.bufferEmpty();
|
||||
|
||||
return this.emitFrames(events);
|
||||
})
|
||||
.then(() => --this.lag);
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.emitFrames = events => this.hooks.onFrames(events)
|
||||
.then(() => {
|
||||
if (this.state.ending) {
|
||||
const lastEvents = this.hooks.bufferEmpty();
|
||||
|
||||
if (lastEvents.length) {
|
||||
return this.emitFrames(lastEvents);
|
||||
}
|
||||
|
||||
this.state.ending = false;
|
||||
this.state.ended = true;
|
||||
|
||||
this.hooks.onStop();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
OutputStream.$inject = ['$q'];
|
||||
|
||||
export default OutputStream;
|
||||
Loading…
x
Reference in New Issue
Block a user