mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 05:29:26 -02:30
use a sliding window over counter intervals
This commit is contained in:
@@ -1,144 +1,110 @@
|
|||||||
const PAGE_LIMIT = 5;
|
const API_PAGE_SIZE = 200;
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
const ORDER_BY = 'counter';
|
||||||
|
|
||||||
const BASE_PARAMS = {
|
const BASE_PARAMS = {
|
||||||
order_by: 'start_line',
|
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
|
order_by: ORDER_BY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const merge = (...objs) => _.merge({}, ...objs);
|
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) {
|
function JobEventsApiService ($http, $q) {
|
||||||
this.init = (endpoint, params) => {
|
this.init = (endpoint, params) => {
|
||||||
this.keys = [];
|
|
||||||
this.cache = {};
|
|
||||||
this.pageSizes = {};
|
|
||||||
this.endpoint = endpoint;
|
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.cache = {};
|
|
||||||
this.keys = [];
|
|
||||||
this.pageSizes = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetch = () => this.first().then(() => this);
|
|
||||||
|
|
||||||
this.getPage = number => {
|
this.getPage = number => {
|
||||||
if (number < 1 || number > this.state.last) {
|
if (number === 1) return this.getFirst();
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cache[number]) {
|
const [low, high] = [1 + PAGE_SIZE * (number - 1), PAGE_SIZE * number];
|
||||||
if (this.pageSizes[number] === PAGE_SIZE) {
|
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
|
||||||
return this.cache[number];
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.pageSizes[number];
|
return $http.get(this.endpoint, { params })
|
||||||
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 })
|
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const { results, count } = data;
|
const { results } = data;
|
||||||
|
|
||||||
this.state.results = results;
|
this.state.current = number;
|
||||||
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.pageSizes[number] = results.length;
|
return results;
|
||||||
|
|
||||||
return { results, page: number };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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.getFirst = () => {
|
||||||
this.next = () => this.getPage(this.state.next);
|
const page = 1;
|
||||||
this.previous = () => this.getPage(this.state.previous);
|
const params = merge(this.params, { page });
|
||||||
|
|
||||||
this.last = () => {
|
return $http.get(this.endpoint, { params })
|
||||||
const params = merge({}, this.state.params);
|
|
||||||
|
|
||||||
delete params.page;
|
|
||||||
delete params.order_by;
|
|
||||||
|
|
||||||
params.page = 1;
|
|
||||||
params.order_by = '-start_line';
|
|
||||||
|
|
||||||
const promise = $http.get(this.endpoint, { params })
|
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const { results, count } = data;
|
const { results, count } = data;
|
||||||
const lastPage = this.getLastPage(count);
|
|
||||||
|
this.state.count = count;
|
||||||
|
this.state.current = page;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getRange = range => {
|
||||||
|
if (!range) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [low, high] = range;
|
||||||
|
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
|
||||||
|
|
||||||
|
params.page_size = API_PAGE_SIZE;
|
||||||
|
|
||||||
|
return $http.get(this.endpoint, { params })
|
||||||
|
.then(({ data }) => {
|
||||||
|
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) {
|
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.count = count;
|
||||||
this.state.page = lastPage;
|
this.state.current = Math.ceil(count / PAGE_SIZE);
|
||||||
this.state.next = lastPage;
|
|
||||||
this.state.last = lastPage;
|
|
||||||
this.state.previous = Math.max(1, this.state.page - 1);
|
|
||||||
|
|
||||||
this.clearCache();
|
return rotated;
|
||||||
|
|
||||||
return { results, page: lastPage };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 = [
|
JobEventsApiService.$inject = ['$http', '$q'];
|
||||||
'$http',
|
|
||||||
'$q'
|
|
||||||
];
|
|
||||||
|
|
||||||
export default JobEventsApiService;
|
export default JobEventsApiService;
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ JobDetailsController.$inject = [
|
|||||||
'OutputStrings',
|
'OutputStrings',
|
||||||
'Wait',
|
'Wait',
|
||||||
'ParseVariableString',
|
'ParseVariableString',
|
||||||
'JobStatusService',
|
'OutputStatusService',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||||
|
<!-- todo: further componentization -->
|
||||||
<div class="JobResults-panelHeader">
|
<div class="JobResults-panelHeader">
|
||||||
<div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div>
|
<div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div>
|
||||||
<!-- LEFT PANE HEADER ACTIONS -->
|
<!-- 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,129 +1,156 @@
|
|||||||
|
/* eslint camelcase: 0 */
|
||||||
let $compile;
|
let $compile;
|
||||||
let $filter;
|
|
||||||
let $q;
|
let $q;
|
||||||
let $scope;
|
let $scope;
|
||||||
let $state;
|
let $state;
|
||||||
|
|
||||||
let page;
|
|
||||||
let render;
|
|
||||||
let resource;
|
let resource;
|
||||||
|
let render;
|
||||||
let scroll;
|
let scroll;
|
||||||
let engine;
|
|
||||||
let status;
|
let status;
|
||||||
|
let slide;
|
||||||
|
let stream;
|
||||||
|
|
||||||
let vm;
|
let vm;
|
||||||
let streaming;
|
|
||||||
let listeners = [];
|
|
||||||
|
|
||||||
function JobsIndexController (
|
const bufferState = [0, 0]; // [length, count]
|
||||||
_$compile_,
|
const listeners = [];
|
||||||
_$filter_,
|
const rx = [];
|
||||||
_$q_,
|
|
||||||
_$scope_,
|
|
||||||
_$state_,
|
|
||||||
_resource_,
|
|
||||||
_page_,
|
|
||||||
_scroll_,
|
|
||||||
_render_,
|
|
||||||
_engine_,
|
|
||||||
_status_,
|
|
||||||
_strings_,
|
|
||||||
) {
|
|
||||||
vm = this || {};
|
|
||||||
|
|
||||||
$compile = _$compile_;
|
let following = false;
|
||||||
$filter = _$filter_;
|
|
||||||
$q = _$q_;
|
|
||||||
$scope = _$scope_;
|
|
||||||
$state = _$state_;
|
|
||||||
|
|
||||||
resource = _resource_;
|
function bufferInit () {
|
||||||
page = _page_;
|
rx.length = 0;
|
||||||
scroll = _scroll_;
|
|
||||||
render = _render_;
|
|
||||||
engine = _engine_;
|
|
||||||
status = _status_;
|
|
||||||
|
|
||||||
vm.strings = _strings_;
|
bufferState[0] = 0;
|
||||||
|
bufferState[1] = 0;
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function init () {
|
function bufferAdd (event) {
|
||||||
status.init({
|
rx.push(event);
|
||||||
resource,
|
|
||||||
});
|
|
||||||
|
|
||||||
page.init({
|
bufferState[0] += 1;
|
||||||
resource,
|
bufferState[1] += 1;
|
||||||
});
|
|
||||||
|
|
||||||
render.init({
|
return bufferState;
|
||||||
compile: html => $compile(html)($scope),
|
}
|
||||||
isStreamActive: engine.isActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
scroll.init({
|
function bufferEmpty () {
|
||||||
isAtRest: scrollIsAtRest,
|
bufferState[0] = 0;
|
||||||
previous,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.init({
|
return rx.splice(0, rx.length);
|
||||||
page,
|
}
|
||||||
scroll,
|
|
||||||
resource,
|
function onFrames (events) {
|
||||||
onEventFrame (events) {
|
if (!following) {
|
||||||
return shift().then(() => append(events, true));
|
const minCounter = Math.min(...events.map(({ counter }) => counter));
|
||||||
},
|
// attachment range
|
||||||
onStart () {
|
const max = slide.getTailCounter() + 1;
|
||||||
status.setJobStatus('running');
|
const min = Math.max(1, slide.getHeadCounter(), max - 50);
|
||||||
},
|
|
||||||
onStop () {
|
if (minCounter > max || minCounter < min) {
|
||||||
stopListening();
|
return $q.resolve();
|
||||||
status.updateStats();
|
|
||||||
status.dispatch();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
streaming = false;
|
follow();
|
||||||
|
|
||||||
if (status.state.running) {
|
|
||||||
return scrollLast().then(() => startListening());
|
|
||||||
} else if (!status.state.finished) {
|
|
||||||
return scrollFirst().then(() => startListening());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () {
|
function stopListening () {
|
||||||
listeners.forEach(deregister => deregister());
|
listeners.forEach(deregister => deregister());
|
||||||
listeners = [];
|
listeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListening () {
|
function startListening () {
|
||||||
stopListening();
|
stopListening();
|
||||||
|
|
||||||
listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
|
listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
|
||||||
listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));
|
listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));
|
||||||
}
|
}
|
||||||
@@ -133,271 +160,95 @@ function handleStatusEvent (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleJobEvent (data) {
|
function handleJobEvent (data) {
|
||||||
streaming = streaming || attachToRunningJob();
|
stream.pushJobEvent(data);
|
||||||
|
status.pushJobEvent(data);
|
||||||
|
}
|
||||||
|
|
||||||
streaming.then(() => {
|
function OutputIndexController (
|
||||||
engine.pushJobEvent(data);
|
_$compile_,
|
||||||
status.pushJobEvent(data);
|
_$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 () {
|
OutputIndexController.$inject = [
|
||||||
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, uuid) {
|
|
||||||
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = [
|
|
||||||
'$compile',
|
'$compile',
|
||||||
'$filter',
|
|
||||||
'$q',
|
'$q',
|
||||||
'$scope',
|
'$scope',
|
||||||
'$state',
|
'$state',
|
||||||
'resource',
|
'resource',
|
||||||
'JobPageService',
|
'OutputScrollService',
|
||||||
'JobScrollService',
|
'OutputRenderService',
|
||||||
'JobRenderService',
|
'OutputStatusService',
|
||||||
'JobEventEngine',
|
'OutputSlideService',
|
||||||
'JobStatusService',
|
'OutputStreamService',
|
||||||
|
'$filter',
|
||||||
'OutputStrings',
|
'OutputStrings',
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = JobsIndexController;
|
module.exports = OutputIndexController;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import atLibComponents from '~components';
|
|||||||
|
|
||||||
import Strings from '~features/output/output.strings';
|
import Strings from '~features/output/output.strings';
|
||||||
import Controller from '~features/output/index.controller';
|
import Controller from '~features/output/index.controller';
|
||||||
import PageService from '~features/output/page.service';
|
|
||||||
import RenderService from '~features/output/render.service';
|
import RenderService from '~features/output/render.service';
|
||||||
import ScrollService from '~features/output/scroll.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 StatusService from '~features/output/status.service';
|
||||||
import MessageService from '~features/output/message.service';
|
import MessageService from '~features/output/message.service';
|
||||||
import EventsApiService from '~features/output/api.events.service';
|
import EventsApiService from '~features/output/api.events.service';
|
||||||
|
import SlideService from '~features/output/slide.service';
|
||||||
import LegacyRedirect from '~features/output/legacy.route';
|
import LegacyRedirect from '~features/output/legacy.route';
|
||||||
|
|
||||||
import DetailsComponent from '~features/output/details.component';
|
import DetailsComponent from '~features/output/details.component';
|
||||||
@@ -24,6 +24,8 @@ const MODULE_NAME = 'at.features.output';
|
|||||||
const PAGE_CACHE = true;
|
const PAGE_CACHE = true;
|
||||||
const PAGE_LIMIT = 5;
|
const PAGE_LIMIT = 5;
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
const ORDER_BY = 'counter';
|
||||||
|
// const ORDER_BY = 'start_line';
|
||||||
const WS_PREFIX = 'ws';
|
const WS_PREFIX = 'ws';
|
||||||
|
|
||||||
function resolveResource (
|
function resolveResource (
|
||||||
@@ -74,7 +76,7 @@ function resolveResource (
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
order_by: 'start_line',
|
order_by: ORDER_BY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@@ -250,13 +252,13 @@ angular
|
|||||||
HostEvent
|
HostEvent
|
||||||
])
|
])
|
||||||
.service('OutputStrings', Strings)
|
.service('OutputStrings', Strings)
|
||||||
.service('JobPageService', PageService)
|
.service('OutputScrollService', ScrollService)
|
||||||
.service('JobScrollService', ScrollService)
|
.service('OutputRenderService', RenderService)
|
||||||
.service('JobRenderService', RenderService)
|
.service('OutputStreamService', StreamService)
|
||||||
.service('JobEventEngine', EngineService)
|
.service('OutputStatusService', StatusService)
|
||||||
.service('JobStatusService', StatusService)
|
.service('OutputMessageService', MessageService)
|
||||||
.service('JobMessageService', MessageService)
|
|
||||||
.service('JobEventsApiService', EventsApiService)
|
.service('JobEventsApiService', EventsApiService)
|
||||||
|
.service('OutputSlideService', SlideService)
|
||||||
.component('atJobSearch', SearchComponent)
|
.component('atJobSearch', SearchComponent)
|
||||||
.component('atJobStats', StatsComponent)
|
.component('atJobStats', StatsComponent)
|
||||||
.component('atJobDetails', DetailsComponent)
|
.component('atJobDetails', DetailsComponent)
|
||||||
|
|||||||
@@ -22,17 +22,17 @@
|
|||||||
ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i>
|
ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i>
|
||||||
</div>
|
</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"
|
<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>
|
||||||
<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>
|
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
|
||||||
</div>
|
</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>
|
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i>
|
||||||
</div>
|
</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>
|
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<div ng-show="vm.scroll.showBackToTop" class="at-Stdout-menuBottom">
|
<div ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
|
||||||
<div class="at-Stdout-menuIconGroup" ng-click="vm.scroll.home()">
|
<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-left"><i class="fa fa-angle-double-up"></i></p>
|
||||||
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
|
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
|
||||||
</div>
|
</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;
|
|
||||||
@@ -4,7 +4,7 @@ const DELAY = 100;
|
|||||||
const THRESHOLD = 0.1;
|
const THRESHOLD = 0.1;
|
||||||
|
|
||||||
function JobScrollService ($q, $timeout) {
|
function JobScrollService ($q, $timeout) {
|
||||||
this.init = (hooks) => {
|
this.init = ({ next, previous }) => {
|
||||||
this.el = $(ELEMENT_CONTAINER);
|
this.el = $(ELEMENT_CONTAINER);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
|
|
||||||
@@ -14,15 +14,15 @@ function JobScrollService ($q, $timeout) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.hooks = {
|
this.hooks = {
|
||||||
isAtRest: hooks.isAtRest,
|
next,
|
||||||
next: hooks.next,
|
previous,
|
||||||
previous: hooks.previous
|
isAtRest: () => $q.resolve()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
locked: false,
|
hidden: false,
|
||||||
paused: false,
|
paused: false,
|
||||||
top: true
|
top: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.el.scroll(this.listen);
|
this.el.scroll(this.listen);
|
||||||
@@ -158,6 +158,20 @@ function JobScrollService ($q, $timeout) {
|
|||||||
this.state.locked = false;
|
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.isLocked = () => this.state.locked;
|
||||||
this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ JobSearchController.$inject = [
|
|||||||
'$state',
|
'$state',
|
||||||
'QuerySet',
|
'QuerySet',
|
||||||
'OutputStrings',
|
'OutputStrings',
|
||||||
'JobStatusService',
|
'OutputStatusService',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
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()">
|
<form ng-submit="vm.submitSearch()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control at-Input"
|
class="form-control at-Input"
|
||||||
ng-class="{ 'at-Input--rejected': vm.rejected }"
|
ng-disabled="vm.disabled"
|
||||||
ng-model="vm.value"
|
ng-class="{ 'at-Input--rejected': vm.rejected }"
|
||||||
ng-attr-placeholder="{{ vm.running ? vm.strings.get('search.PLACEHOLDER_RUNNING') :
|
ng-model="vm.value"
|
||||||
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}"
|
ng-attr-placeholder="{{ vm.running ?
|
||||||
ng-disabled="vm.disabled">
|
vm.strings.get('search.PLACEHOLDER_RUNNING') :
|
||||||
<span class="input-group-btn">
|
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}">
|
||||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
<span class="input-group-btn">
|
||||||
|
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||||
ng-click="vm.submitSearch()"
|
ng-click="vm.submitSearch()"
|
||||||
ng-disabled="vm.disabled"
|
ng-disabled="vm.disabled"
|
||||||
type="button">
|
type="button">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn jobz-Button-searchKey"
|
<button class="btn jobz-Button-searchKey"
|
||||||
ng-if="vm.key"
|
ng-if="vm.key"
|
||||||
ng-disabled="vm.disabled"
|
ng-disabled="vm.disabled"
|
||||||
ng-click="vm.toggleSearchKey()"
|
ng-click="vm.toggleSearchKey()"
|
||||||
type="button">
|
type="button"> {{:: vm.strings.get('search.KEY') }}
|
||||||
{{:: vm.strings.get('search.KEY') }}
|
</button>
|
||||||
</button>
|
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
|
||||||
ng-if="!vm.key"
|
ng-if="!vm.key"
|
||||||
ng-disabled="vm.disabled"
|
ng-disabled="vm.disabled"
|
||||||
ng-click="vm.toggleSearchKey()"
|
ng-click="vm.toggleSearchKey()"
|
||||||
type="button">
|
type="button"> {{:: vm.strings.get('search.KEY') }}
|
||||||
{{:: vm.strings.get('search.KEY') }}
|
</button>
|
||||||
</button>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<p ng-if="vm.rejected" class="at-InputMessage--rejected">{{ vm.message }}</p>
|
||||||
<p ng-if="vm.rejected" class="at-InputMessage--rejected">
|
|
||||||
{{ vm.message }}
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="jobz-tagz">
|
<div class="jobz-tagz">
|
||||||
@@ -41,19 +39,25 @@
|
|||||||
<at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag>
|
<at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobz-searchClearAllContainer">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="jobz-searchKeyPaneContainer" ng-show="vm.key">
|
<div class="jobz-searchKeyPaneContainer" ng-show="vm.key">
|
||||||
<div class="jobz-searchKeyPane">
|
<div class="jobz-searchKeyPane">
|
||||||
<div class="SmartSearch-keyRow">
|
<div class="SmartSearch-keyRow">
|
||||||
<div class="SmartSearch-examples">
|
<div class="SmartSearch-examples">
|
||||||
<div class="SmartSearch-examples--title">
|
<div class="SmartSearch-examples--title">
|
||||||
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
|
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
|
||||||
|
</div>
|
||||||
|
<div class="SmartSearch-examples--search"
|
||||||
|
ng-repeat="tag in vm.examples">{{ tag }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="SmartSearch-examples--search" ng-repeat="tag in vm.examples">{{ tag }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="SmartSearch-keyRow">
|
<div class="SmartSearch-keyRow">
|
||||||
<b>{{:: vm.strings.get('search.FIELDS') }}:</b>
|
<b>{{:: vm.strings.get('search.FIELDS') }}:</b>
|
||||||
|
|||||||
298
awx/ui/client/features/output/slide.service.js
Normal file
298
awx/ui/client/features/output/slide.service.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
/* eslint camelcase: 0 */
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
const PAGE_LIMIT = 5;
|
||||||
|
const EVENT_LIMIT = PAGE_LIMIT * PAGE_SIZE;
|
||||||
|
|
||||||
|
const TAIL_ADDITION = 'TAIL_ADDITION';
|
||||||
|
const TAIL_DELETION = 'TAIL_DELETION';
|
||||||
|
const HEAD_ADDITION = 'HEAD_ADDITION';
|
||||||
|
const HEAD_DELETION = 'HEAD_DELETION';
|
||||||
|
|
||||||
|
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.commands = {
|
||||||
|
[TAIL_ADDITION]: this.pushFront,
|
||||||
|
[HEAD_ADDITION]: this.pushBack,
|
||||||
|
[TAIL_DELETION]: this.popFront,
|
||||||
|
[HEAD_DELETION]: this.popBack
|
||||||
|
};
|
||||||
|
|
||||||
|
this.vectors = {
|
||||||
|
[TAIL_ADDITION]: [0, 1],
|
||||||
|
[HEAD_ADDITION]: [-1, 0],
|
||||||
|
[TAIL_DELETION]: [0, -1],
|
||||||
|
[HEAD_DELETION]: [1, 0],
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = Math.max(this.getHeadCounter(), max - count);
|
||||||
|
|
||||||
|
let lines = 0;
|
||||||
|
|
||||||
|
for (let i = min; i <= max; ++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 = min; i <= max; ++i) {
|
||||||
|
delete this.records[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.popBack = count => {
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = this.getHeadCounter();
|
||||||
|
const max = Math.min(this.getTailCounter(), min + count);
|
||||||
|
|
||||||
|
let lines = 0;
|
||||||
|
|
||||||
|
for (let i = min; i <= max; ++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 additions = [];
|
||||||
|
const deletions = [];
|
||||||
|
|
||||||
|
for (let counter = tail + 1; counter <= newTail; counter++) {
|
||||||
|
additions.push([counter, TAIL_ADDITION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let counter = head - 1; counter >= newHead; counter--) {
|
||||||
|
additions.push([counter, HEAD_ADDITION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let counter = head; counter < newHead; counter++) {
|
||||||
|
deletions.push([counter, HEAD_DELETION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let counter = tail; counter > newTail; counter--) {
|
||||||
|
deletions.push([counter, TAIL_DELETION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCounter = (items, n) => items
|
||||||
|
.filter(([counter]) => counter === n).length !== 0;
|
||||||
|
|
||||||
|
const commandRange = {
|
||||||
|
[TAIL_DELETION]: 0,
|
||||||
|
[HEAD_DELETION]: 0,
|
||||||
|
[TAIL_ADDITION]: [tail, tail],
|
||||||
|
[HEAD_ADDITION]: [head, head],
|
||||||
|
};
|
||||||
|
|
||||||
|
deletions.forEach(([counter, key]) => {
|
||||||
|
if (!hasCounter(additions, counter)) {
|
||||||
|
commandRange[key] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandRange[TAIL_ADDITION][0] += this.vectors[key][0];
|
||||||
|
commandRange[TAIL_ADDITION][1] += this.vectors[key][1];
|
||||||
|
|
||||||
|
commandRange[HEAD_ADDITION][0] += this.vectors[key][0];
|
||||||
|
commandRange[HEAD_ADDITION][1] += this.vectors[key][1];
|
||||||
|
});
|
||||||
|
|
||||||
|
additions.forEach(([counter, key]) => {
|
||||||
|
if (!hasCounter(deletions, counter)) {
|
||||||
|
if (counter < commandRange[key][0]) {
|
||||||
|
commandRange[key][0] = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counter > commandRange[key][1]) {
|
||||||
|
commandRange[key][1] = counter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chain = this.chain
|
||||||
|
.then(() => this.commands[TAIL_DELETION](commandRange[TAIL_DELETION]))
|
||||||
|
.then(() => this.commands[HEAD_DELETION](commandRange[HEAD_DELETION]))
|
||||||
|
.then(() => this.api.getRange(commandRange[TAIL_ADDITION]))
|
||||||
|
.then(events => this.commands[TAIL_ADDITION](events))
|
||||||
|
.then(() => this.api.getRange(commandRange[HEAD_ADDITION]))
|
||||||
|
.then(events => this.commands[HEAD_ADDITION](events))
|
||||||
|
.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 headDisplacement = Math.min(tailRoom, displacement);
|
||||||
|
|
||||||
|
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 tailDisplacement = Math.min(headRoom, displacement);
|
||||||
|
|
||||||
|
return this.move([head - headDisplacement, 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.commands[HEAD_DELETION](count));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chain;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getFirst = () => this.clear()
|
||||||
|
.then(() => this.api.getFirst())
|
||||||
|
.then(events => this.commands[TAIL_ADDITION](events))
|
||||||
|
.then(() => this.moveTail(PAGE_SIZE));
|
||||||
|
|
||||||
|
this.getLast = () => this.clear()
|
||||||
|
.then(() => this.api.getLast())
|
||||||
|
.then(events => this.commands[HEAD_ADDITION](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 = [
|
JobStatsController.$inject = [
|
||||||
'OutputStrings',
|
'OutputStrings',
|
||||||
'JobStatusService',
|
'OutputStatusService',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
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">
|
<div class="at-u-floatRight">
|
||||||
<span class="at-Panel-label">plays</span>
|
<span class="at-Panel-label">plays</span>
|
||||||
<span ng-show="!vm.plays" class="at-Panel-headingTitleBadge">...</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.dispatch = () => message.dispatch('status', this.state);
|
||||||
this.subscribe = listener => message.subscribe('status', listener);
|
this.subscribe = listener => message.subscribe('status', listener);
|
||||||
|
|
||||||
this.init = ({ resource }) => {
|
this.init = ({ model, stats }) => {
|
||||||
const { model } = resource;
|
|
||||||
|
|
||||||
this.created = model.get('created');
|
this.created = model.get('created');
|
||||||
this.job = model.get('id');
|
this.job = model.get('id');
|
||||||
this.jobType = model.get('type');
|
this.jobType = model.get('type');
|
||||||
@@ -43,7 +41,7 @@ function JobStatusService (moment, message) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setStatsEvent(resource.stats);
|
this.setStatsEvent(stats);
|
||||||
this.updateStats();
|
this.updateStats();
|
||||||
this.updateRunningState();
|
this.updateRunningState();
|
||||||
|
|
||||||
@@ -213,7 +211,7 @@ function JobStatusService (moment, message) {
|
|||||||
|
|
||||||
JobStatusService.$inject = [
|
JobStatusService.$inject = [
|
||||||
'moment',
|
'moment',
|
||||||
'JobMessageService',
|
'OutputMessageService',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default JobStatusService;
|
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;
|
||||||
Reference in New Issue
Block a user