mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
implement output follow-scroll behavior
This commit is contained in:
parent
fed729f101
commit
80d6b0167c
@ -13,21 +13,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-menuBottom {
|
||||
color: @at-gray-848992;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
bottom: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIconGroup {
|
||||
& > p {
|
||||
margin: 0;
|
||||
@ -74,12 +59,6 @@
|
||||
color: @at-blue;
|
||||
}
|
||||
|
||||
&-menuIconStack--wrapper {
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
|
||||
|
||||
@ -109,6 +109,11 @@ function JobEventsApiService ($http, $q) {
|
||||
}
|
||||
|
||||
const [low, high] = range;
|
||||
|
||||
if (low > high) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
|
||||
|
||||
params.page_size = API_MAX_PAGE_SIZE;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import {
|
||||
EVENT_START_PLAY,
|
||||
EVENT_START_TASK,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
let $compile;
|
||||
@ -54,91 +55,111 @@ function bufferEmpty (min, max) {
|
||||
return removed;
|
||||
}
|
||||
|
||||
let attached = false;
|
||||
let noframes = false;
|
||||
let isOnLastPage = false;
|
||||
|
||||
let lockFrames;
|
||||
function onFrames (events) {
|
||||
if (noframes) {
|
||||
if (lockFrames) {
|
||||
events.forEach(bufferAdd);
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (!attached) {
|
||||
const minCounter = Math.min(...events.map(({ counter }) => counter));
|
||||
|
||||
if (minCounter > slide.getTailCounter() + 1) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
attached = true;
|
||||
}
|
||||
|
||||
if (vm.isInFollowMode) {
|
||||
vm.isFollowing = true;
|
||||
}
|
||||
|
||||
const capacity = slide.getCapacity();
|
||||
|
||||
if (capacity <= 0 && !isOnLastPage) {
|
||||
attached = false;
|
||||
events = slide.pushFrames(events);
|
||||
const popCount = events.length - slide.getCapacity();
|
||||
const isAttached = events.length > 0;
|
||||
|
||||
if (!isAttached) {
|
||||
stopFollowing();
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return slide.popBack(events.length - capacity)
|
||||
.then(() => slide.pushFront(events))
|
||||
if (!vm.isFollowing && canStartFollowing()) {
|
||||
startFollowing();
|
||||
}
|
||||
|
||||
if (!vm.isFollowing && popCount > 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.popBack(popCount)
|
||||
.then(() => {
|
||||
if (vm.isFollowing && scroll.isBeyondLowerThreshold()) {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.pushFront(events);
|
||||
})
|
||||
.then(() => {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function first () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
unfollow();
|
||||
lockFrames = true;
|
||||
|
||||
attached = false;
|
||||
noframes = true;
|
||||
isOnLastPage = false;
|
||||
stopFollowing();
|
||||
|
||||
slide.getFirst()
|
||||
return slide.getFirst()
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
noframes = false;
|
||||
|
||||
return $q.resolve();
|
||||
lockFrames = false;
|
||||
});
|
||||
}
|
||||
|
||||
function next () {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (slide.getTailCounter() >= slide.getMaxCounter()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
return slide.getNext()
|
||||
.then(() => {
|
||||
isOnLastPage = slide.isOnLastPage();
|
||||
if (isOnLastPage) {
|
||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||
if (scroll.isBeyondLowerThreshold()) {
|
||||
scroll.scrollToBottom();
|
||||
follow();
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => scroll.resume());
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
stopFollowing();
|
||||
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
isOnLastPage = false;
|
||||
|
||||
return slide.getPrevious()
|
||||
.then(popHeight => {
|
||||
@ -147,17 +168,87 @@ function previous () {
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.finally(() => scroll.resume());
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
});
|
||||
}
|
||||
|
||||
function last () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
return slide.getLast()
|
||||
.then(() => {
|
||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
});
|
||||
}
|
||||
|
||||
let followOnce;
|
||||
let lockFollow;
|
||||
function canStartFollowing () {
|
||||
if (lockFollow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slide.isOnLastPage() && scroll.isBeyondLowerThreshold()) {
|
||||
followOnce = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (followOnce && // one-time activation from top of first page
|
||||
scroll.isBeyondUpperThreshold() &&
|
||||
slide.getHeadCounter() === 1 &&
|
||||
slide.getTailCounter() >= OUTPUT_PAGE_SIZE) {
|
||||
followOnce = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function startFollowing () {
|
||||
if (vm.isFollowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
vm.isFollowing = true;
|
||||
vm.followTooltip = vm.strings.get('tooltips.MENU_FOLLOWING');
|
||||
}
|
||||
|
||||
function stopFollowing () {
|
||||
if (!vm.isFollowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
vm.isFollowing = false;
|
||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||
}
|
||||
|
||||
function menuLast () {
|
||||
if (vm.isFollowing) {
|
||||
unfollow();
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (isOnLastPage) {
|
||||
lockFollow = false;
|
||||
|
||||
if (slide.isOnLastPage()) {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
@ -166,22 +257,6 @@ function menuLast () {
|
||||
return last();
|
||||
}
|
||||
|
||||
function last () {
|
||||
scroll.pause();
|
||||
|
||||
return slide.getLast()
|
||||
.then(() => {
|
||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
|
||||
isOnLastPage = true;
|
||||
follow();
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function down () {
|
||||
scroll.moveDown();
|
||||
}
|
||||
@ -190,20 +265,6 @@ function up () {
|
||||
scroll.moveUp();
|
||||
}
|
||||
|
||||
function follow () {
|
||||
isOnLastPage = slide.isOnLastPage();
|
||||
|
||||
if (resource.model.get('event_processing_finished')) return;
|
||||
if (!isOnLastPage) return;
|
||||
|
||||
vm.isInFollowMode = true;
|
||||
}
|
||||
|
||||
function unfollow () {
|
||||
vm.isInFollowMode = false;
|
||||
vm.isFollowing = false;
|
||||
}
|
||||
|
||||
function togglePanelExpand () {
|
||||
vm.isPanelExpanded = !vm.isPanelExpanded;
|
||||
}
|
||||
@ -276,7 +337,10 @@ function showHostDetails (id, uuid) {
|
||||
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
||||
}
|
||||
|
||||
let streaming;
|
||||
function stopListening () {
|
||||
streaming = null;
|
||||
|
||||
listeners.forEach(deregister => deregister());
|
||||
listeners.length = 0;
|
||||
}
|
||||
@ -293,13 +357,46 @@ function startListening () {
|
||||
listeners.push($scope.$on(resource.ws.summary, (scope, data) => handleSummaryEvent(data)));
|
||||
}
|
||||
|
||||
function handleStatusEvent (data) {
|
||||
status.pushStatusEvent(data);
|
||||
function handleJobEvent (data) {
|
||||
streaming = streaming || resource.events
|
||||
.getRange([Math.max(1, data.counter - 50), data.counter + 50])
|
||||
.then(results => {
|
||||
results = results.concat(data);
|
||||
|
||||
const counters = results.map(({ counter }) => counter);
|
||||
const min = Math.min(...counters);
|
||||
const max = Math.max(...counters);
|
||||
|
||||
const missing = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
if (counters.indexOf(i) < 0) {
|
||||
missing.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
const maxMissing = Math.max(...missing);
|
||||
results = results.filter(({ counter }) => counter > maxMissing);
|
||||
}
|
||||
|
||||
stream.setMissingCounterThreshold(max + 1);
|
||||
results.forEach(item => {
|
||||
stream.pushJobEvent(item);
|
||||
status.pushJobEvent(item);
|
||||
});
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
streaming
|
||||
.then(() => {
|
||||
stream.pushJobEvent(data);
|
||||
status.pushJobEvent(data);
|
||||
});
|
||||
}
|
||||
|
||||
function handleJobEvent (data) {
|
||||
stream.pushJobEvent(data);
|
||||
status.pushJobEvent(data);
|
||||
function handleStatusEvent (data) {
|
||||
status.pushStatusEvent(data);
|
||||
}
|
||||
|
||||
function handleSummaryEvent (data) {
|
||||
@ -315,13 +412,6 @@ function reloadState (params) {
|
||||
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
|
||||
}
|
||||
|
||||
function getMaxCounter () {
|
||||
const apiMax = resource.events.getMaxCounter();
|
||||
const wsMax = stream.getMaxCounter();
|
||||
|
||||
return Math.max(apiMax, wsMax);
|
||||
}
|
||||
|
||||
function OutputIndexController (
|
||||
_$compile_,
|
||||
_$q_,
|
||||
@ -367,28 +457,27 @@ function OutputIndexController (
|
||||
vm.menu = { last: menuLast, first, down, up };
|
||||
vm.isMenuExpanded = true;
|
||||
vm.isFollowing = false;
|
||||
vm.isInFollowMode = false;
|
||||
vm.toggleMenuExpand = toggleMenuExpand;
|
||||
vm.toggleLineExpand = toggleLineExpand;
|
||||
vm.showHostDetails = showHostDetails;
|
||||
vm.toggleLineEnabled = resource.model.get('type') === 'job';
|
||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||
|
||||
render.requestAnimationFrame(() => {
|
||||
bufferInit();
|
||||
|
||||
status.init(resource);
|
||||
slide.init(render, resource.events, scroll, { getMaxCounter });
|
||||
slide.init(render, resource.events, scroll);
|
||||
render.init({ compile, toggles: vm.toggleLineEnabled });
|
||||
|
||||
scroll.init({
|
||||
next,
|
||||
previous,
|
||||
onLeaveLower () {
|
||||
unfollow();
|
||||
return $q.resolve();
|
||||
},
|
||||
onEnterLower () {
|
||||
follow();
|
||||
onThresholdLeave () {
|
||||
followOnce = false;
|
||||
lockFollow = false;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
},
|
||||
});
|
||||
@ -398,15 +487,29 @@ function OutputIndexController (
|
||||
bufferEmpty,
|
||||
onFrames,
|
||||
onStop () {
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
stopListening();
|
||||
status.updateStats();
|
||||
status.dispatch();
|
||||
unfollow();
|
||||
status.sync();
|
||||
scroll.stop();
|
||||
}
|
||||
});
|
||||
|
||||
startListening();
|
||||
status.subscribe(data => { vm.status = data.status; });
|
||||
if (resource.model.get('event_processing_finished')) {
|
||||
followOnce = false;
|
||||
lockFollow = true;
|
||||
lockFrames = true;
|
||||
stopListening();
|
||||
} else {
|
||||
followOnce = true;
|
||||
lockFollow = false;
|
||||
lockFrames = false;
|
||||
resource.events.clearCache();
|
||||
status.subscribe(data => { vm.status = data.status; });
|
||||
startListening();
|
||||
}
|
||||
|
||||
return last();
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import atLibModels from '~models';
|
||||
import atLibComponents from '~components';
|
||||
|
||||
@ -41,9 +42,7 @@ function resolveResource (
|
||||
Wait,
|
||||
Events,
|
||||
) {
|
||||
const { id, type, handleErrors } = $stateParams;
|
||||
const { job_event_search } = $stateParams; // eslint-disable-line camelcase
|
||||
|
||||
const { id, type, handleErrors, job_event_search } = $stateParams;
|
||||
const { name, key } = getWebSocketResource(type);
|
||||
|
||||
let Resource;
|
||||
|
||||
@ -7,45 +7,52 @@
|
||||
</at-panel>
|
||||
|
||||
<at-panel class="at-Stdout" ng-class="{'at-Stdout--fullscreen': vm.isPanelExpanded}">
|
||||
<div class="at-Stdout-wrapper">
|
||||
<div class="at-Panel-headingTitle">
|
||||
<i ng-show="vm.isPanelExpanded && vm.status"
|
||||
class="JobResults-statusResultIcon fa icon-job-{{ vm.status }}"></i>
|
||||
{{ vm.title }}
|
||||
</div>
|
||||
<at-job-stats
|
||||
resource="vm.resource"
|
||||
expanded="vm.isPanelExpanded">
|
||||
</at-job-stats>
|
||||
<at-job-search reload="vm.reloadState"></at-job-search>
|
||||
|
||||
<div class="at-Stdout-menuTop">
|
||||
<div class="pull-left" ng-click="vm.toggleMenuExpand()">
|
||||
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
|
||||
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
|
||||
<div class="at-Stdout-wrapper">
|
||||
<div class="at-Panel-headingTitle">
|
||||
<i ng-show="vm.isPanelExpanded && vm.status"
|
||||
class="JobResults-statusResultIcon fa icon-job-{{ vm.status }}"></i>
|
||||
{{ vm.title }}
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.last()">
|
||||
<at-job-stats
|
||||
resource="vm.resource"
|
||||
expanded="vm.isPanelExpanded">
|
||||
</at-job-stats>
|
||||
<at-job-search
|
||||
reload="vm.reloadState">
|
||||
</at-job-search>
|
||||
<div class="at-Stdout-menuTop">
|
||||
<div class="pull-left" ng-click="vm.toggleMenuExpand()">
|
||||
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
|
||||
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.last()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
ng-class=" { 'at-Stdout-menuIconStackTop--active': false }"></i>
|
||||
ng-class="{ 'at-Stdout-menuIcon--active': vm.isFollowing }"
|
||||
data-placement="top"
|
||||
data-trigger="hover"
|
||||
data-tip-watch="vm.followTooltip"
|
||||
aw-tool-tip="{{ vm.followTooltip }}">
|
||||
</i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.first()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"
|
||||
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_FIRST') }}"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.down()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"
|
||||
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_DOWN') }}"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.up()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"
|
||||
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_UP') }}"></i>
|
||||
</div>
|
||||
<div class="at-u-clear"></div>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.first()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
|
||||
<div class="at-Stdout-container">
|
||||
<div class="at-Stdout-borderHeader"></div>
|
||||
<div id="atStdoutResultTable"></div>
|
||||
<div class="at-Stdout-borderFooter"></div>
|
||||
</div>
|
||||
<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.menu.up()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
|
||||
</div>
|
||||
|
||||
<div class="at-u-clear"></div>
|
||||
</div>
|
||||
|
||||
<div class="at-Stdout-container">
|
||||
<div class="at-Stdout-borderHeader"></div>
|
||||
<div id="atStdoutResultTable"></div>
|
||||
<div class="at-Stdout-borderFooter"></div>
|
||||
</div>
|
||||
</div>
|
||||
</at-panel>
|
||||
</div>
|
||||
|
||||
@ -20,7 +20,7 @@ function OutputStrings (BaseString) {
|
||||
DOWNLOAD_OUTPUT: t.s('Download Output'),
|
||||
CREDENTIAL: t.s('View the Credential'),
|
||||
EXPAND_OUTPUT: t.s('Expand Output'),
|
||||
EXTRA_VARS: t.s('Read-only view of extra variables added to the job template.'),
|
||||
EXTRA_VARS: t.s('Read-only view of extra variables added to the job template'),
|
||||
INVENTORY: t.s('View the Inventory'),
|
||||
JOB_TEMPLATE: t.s('View the Job Template'),
|
||||
PROJECT: t.s('View the Project'),
|
||||
@ -28,6 +28,11 @@ function OutputStrings (BaseString) {
|
||||
SCHEDULE: t.s('View the Schedule'),
|
||||
SOURCE_WORKFLOW_JOB: t.s('View the source Workflow Job'),
|
||||
USER: t.s('View the User'),
|
||||
MENU_FIRST: t.s('Go to first page'),
|
||||
MENU_DOWN: t.s('Get next page'),
|
||||
MENU_UP: t.s('Get previous page'),
|
||||
MENU_LAST: t.s('Go to last page of available output'),
|
||||
MENU_FOLLOWING: t.s('Currently following output as it arrives. Click to unfollow'),
|
||||
};
|
||||
|
||||
ns.details = {
|
||||
|
||||
@ -4,13 +4,14 @@ import { OUTPUT_PAGE_LIMIT } from './constants';
|
||||
function PageService ($q) {
|
||||
this.init = (storage, api, { getScrollHeight }) => {
|
||||
const { prepend, append, shift, pop, deleteRecord } = storage;
|
||||
const { getPage, getFirst, getLast, getLastPageNumber } = api;
|
||||
const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api;
|
||||
|
||||
this.api = {
|
||||
getPage,
|
||||
getFirst,
|
||||
getLast,
|
||||
getLastPageNumber,
|
||||
getMaxCounter,
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
@ -238,6 +239,7 @@ function PageService ($q) {
|
||||
this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail;
|
||||
this.getRecordCount = () => Object.keys(this.records).length;
|
||||
this.getTailCounter = () => this.state.tail;
|
||||
this.getMaxCounter = () => this.api.getMaxCounter();
|
||||
}
|
||||
|
||||
PageService.$inject = ['$q'];
|
||||
|
||||
@ -69,6 +69,10 @@ function JobRenderService ($q, $sce, $window) {
|
||||
};
|
||||
|
||||
this.transformEvent = event => {
|
||||
if (this.record[event.uuid]) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
if (!event || !event.stdout) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
@ -127,6 +131,7 @@ function JobRenderService ($q, $sce, $window) {
|
||||
start: event.start_line,
|
||||
end: event.end_line,
|
||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||
lineCount: lines.length,
|
||||
isHost: this.isHostEvent(event),
|
||||
};
|
||||
|
||||
@ -167,6 +172,8 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return info;
|
||||
};
|
||||
|
||||
this.getRecord = uuid => this.record[uuid];
|
||||
|
||||
this.deleteRecord = uuid => {
|
||||
delete this.record[uuid];
|
||||
};
|
||||
|
||||
@ -5,9 +5,12 @@ import {
|
||||
OUTPUT_SCROLL_THRESHOLD,
|
||||
} from './constants';
|
||||
|
||||
const MAX_THRASH = 20;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = ({ next, previous, onLeaveLower, onEnterLower }) => {
|
||||
this.init = ({ next, previous, onThresholdLeave }) => {
|
||||
this.el = $(OUTPUT_ELEMENT_CONTAINER);
|
||||
this.chain = $q.resolve();
|
||||
this.timer = null;
|
||||
|
||||
this.position = {
|
||||
@ -23,16 +26,35 @@ function JobScrollService ($q, $timeout) {
|
||||
this.hooks = {
|
||||
next,
|
||||
previous,
|
||||
onLeaveLower,
|
||||
onEnterLower,
|
||||
onThresholdLeave,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
paused: false,
|
||||
locked: false,
|
||||
hover: false,
|
||||
running: true,
|
||||
thrash: 0,
|
||||
};
|
||||
|
||||
this.chain = $q.resolve();
|
||||
this.el.scroll(this.listen);
|
||||
this.el.mouseenter(this.onMouseEnter);
|
||||
this.el.mouseleave(this.onMouseLeave);
|
||||
};
|
||||
|
||||
this.onMouseEnter = () => {
|
||||
this.state.hover = true;
|
||||
|
||||
if (this.state.thrash >= MAX_THRASH) {
|
||||
this.state.thrash = MAX_THRASH - 1;
|
||||
}
|
||||
|
||||
this.unlock();
|
||||
this.unhide();
|
||||
};
|
||||
|
||||
this.onMouseLeave = () => {
|
||||
this.state.hover = false;
|
||||
};
|
||||
|
||||
this.listen = () => {
|
||||
@ -40,6 +62,31 @@ function JobScrollService ($q, $timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.thrash > 0) {
|
||||
if (this.isLocked() || this.state.hover) {
|
||||
this.state.thrash--;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.state.hover) {
|
||||
this.state.thrash++;
|
||||
}
|
||||
|
||||
if (this.state.thrash >= MAX_THRASH) {
|
||||
if (this.isRunning()) {
|
||||
this.lock();
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.hover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timer) {
|
||||
$timeout.cancel(this.timer);
|
||||
}
|
||||
@ -47,17 +94,7 @@ function JobScrollService ($q, $timeout) {
|
||||
this.timer = $timeout(this.register, OUTPUT_SCROLL_DELAY);
|
||||
};
|
||||
|
||||
this.isBeyondThreshold = () => {
|
||||
const position = this.getScrollPosition();
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
const threshold = position / viewport;
|
||||
|
||||
return (1 - threshold) < OUTPUT_SCROLL_THRESHOLD;
|
||||
};
|
||||
|
||||
this.register = () => {
|
||||
this.pause();
|
||||
|
||||
const position = this.getScrollPosition();
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
|
||||
@ -70,20 +107,22 @@ function JobScrollService ($q, $timeout) {
|
||||
const wasBeyondUpperThreshold = this.threshold.previous < OUTPUT_SCROLL_THRESHOLD;
|
||||
const wasBeyondLowerThreshold = (1 - this.threshold.previous) < OUTPUT_SCROLL_THRESHOLD;
|
||||
|
||||
const enteredUpperThreshold = isBeyondUpperThreshold && !wasBeyondUpperThreshold;
|
||||
const enteredLowerThreshold = isBeyondLowerThreshold && !wasBeyondLowerThreshold;
|
||||
const leftLowerThreshold = !isBeyondLowerThreshold && wasBeyondLowerThreshold;
|
||||
|
||||
const transitions = [];
|
||||
|
||||
if (position <= 0 || (isBeyondUpperThreshold && !wasBeyondUpperThreshold)) {
|
||||
if (position <= 0 || enteredUpperThreshold) {
|
||||
transitions.push(this.hooks.onThresholdLeave);
|
||||
transitions.push(this.hooks.previous);
|
||||
}
|
||||
|
||||
if (!isBeyondLowerThreshold && wasBeyondLowerThreshold) {
|
||||
transitions.push(this.hooks.onLeaveLower);
|
||||
if (leftLowerThreshold) {
|
||||
transitions.push(this.hooks.onThresholdLeave);
|
||||
}
|
||||
|
||||
if (isBeyondLowerThreshold && !wasBeyondLowerThreshold) {
|
||||
transitions.push(this.hooks.onEnterLower);
|
||||
transitions.push(this.hooks.next);
|
||||
} else if (threshold >= 1) {
|
||||
if (threshold >= 1 || enteredLowerThreshold) {
|
||||
transitions.push(this.hooks.next);
|
||||
}
|
||||
|
||||
@ -100,7 +139,6 @@ function JobScrollService ($q, $timeout) {
|
||||
|
||||
return this.chain
|
||||
.then(() => {
|
||||
this.resume();
|
||||
this.setScrollPosition(this.getScrollPosition());
|
||||
|
||||
return $q.resolve();
|
||||
@ -157,16 +195,70 @@ function JobScrollService ($q, $timeout) {
|
||||
this.setScrollPosition(this.getScrollHeight());
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
this.start = () => {
|
||||
this.state.running = true;
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
this.unlock();
|
||||
this.unhide();
|
||||
this.state.running = false;
|
||||
};
|
||||
|
||||
this.lock = () => {
|
||||
this.state.locked = true;
|
||||
};
|
||||
|
||||
this.unlock = () => {
|
||||
this.state.locked = false;
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
};
|
||||
|
||||
this.hide = () => {
|
||||
if (this.state.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.hidden = true;
|
||||
this.el.css('overflow-y', 'hidden');
|
||||
};
|
||||
|
||||
this.unhide = () => {
|
||||
if (!this.state.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.hidden = false;
|
||||
this.el.css('overflow-y', 'auto');
|
||||
};
|
||||
|
||||
this.isBeyondLowerThreshold = () => {
|
||||
const position = this.getScrollPosition();
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
const threshold = position / viewport;
|
||||
|
||||
return (1 - threshold) < OUTPUT_SCROLL_THRESHOLD;
|
||||
};
|
||||
|
||||
this.isBeyondUpperThreshold = () => {
|
||||
const position = this.getScrollPosition();
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
const threshold = position / viewport;
|
||||
|
||||
return threshold < OUTPUT_SCROLL_THRESHOLD;
|
||||
};
|
||||
|
||||
this.isPaused = () => this.state.paused;
|
||||
this.isRunning = () => this.state.running;
|
||||
this.isLocked = () => this.state.locked;
|
||||
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
}
|
||||
|
||||
JobScrollService.$inject = ['$q', '$timeout'];
|
||||
|
||||
@ -1,85 +1,42 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import {
|
||||
API_MAX_PAGE_SIZE,
|
||||
OUTPUT_EVENT_LIMIT,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
function getContinuous (events, reverse = false) {
|
||||
const counters = events.map(({ counter }) => counter);
|
||||
|
||||
return (range[1] - range[0]) + (other[1] - other[0]) >= span;
|
||||
}
|
||||
const min = Math.min(...counters);
|
||||
const max = Math.max(...counters);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const missing = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
if (counters.indexOf(i) < 0) {
|
||||
missing.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return [range[0] - other[0], other[1] - range[1]];
|
||||
}
|
||||
if (missing.length === 0) {
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a minimum and maximum boundary to a range.
|
||||
*
|
||||
* @arg {Array} range - A [low, high] range array.
|
||||
* @arg {Array} other - A [low, high] range array to be applied as a boundary.
|
||||
*
|
||||
* @returns {(Array)} - Returns a new range array by applying the second range
|
||||
* as a boundary to the first.
|
||||
*
|
||||
* getBoundedRange([2, 6], [2, 8]) = [2, 6]
|
||||
* getBoundedRange([1, 9], [2, 8]) = [2, 8]
|
||||
* getBoundedRange([4, 9], [2, 8]) = [4, 8]
|
||||
*/
|
||||
function getBoundedRange (range, other) {
|
||||
return [Math.max(range[0], other[0]), Math.min(range[1], other[1])];
|
||||
if (reverse) {
|
||||
const threshold = Math.max(...missing);
|
||||
|
||||
return events.filter(({ counter }) => counter > threshold);
|
||||
}
|
||||
|
||||
const threshold = Math.min(...missing);
|
||||
|
||||
return events.filter(({ counter }) => counter < threshold);
|
||||
}
|
||||
|
||||
function SlidingWindowService ($q) {
|
||||
this.init = (storage, api, { getScrollHeight }, { getMaxCounter }) => {
|
||||
const { prepend, append, shift, pop, deleteRecord } = storage;
|
||||
const { getRange, getFirst, getLast } = api;
|
||||
this.init = (storage, api, { getScrollHeight }) => {
|
||||
const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage;
|
||||
const { getRange, getFirst, getLast, getMaxCounter } = api;
|
||||
|
||||
this.api = {
|
||||
getRange,
|
||||
@ -89,10 +46,12 @@ function SlidingWindowService ($q) {
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
clear,
|
||||
prepend,
|
||||
append,
|
||||
shift,
|
||||
pop,
|
||||
getRecord,
|
||||
deleteRecord,
|
||||
};
|
||||
|
||||
@ -100,11 +59,79 @@ function SlidingWindowService ($q) {
|
||||
getScrollHeight,
|
||||
};
|
||||
|
||||
this.records = {};
|
||||
this.lines = {};
|
||||
this.uuids = {};
|
||||
this.chain = $q.resolve();
|
||||
|
||||
api.clearCache();
|
||||
this.state = { head: null, tail: null };
|
||||
this.cache = { first: null };
|
||||
|
||||
this.buffer = {
|
||||
events: [],
|
||||
min: 0,
|
||||
max: 0,
|
||||
count: 0,
|
||||
};
|
||||
};
|
||||
|
||||
this.getBoundedRange = range => {
|
||||
const bounds = [1, this.getMaxCounter()];
|
||||
|
||||
return [Math.max(range[0], bounds[0]), Math.min(range[1], bounds[1])];
|
||||
};
|
||||
|
||||
this.getNextRange = displacement => {
|
||||
const tail = this.getTailCounter();
|
||||
|
||||
return this.getBoundedRange([tail + 1, tail + 1 + displacement]);
|
||||
};
|
||||
|
||||
this.getPreviousRange = displacement => {
|
||||
const head = this.getHeadCounter();
|
||||
|
||||
return this.getBoundedRange([head - 1 - displacement, head - 1]);
|
||||
};
|
||||
|
||||
this.createRecord = ({ counter, uuid, start_line, end_line }) => {
|
||||
this.lines[counter] = end_line - start_line;
|
||||
this.uuids[counter] = uuid;
|
||||
|
||||
if (this.state.tail === null) {
|
||||
this.state.tail = counter;
|
||||
}
|
||||
|
||||
if (counter > this.state.tail) {
|
||||
this.state.tail = counter;
|
||||
}
|
||||
|
||||
if (this.state.head === null) {
|
||||
this.state.head = counter;
|
||||
}
|
||||
|
||||
if (counter < this.state.head) {
|
||||
this.state.head = counter;
|
||||
}
|
||||
};
|
||||
|
||||
this.deleteRecord = counter => {
|
||||
this.storage.deleteRecord(this.uuids[counter]);
|
||||
|
||||
delete this.uuids[counter];
|
||||
delete this.lines[counter];
|
||||
};
|
||||
|
||||
this.getLineCount = counter => {
|
||||
const record = this.storage.getRecord(counter);
|
||||
|
||||
if (record && record.lineCount) {
|
||||
return record.lineCount;
|
||||
}
|
||||
|
||||
if (this.lines[counter]) {
|
||||
return this.lines[counter];
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
this.pushFront = events => {
|
||||
@ -113,10 +140,7 @@ function SlidingWindowService ($q) {
|
||||
|
||||
return this.storage.append(newEvents)
|
||||
.then(() => {
|
||||
newEvents.forEach(({ counter, start_line, end_line, uuid }) => {
|
||||
this.records[counter] = { start_line, end_line };
|
||||
this.uuids[counter] = uuid;
|
||||
});
|
||||
newEvents.forEach(event => this.createRecord(event));
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
@ -129,10 +153,7 @@ function SlidingWindowService ($q) {
|
||||
|
||||
return this.storage.prepend(newEvents)
|
||||
.then(() => {
|
||||
newEvents.forEach(({ counter, start_line, end_line, uuid }) => {
|
||||
this.records[counter] = { start_line, end_line };
|
||||
this.uuids[counter] = uuid;
|
||||
});
|
||||
newEvents.forEach(event => this.createRecord(event));
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
@ -149,18 +170,14 @@ function SlidingWindowService ($q) {
|
||||
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);
|
||||
}
|
||||
lines += this.getLineCount(i);
|
||||
}
|
||||
|
||||
return this.storage.pop(lines)
|
||||
.then(() => {
|
||||
for (let i = max; i >= min; --i) {
|
||||
delete this.records[i];
|
||||
|
||||
this.storage.deleteRecord(this.uuids[i]);
|
||||
delete this.uuids[i];
|
||||
this.deleteRecord(i);
|
||||
this.state.tail--;
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
@ -178,184 +195,219 @@ function SlidingWindowService ($q) {
|
||||
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);
|
||||
}
|
||||
lines += this.getLineCount(i);
|
||||
}
|
||||
|
||||
return this.storage.shift(lines)
|
||||
.then(() => {
|
||||
for (let i = min; i <= max; ++i) {
|
||||
delete this.records[i];
|
||||
|
||||
this.storage.deleteRecord(this.uuids[i]);
|
||||
delete this.uuids[i];
|
||||
this.deleteRecord(i);
|
||||
this.state.head++;
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.move = ([low, high]) => {
|
||||
const bounds = [1, this.getMaxCounter()];
|
||||
const [newHead, newTail] = getBoundedRange([low, high], bounds);
|
||||
this.clear = () => this.storage.clear()
|
||||
.then(() => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
let popHeight = this.hooks.getScrollHeight();
|
||||
for (let i = head; i <= tail; ++i) {
|
||||
this.deleteRecord(i);
|
||||
}
|
||||
|
||||
if (newHead > newTail) {
|
||||
this.chain = this.chain
|
||||
.then(() => $q.resolve(popHeight));
|
||||
this.state.head = null;
|
||||
this.state.tail = null;
|
||||
|
||||
return this.chain;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(newHead) || !Number.isFinite(newTail)) {
|
||||
this.chain = this.chain
|
||||
.then(() => $q.resolve(popHeight));
|
||||
|
||||
return this.chain;
|
||||
}
|
||||
|
||||
const [head, tail] = this.getRange();
|
||||
const overlap = getOverlapArray([head, tail], [newHead, newTail]);
|
||||
|
||||
if (!overlap) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.clear())
|
||||
.then(() => this.api.getRange([newHead, newTail]))
|
||||
.then(events => this.pushFront(events));
|
||||
}
|
||||
|
||||
if (overlap && overlap[0] < 0) {
|
||||
const popBackCount = Math.abs(overlap[0]);
|
||||
|
||||
this.chain = this.chain.then(() => this.popBack(popBackCount));
|
||||
}
|
||||
|
||||
if (overlap && overlap[1] < 0) {
|
||||
const popFrontCount = Math.abs(overlap[1]);
|
||||
|
||||
this.chain = this.chain.then(() => this.popFront(popFrontCount));
|
||||
}
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
popHeight = this.hooks.getScrollHeight();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => $q.resolve(popHeight));
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.getNext = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||
const next = this.getNextRange(displacement);
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const tailRoom = this.getMaxCounter() - tail;
|
||||
const tailDisplacement = Math.min(tailRoom, displacement);
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getRange(next))
|
||||
.then(events => {
|
||||
const results = getContinuous(events);
|
||||
const min = Math.min(...results.map(({ counter }) => counter));
|
||||
|
||||
const newTail = tail + tailDisplacement;
|
||||
if (min > tail + 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
let headDisplacement = 0;
|
||||
return $q.resolve(results);
|
||||
})
|
||||
.then(results => {
|
||||
const count = (tail - head + results.length);
|
||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
||||
|
||||
if (newTail - head > OUTPUT_EVENT_LIMIT) {
|
||||
headDisplacement = (newTail - OUTPUT_EVENT_LIMIT) - head;
|
||||
}
|
||||
return this.popBack(excess)
|
||||
.then(() => {
|
||||
const popHeight = this.hooks.getScrollHeight();
|
||||
|
||||
return this.move([head + headDisplacement, tail + tailDisplacement]);
|
||||
return this.pushFront(results).then(() => $q.resolve(popHeight));
|
||||
});
|
||||
});
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||
const previous = this.getPreviousRange(displacement);
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const headRoom = head - 1;
|
||||
const headDisplacement = Math.min(headRoom, displacement);
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getRange(previous))
|
||||
.then(events => {
|
||||
const results = getContinuous(events, true);
|
||||
const max = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
const newHead = head - headDisplacement;
|
||||
if (head > max + 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
let tailDisplacement = 0;
|
||||
return $q.resolve(results);
|
||||
})
|
||||
.then(results => {
|
||||
const count = (tail - head + results.length);
|
||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
||||
|
||||
if (tail - newHead > OUTPUT_EVENT_LIMIT) {
|
||||
tailDisplacement = tail - (newHead + OUTPUT_EVENT_LIMIT);
|
||||
}
|
||||
return this.popFront(excess)
|
||||
.then(() => {
|
||||
const popHeight = this.hooks.getScrollHeight();
|
||||
|
||||
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.pushBack(results).then(() => $q.resolve(popHeight));
|
||||
});
|
||||
});
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.getFirst = () => this.clear()
|
||||
.then(() => this.api.getFirst())
|
||||
.then(events => this.pushFront(events))
|
||||
.then(() => this.moveTail(OUTPUT_PAGE_SIZE));
|
||||
this.getFirst = () => {
|
||||
this.chain = this.chain
|
||||
.then(() => this.clear())
|
||||
.then(() => {
|
||||
if (this.cache.first) {
|
||||
return $q.resolve(this.cache.first);
|
||||
}
|
||||
|
||||
this.getLast = () => this.clear()
|
||||
.then(() => this.api.getLast())
|
||||
.then(events => this.pushBack(events))
|
||||
.then(() => this.moveHead(-OUTPUT_PAGE_SIZE));
|
||||
return this.api.getFirst();
|
||||
})
|
||||
.then(events => {
|
||||
if (events.length === OUTPUT_PAGE_SIZE) {
|
||||
this.cache.first = events;
|
||||
}
|
||||
|
||||
return this.pushFront(events);
|
||||
});
|
||||
|
||||
return this.chain
|
||||
.then(() => this.getNext());
|
||||
};
|
||||
|
||||
this.getLast = () => {
|
||||
this.chain = this.chain
|
||||
.then(() => this.getFrames())
|
||||
.then(frames => {
|
||||
if (frames.length > 0) {
|
||||
return $q.resolve(frames);
|
||||
}
|
||||
|
||||
return this.api.getLast();
|
||||
})
|
||||
.then(events => {
|
||||
const min = Math.min(...events.map(({ counter }) => counter));
|
||||
|
||||
if (min <= this.getTailCounter() + 1) {
|
||||
return this.pushFront(events);
|
||||
}
|
||||
|
||||
return this.clear()
|
||||
.then(() => this.pushBack(events));
|
||||
});
|
||||
|
||||
return this.chain
|
||||
.then(() => this.getPrevious());
|
||||
};
|
||||
|
||||
this.getTailCounter = () => {
|
||||
const tail = Math.max(...Object.keys(this.records));
|
||||
if (this.state.tail === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number.isFinite(tail) ? tail : 0;
|
||||
if (this.state.tail < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.state.tail;
|
||||
};
|
||||
|
||||
this.getHeadCounter = () => {
|
||||
const head = Math.min(...Object.keys(this.records));
|
||||
if (this.state.head === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number.isFinite(head) ? head : 0;
|
||||
if (this.state.head < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.state.head;
|
||||
};
|
||||
|
||||
this.pushFrames = events => {
|
||||
const frames = this.buffer.events.concat(events);
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
let min;
|
||||
let max;
|
||||
let count = 0;
|
||||
|
||||
for (let i = frames.length - 1; i >= 0; i--) {
|
||||
count++;
|
||||
|
||||
if (count > API_MAX_PAGE_SIZE) {
|
||||
frames.splice(i, 1);
|
||||
|
||||
count--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!min || frames[i].counter < min) {
|
||||
min = frames[i].counter;
|
||||
}
|
||||
|
||||
if (!max || frames[i].counter > max) {
|
||||
max = frames[i].counter;
|
||||
}
|
||||
}
|
||||
|
||||
this.buffer.events = frames;
|
||||
this.buffer.min = min;
|
||||
this.buffer.max = max;
|
||||
this.buffer.count = count;
|
||||
|
||||
if (min >= head && min <= tail + 1) {
|
||||
return frames.filter(({ counter }) => counter > tail);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
this.getFrames = () => $q.resolve(this.buffer.events);
|
||||
|
||||
this.getMaxCounter = () => {
|
||||
if (this.buffer.min) {
|
||||
return this.buffer.min;
|
||||
}
|
||||
|
||||
return this.api.getMaxCounter();
|
||||
};
|
||||
|
||||
this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE);
|
||||
this.getMaxCounter = () => this.api.getMaxCounter();
|
||||
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
|
||||
this.getRecordCount = () => Object.keys(this.records).length;
|
||||
this.getRecordCount = () => Object.keys(this.lines).length;
|
||||
this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount();
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ function JobStatusService (moment, message) {
|
||||
this.subscribe = listener => message.subscribe('status', listener);
|
||||
|
||||
this.init = ({ model }) => {
|
||||
this.model = model;
|
||||
this.created = model.get('created');
|
||||
this.job = model.get('id');
|
||||
this.jobType = model.get('type');
|
||||
@ -44,6 +45,14 @@ function JobStatusService (moment, message) {
|
||||
},
|
||||
};
|
||||
|
||||
this.initHostStatusCounts({ model });
|
||||
this.initPlaybookCounts({ model });
|
||||
|
||||
this.updateRunningState();
|
||||
this.dispatch();
|
||||
};
|
||||
|
||||
this.initHostStatusCounts = ({ model }) => {
|
||||
if (model.has('host_status_counts')) {
|
||||
this.setHostStatusCounts(model.get('host_status_counts'));
|
||||
} else {
|
||||
@ -51,15 +60,14 @@ function JobStatusService (moment, message) {
|
||||
|
||||
this.setHostStatusCounts(hostStatusCounts);
|
||||
}
|
||||
};
|
||||
|
||||
this.initPlaybookCounts = ({ model }) => {
|
||||
if (model.has('playbook_counts')) {
|
||||
this.setPlaybookCounts(model.get('playbook_counts'));
|
||||
} else {
|
||||
this.setPlaybookCounts({ task_count: 1, play_count: 1 });
|
||||
}
|
||||
|
||||
this.updateRunningState();
|
||||
this.dispatch();
|
||||
};
|
||||
|
||||
this.createHostStatusCounts = status => {
|
||||
@ -198,13 +206,16 @@ function JobStatusService (moment, message) {
|
||||
const isFinished = JOB_STATUS_FINISHED.includes(status);
|
||||
const isAlreadyFinished = JOB_STATUS_FINISHED.includes(this.state.status);
|
||||
|
||||
if (isAlreadyFinished) {
|
||||
if (isAlreadyFinished && !isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isExpectingStats && isIncomplete) || (!isExpectingStats && isFinished)) {
|
||||
if (this.latestTime) {
|
||||
this.setFinished(this.latestTime);
|
||||
if (!this.state.finished) {
|
||||
this.setFinished(this.latestTime);
|
||||
}
|
||||
|
||||
if (!this.state.started && this.state.elapsed) {
|
||||
this.setStarted(moment(this.latestTime)
|
||||
.subtract(this.state.elapsed, 'seconds'));
|
||||
@ -217,10 +228,14 @@ function JobStatusService (moment, message) {
|
||||
};
|
||||
|
||||
this.setElapsed = elapsed => {
|
||||
if (!elapsed) return;
|
||||
|
||||
this.state.elapsed = elapsed;
|
||||
};
|
||||
|
||||
this.setStarted = started => {
|
||||
if (!started) return;
|
||||
|
||||
this.state.started = started;
|
||||
this.updateRunningState();
|
||||
};
|
||||
@ -234,11 +249,15 @@ function JobStatusService (moment, message) {
|
||||
};
|
||||
|
||||
this.setFinished = time => {
|
||||
if (!time) return;
|
||||
|
||||
this.state.finished = time;
|
||||
this.updateRunningState();
|
||||
};
|
||||
|
||||
this.setStatsEvent = data => {
|
||||
if (!data) return;
|
||||
|
||||
this.statsEvent = data;
|
||||
};
|
||||
|
||||
@ -267,6 +286,23 @@ function JobStatusService (moment, message) {
|
||||
this.state.counts.tasks = 0;
|
||||
this.state.counts.hosts = 0;
|
||||
};
|
||||
|
||||
this.sync = () => {
|
||||
const { model } = this;
|
||||
|
||||
return model.http.get({ resource: model.get('id') })
|
||||
.then(() => {
|
||||
this.setFinished(model.get('finished'));
|
||||
this.setElapsed(model.get('elapsed'));
|
||||
this.setStarted(model.get('started'));
|
||||
this.setJobStatus(model.get('status'));
|
||||
|
||||
this.initHostStatusCounts({ model });
|
||||
this.initPlaybookCounts({ model });
|
||||
|
||||
this.dispatch();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
JobStatusService.$inject = [
|
||||
|
||||
@ -24,7 +24,7 @@ function OutputStream ($q) {
|
||||
|
||||
this.state = {
|
||||
ending: false,
|
||||
ended: false
|
||||
ended: false,
|
||||
};
|
||||
|
||||
this.lag = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user