mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge pull request #2740 from jakemcdermott/fix-2241
implement auto-follow-scroll behavior for job output
This commit is contained in:
commit
41f88fd2f6
@ -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;
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
const API_PAGE_SIZE = 200;
|
||||
const PAGE_SIZE = 50;
|
||||
const ORDER_BY = 'counter';
|
||||
import {
|
||||
API_MAX_PAGE_SIZE,
|
||||
OUTPUT_ORDER_BY,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
const BASE_PARAMS = {
|
||||
page_size: PAGE_SIZE,
|
||||
order_by: ORDER_BY,
|
||||
page_size: OUTPUT_PAGE_SIZE,
|
||||
order_by: OUTPUT_ORDER_BY,
|
||||
};
|
||||
|
||||
const merge = (...objs) => _.merge({}, ...objs);
|
||||
@ -18,12 +20,6 @@ function JobEventsApiService ($http, $q) {
|
||||
this.cache = {};
|
||||
};
|
||||
|
||||
this.clearCache = () => {
|
||||
Object.keys(this.cache).forEach(key => {
|
||||
delete this.cache[key];
|
||||
});
|
||||
};
|
||||
|
||||
this.fetch = () => this.getLast()
|
||||
.then(results => {
|
||||
this.cache.last = results;
|
||||
@ -31,20 +27,31 @@ function JobEventsApiService ($http, $q) {
|
||||
return this;
|
||||
});
|
||||
|
||||
this.clearCache = () => {
|
||||
Object.keys(this.cache).forEach(key => {
|
||||
delete this.cache[key];
|
||||
});
|
||||
};
|
||||
|
||||
this.pushMaxCounter = events => {
|
||||
const maxCounter = Math.max(...events.map(({ counter }) => counter));
|
||||
|
||||
if (maxCounter > this.state.maxCounter) {
|
||||
this.state.maxCounter = maxCounter;
|
||||
}
|
||||
|
||||
return maxCounter;
|
||||
};
|
||||
|
||||
this.getFirst = () => {
|
||||
const page = 1;
|
||||
const params = merge(this.params, { page });
|
||||
const params = merge(this.params, { page: 1 });
|
||||
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results, count } = data;
|
||||
const maxCounter = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
this.state.count = count;
|
||||
|
||||
if (maxCounter > this.state.maxCounter) {
|
||||
this.state.maxCounter = maxCounter;
|
||||
}
|
||||
this.pushMaxCounter(results);
|
||||
|
||||
return results;
|
||||
});
|
||||
@ -60,13 +67,9 @@ function JobEventsApiService ($http, $q) {
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results, count } = data;
|
||||
const maxCounter = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
this.state.count = count;
|
||||
|
||||
if (maxCounter > this.state.maxCounter) {
|
||||
this.state.maxCounter = maxCounter;
|
||||
}
|
||||
this.pushMaxCounter(results);
|
||||
|
||||
return results;
|
||||
});
|
||||
@ -77,17 +80,16 @@ function JobEventsApiService ($http, $q) {
|
||||
return $q.resolve(this.cache.last);
|
||||
}
|
||||
|
||||
const params = merge(this.params, { page: 1, order_by: `-${ORDER_BY}` });
|
||||
const params = merge(this.params, { page: 1, order_by: `-${OUTPUT_ORDER_BY}` });
|
||||
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results, count } = data;
|
||||
const maxCounter = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
let rotated = results;
|
||||
|
||||
if (count > PAGE_SIZE) {
|
||||
rotated = results.splice(count % PAGE_SIZE);
|
||||
if (count > OUTPUT_PAGE_SIZE) {
|
||||
rotated = results.splice(count % OUTPUT_PAGE_SIZE);
|
||||
|
||||
if (results.length > 0) {
|
||||
rotated = results;
|
||||
@ -95,10 +97,7 @@ function JobEventsApiService ($http, $q) {
|
||||
}
|
||||
|
||||
this.state.count = count;
|
||||
|
||||
if (maxCounter > this.state.maxCounter) {
|
||||
this.state.maxCounter = maxCounter;
|
||||
}
|
||||
this.pushMaxCounter(results);
|
||||
|
||||
return rotated;
|
||||
});
|
||||
@ -110,24 +109,26 @@ 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_PAGE_SIZE;
|
||||
params.page_size = API_MAX_PAGE_SIZE;
|
||||
|
||||
return $http.get(this.endpoint, { params })
|
||||
.then(({ data }) => {
|
||||
const { results } = data;
|
||||
const maxCounter = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
if (maxCounter > this.state.maxCounter) {
|
||||
this.state.maxCounter = maxCounter;
|
||||
}
|
||||
this.pushMaxCounter(results);
|
||||
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
this.getLastPageNumber = () => Math.ceil(this.state.count / PAGE_SIZE);
|
||||
this.getLastPageNumber = () => Math.ceil(this.state.count / OUTPUT_PAGE_SIZE);
|
||||
this.getMaxCounter = () => this.state.maxCounter;
|
||||
}
|
||||
|
||||
|
||||
30
awx/ui/client/features/output/constants.js
Normal file
30
awx/ui/client/features/output/constants.js
Normal file
@ -0,0 +1,30 @@
|
||||
export const API_MAX_PAGE_SIZE = 200;
|
||||
export const API_ROOT = '/api/v2/';
|
||||
|
||||
export const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
export const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
export const EVENT_START_PLAYBOOK = 'playbook_on_start';
|
||||
export const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||
|
||||
export const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped'];
|
||||
|
||||
export const JOB_STATUS_COMPLETE = ['successful', 'failed', 'unknown'];
|
||||
export const JOB_STATUS_INCOMPLETE = ['canceled', 'error'];
|
||||
export const JOB_STATUS_UNSUCCESSFUL = ['failed'].concat(JOB_STATUS_INCOMPLETE);
|
||||
export const JOB_STATUS_FINISHED = JOB_STATUS_COMPLETE.concat(JOB_STATUS_INCOMPLETE);
|
||||
|
||||
export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
export const OUTPUT_MAX_LAG = 120;
|
||||
export const OUTPUT_ORDER_BY = 'counter';
|
||||
export const OUTPUT_PAGE_CACHE = true;
|
||||
export const OUTPUT_PAGE_LIMIT = 5;
|
||||
export const OUTPUT_PAGE_SIZE = 50;
|
||||
export const OUTPUT_SCROLL_DELAY = 100;
|
||||
export const OUTPUT_SCROLL_THRESHOLD = 0.1;
|
||||
export const OUTPUT_SEARCH_DOCLINK = 'https://docs.ansible.com/ansible-tower/3.3.0/html/userguide/search_sort.html';
|
||||
export const OUTPUT_SEARCH_FIELDS = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play'];
|
||||
export const OUTPUT_SEARCH_KEY_EXAMPLES = ['host_name:localhost', 'task:set', 'created:>=2000-01-01'];
|
||||
export const OUTPUT_EVENT_LIMIT = OUTPUT_PAGE_LIMIT * OUTPUT_PAGE_SIZE;
|
||||
|
||||
export const WS_PREFIX = 'ws';
|
||||
@ -1,6 +1,9 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
import {
|
||||
EVENT_START_PLAY,
|
||||
EVENT_START_TASK,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
let $compile;
|
||||
let $q;
|
||||
@ -20,9 +23,6 @@ const bufferState = [0, 0]; // [length, count]
|
||||
const listeners = [];
|
||||
const rx = [];
|
||||
|
||||
let following = false;
|
||||
let attach = true;
|
||||
|
||||
function bufferInit () {
|
||||
rx.length = 0;
|
||||
|
||||
@ -55,57 +55,109 @@ function bufferEmpty (min, max) {
|
||||
return removed;
|
||||
}
|
||||
|
||||
let lockFrames;
|
||||
function onFrames (events) {
|
||||
if (!following) {
|
||||
const minCounter = Math.min(...events.map(({ counter }) => counter));
|
||||
// attachment range
|
||||
const max = slide.getTailCounter() + 1;
|
||||
const min = Math.max(1, slide.getHeadCounter(), max - 50);
|
||||
|
||||
if (minCounter > max || minCounter < min) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (!attach) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
follow();
|
||||
if (lockFrames) {
|
||||
events.forEach(bufferAdd);
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const capacity = slide.getCapacity();
|
||||
events = slide.pushFrames(events);
|
||||
const popCount = events.length - slide.getCapacity();
|
||||
const isAttached = events.length > 0;
|
||||
|
||||
return slide.popBack(events.length - capacity)
|
||||
.then(() => slide.pushFront(events))
|
||||
.then(() => {
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
if (!isAttached) {
|
||||
stopFollowing();
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
if (!vm.isFollowing && canStartFollowing()) {
|
||||
startFollowing();
|
||||
}
|
||||
|
||||
if (!vm.isFollowing && popCount > 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
function first () {
|
||||
unfollow();
|
||||
scroll.pause();
|
||||
|
||||
return slide.getFirst()
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.popBack(popCount)
|
||||
.then(() => {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.pushFront(events);
|
||||
})
|
||||
.then(() => {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function next () {
|
||||
function first () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
stopFollowing();
|
||||
|
||||
return slide.getFirst()
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
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()
|
||||
.finally(() => scroll.resume());
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
unfollow();
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
stopFollowing();
|
||||
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
|
||||
@ -116,51 +168,101 @@ 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.setScrollPosition(scroll.getScrollHeight());
|
||||
|
||||
attach = true;
|
||||
scroll.resume();
|
||||
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) {
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
lockFollow = false;
|
||||
|
||||
if (slide.isOnLastPage()) {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return last();
|
||||
}
|
||||
|
||||
function down () {
|
||||
scroll.moveDown();
|
||||
}
|
||||
|
||||
function up () {
|
||||
if (following) {
|
||||
unfollow();
|
||||
} else {
|
||||
scroll.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
function follow () {
|
||||
scroll.pause();
|
||||
scroll.hide();
|
||||
|
||||
following = true;
|
||||
vm.isFollowing = following;
|
||||
}
|
||||
|
||||
function unfollow () {
|
||||
attach = false;
|
||||
following = false;
|
||||
vm.isFollowing = following;
|
||||
|
||||
scroll.unhide();
|
||||
scroll.resume();
|
||||
scroll.moveUp();
|
||||
}
|
||||
|
||||
function togglePanelExpand () {
|
||||
@ -235,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;
|
||||
}
|
||||
@ -252,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) {
|
||||
@ -316,37 +454,62 @@ function OutputIndexController (
|
||||
vm.togglePanelExpand = togglePanelExpand;
|
||||
|
||||
// Stdout Navigation
|
||||
vm.menu = { last, first, down, up };
|
||||
vm.menu = { last: menuLast, first, down, up };
|
||||
vm.isMenuExpanded = true;
|
||||
vm.isFollowing = following;
|
||||
vm.isFollowing = 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);
|
||||
|
||||
render.init({ compile, toggles: vm.toggleLineEnabled });
|
||||
scroll.init({ previous, next });
|
||||
|
||||
scroll.init({
|
||||
next,
|
||||
previous,
|
||||
onThresholdLeave () {
|
||||
followOnce = false;
|
||||
lockFollow = false;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
stream.init({
|
||||
bufferAdd,
|
||||
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';
|
||||
|
||||
@ -18,16 +19,15 @@ import SearchComponent from '~features/output/search.component';
|
||||
import StatsComponent from '~features/output/stats.component';
|
||||
import HostEvent from './host-event/index';
|
||||
|
||||
const Template = require('~features/output/index.view.html');
|
||||
import {
|
||||
API_ROOT,
|
||||
OUTPUT_ORDER_BY,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
WS_PREFIX,
|
||||
} from './constants';
|
||||
|
||||
const MODULE_NAME = 'at.features.output';
|
||||
|
||||
const PAGE_CACHE = true;
|
||||
const PAGE_LIMIT = 5;
|
||||
const PAGE_SIZE = 50;
|
||||
const ORDER_BY = 'counter';
|
||||
const WS_PREFIX = 'ws';
|
||||
const API_ROOT = '/api/v2/';
|
||||
const Template = require('~features/output/index.view.html');
|
||||
|
||||
function resolveResource (
|
||||
$state,
|
||||
@ -42,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;
|
||||
@ -80,23 +78,16 @@ function resolveResource (
|
||||
}
|
||||
|
||||
const params = {
|
||||
page_size: PAGE_SIZE,
|
||||
order_by: ORDER_BY,
|
||||
};
|
||||
|
||||
const config = {
|
||||
params,
|
||||
pageCache: PAGE_CACHE,
|
||||
pageLimit: PAGE_LIMIT,
|
||||
page_size: OUTPUT_PAGE_SIZE,
|
||||
order_by: OUTPUT_ORDER_BY,
|
||||
};
|
||||
|
||||
if (job_event_search) { // eslint-disable-line camelcase
|
||||
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
||||
|
||||
Object.assign(config.params, query);
|
||||
Object.assign(params, query);
|
||||
}
|
||||
|
||||
Events.init(`${API_ROOT}${related}`, config.params);
|
||||
Events.init(`${API_ROOT}${related}`, params);
|
||||
|
||||
Wait('start');
|
||||
const promise = Promise.all([new Resource(['get', 'options'], [id, id]), Events.fetch()])
|
||||
|
||||
@ -7,56 +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 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>
|
||||
<div class="pull-right" ng-click="vm.menu.last()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
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="at-Stdout-container">
|
||||
<div class="at-Stdout-borderHeader"></div>
|
||||
<div id="atStdoutResultTable"></div>
|
||||
<div class="at-Stdout-borderFooter"></div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="pull-right" ng-click="vm.menu.last()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.first()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
|
||||
</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"
|
||||
ng-class=" { 'at-Stdout-menuIcon--active': vm.isFollowing }"></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 ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
|
||||
<div class="at-Stdout-menuIconGroup" ng-click="vm.menu.home()">
|
||||
<p class="pull-left"><i class="fa fa-angle-double-up"></i></p>
|
||||
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="at-u-clear"></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 = {
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const PAGE_LIMIT = 5;
|
||||
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 = {
|
||||
@ -150,7 +151,7 @@ function PageService ($q) {
|
||||
|
||||
const pageCount = this.state.head - this.state.tail;
|
||||
|
||||
if (pageCount >= PAGE_LIMIT) {
|
||||
if (pageCount >= OUTPUT_PAGE_LIMIT) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.popBack())
|
||||
.then(() => {
|
||||
@ -185,7 +186,7 @@ function PageService ($q) {
|
||||
|
||||
const pageCount = this.state.head - this.state.tail;
|
||||
|
||||
if (pageCount >= PAGE_LIMIT) {
|
||||
if (pageCount >= OUTPUT_PAGE_LIMIT) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.popFront())
|
||||
.then(() => {
|
||||
@ -235,8 +236,10 @@ function PageService ($q) {
|
||||
})
|
||||
.then(() => this.getNext());
|
||||
|
||||
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'];
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import Ansi from 'ansi-to-html';
|
||||
import Entities from 'html-entities';
|
||||
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||
import {
|
||||
EVENT_START_PLAY,
|
||||
EVENT_STATS_PLAY,
|
||||
EVENT_START_TASK,
|
||||
OUTPUT_ELEMENT_TBODY,
|
||||
} from './constants';
|
||||
|
||||
const EVENT_GROUPS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY
|
||||
EVENT_START_PLAY,
|
||||
];
|
||||
|
||||
const TIME_EVENTS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY,
|
||||
EVENT_STATS_PLAY
|
||||
EVENT_STATS_PLAY,
|
||||
];
|
||||
|
||||
const ansi = new Ansi();
|
||||
@ -33,7 +35,7 @@ function JobRenderService ($q, $sce, $window) {
|
||||
this.init = ({ compile, toggles }) => {
|
||||
this.parent = null;
|
||||
this.record = {};
|
||||
this.el = $(ELEMENT_TBODY);
|
||||
this.el = $(OUTPUT_ELEMENT_TBODY);
|
||||
this.hooks = { compile };
|
||||
|
||||
this.createToggles = toggles;
|
||||
@ -67,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 };
|
||||
}
|
||||
@ -125,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),
|
||||
};
|
||||
|
||||
@ -165,6 +172,8 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return info;
|
||||
};
|
||||
|
||||
this.getRecord = uuid => this.record[uuid];
|
||||
|
||||
this.deleteRecord = uuid => {
|
||||
delete this.record[uuid];
|
||||
};
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
const ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const DELAY = 100;
|
||||
const THRESHOLD = 0.1;
|
||||
import {
|
||||
OUTPUT_ELEMENT_CONTAINER,
|
||||
OUTPUT_ELEMENT_TBODY,
|
||||
OUTPUT_SCROLL_DELAY,
|
||||
OUTPUT_SCROLL_THRESHOLD,
|
||||
} from './constants';
|
||||
|
||||
const MAX_THRASH = 20;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = ({ next, previous }) => {
|
||||
this.el = $(ELEMENT_CONTAINER);
|
||||
this.init = ({ next, previous, onThresholdLeave }) => {
|
||||
this.el = $(OUTPUT_ELEMENT_CONTAINER);
|
||||
this.chain = $q.resolve();
|
||||
this.timer = null;
|
||||
|
||||
this.position = {
|
||||
@ -13,19 +18,43 @@ function JobScrollService ($q, $timeout) {
|
||||
current: 0
|
||||
};
|
||||
|
||||
this.threshold = {
|
||||
previous: 0,
|
||||
current: 0,
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
next,
|
||||
previous,
|
||||
isAtRest: () => $q.resolve()
|
||||
onThresholdLeave,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
hidden: false,
|
||||
paused: false,
|
||||
top: true,
|
||||
locked: false,
|
||||
hover: false,
|
||||
running: true,
|
||||
thrash: 0,
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
@ -33,77 +62,105 @@ 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);
|
||||
}
|
||||
|
||||
this.timer = $timeout(this.register, DELAY);
|
||||
this.timer = $timeout(this.register, OUTPUT_SCROLL_DELAY);
|
||||
};
|
||||
|
||||
this.register = () => {
|
||||
this.pause();
|
||||
const position = this.getScrollPosition();
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
|
||||
const current = this.getScrollPosition();
|
||||
const downward = current > this.position.previous;
|
||||
const threshold = position / viewport;
|
||||
const downward = position > this.position.previous;
|
||||
|
||||
let promise;
|
||||
const isBeyondUpperThreshold = threshold < OUTPUT_SCROLL_THRESHOLD;
|
||||
const isBeyondLowerThreshold = (1 - threshold) < OUTPUT_SCROLL_THRESHOLD;
|
||||
|
||||
if (downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.next;
|
||||
} else if (!downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.previous;
|
||||
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 || enteredUpperThreshold) {
|
||||
transitions.push(this.hooks.onThresholdLeave);
|
||||
transitions.push(this.hooks.previous);
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
this.setScrollPosition(current);
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
|
||||
return $q.resolve();
|
||||
if (leftLowerThreshold) {
|
||||
transitions.push(this.hooks.onThresholdLeave);
|
||||
}
|
||||
|
||||
return promise()
|
||||
if (threshold >= 1 || enteredLowerThreshold) {
|
||||
transitions.push(this.hooks.next);
|
||||
}
|
||||
|
||||
if (!downward) {
|
||||
transitions.reverse();
|
||||
}
|
||||
|
||||
this.position.current = position;
|
||||
this.threshold.current = threshold;
|
||||
|
||||
transitions.forEach(promise => {
|
||||
this.chain = this.chain.then(() => promise());
|
||||
});
|
||||
|
||||
return this.chain
|
||||
.then(() => {
|
||||
this.setScrollPosition(this.getScrollPosition());
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.isBeyondThreshold = (downward, current) => {
|
||||
const height = this.getScrollHeight();
|
||||
|
||||
if (downward) {
|
||||
current += this.getViewableHeight();
|
||||
|
||||
if (current >= height || ((height - current) / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
} else if (current <= 0 || (current / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move scroll position up by one page of visible content.
|
||||
*/
|
||||
this.moveUp = () => {
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
const position = this.getScrollPosition() - this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top - height);
|
||||
this.setScrollPosition(position);
|
||||
};
|
||||
|
||||
/**
|
||||
* Move scroll position down by one page of visible content.
|
||||
*/
|
||||
this.moveDown = () => {
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
const position = this.getScrollPosition() + this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top + height);
|
||||
this.setScrollPosition(position);
|
||||
};
|
||||
|
||||
this.getScrollHeight = () => this.el[0].scrollHeight;
|
||||
@ -117,43 +174,37 @@ function JobScrollService ($q, $timeout) {
|
||||
this.getScrollPosition = () => this.el[0].scrollTop;
|
||||
|
||||
this.setScrollPosition = position => {
|
||||
const viewport = this.getScrollHeight() - this.getViewableHeight();
|
||||
|
||||
this.position.previous = this.position.current;
|
||||
this.threshold.previous = this.position.previous / viewport;
|
||||
this.position.current = position;
|
||||
|
||||
this.el[0].scrollTop = position;
|
||||
this.isAtRest();
|
||||
};
|
||||
|
||||
this.resetScrollPosition = () => {
|
||||
this.threshold.previous = 0;
|
||||
this.position.previous = 0;
|
||||
this.position.current = 0;
|
||||
|
||||
this.el[0].scrollTop = 0;
|
||||
this.isAtRest();
|
||||
};
|
||||
|
||||
this.scrollToBottom = () => {
|
||||
this.setScrollPosition(this.getScrollHeight());
|
||||
};
|
||||
|
||||
this.isAtRest = () => {
|
||||
if (this.position.current === 0 && !this.state.top) {
|
||||
this.state.top = true;
|
||||
this.hooks.isAtRest(true);
|
||||
} else if (this.position.current > 0 && this.state.top) {
|
||||
this.state.top = false;
|
||||
this.hooks.isAtRest(false);
|
||||
}
|
||||
this.start = () => {
|
||||
this.state.running = true;
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
this.stop = () => {
|
||||
this.unlock();
|
||||
this.unhide();
|
||||
this.state.running = false;
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.isPaused = () => this.state.paused;
|
||||
|
||||
this.lock = () => {
|
||||
this.state.locked = true;
|
||||
};
|
||||
@ -162,22 +213,52 @@ function JobScrollService ($q, $timeout) {
|
||||
this.state.locked = false;
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
};
|
||||
|
||||
this.hide = () => {
|
||||
if (!this.state.hidden) {
|
||||
this.el.css('overflow', 'hidden');
|
||||
this.state.hidden = true;
|
||||
if (this.state.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.hidden = true;
|
||||
this.el.css('overflow-y', 'hidden');
|
||||
};
|
||||
|
||||
this.unhide = () => {
|
||||
if (this.state.hidden) {
|
||||
this.el.css('overflow', 'auto');
|
||||
this.state.hidden = false;
|
||||
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 = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
}
|
||||
|
||||
JobScrollService.$inject = ['$q', '$timeout'];
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
const templateUrl = require('~features/output/search.partial.html');
|
||||
import {
|
||||
OUTPUT_SEARCH_DOCLINK,
|
||||
OUTPUT_SEARCH_FIELDS,
|
||||
OUTPUT_SEARCH_KEY_EXAMPLES,
|
||||
} from './constants';
|
||||
|
||||
const searchKeyExamples = ['host_name:localhost', 'task:set', 'created:>=2000-01-01'];
|
||||
const searchKeyFields = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play'];
|
||||
const searchKeyDocLink = 'https://docs.ansible.com/ansible-tower/3.3.0/html/userguide/search_sort.html';
|
||||
const templateUrl = require('~features/output/search.partial.html');
|
||||
|
||||
let $state;
|
||||
let qs;
|
||||
@ -50,7 +52,7 @@ function reloadQueryset (queryset, rejection = strings.get('search.REJECT_DEFAUL
|
||||
|
||||
const isFilterable = term => {
|
||||
const field = term[0].split('.')[0].replace(/^-/, '');
|
||||
return (searchKeyFields.indexOf(field) > -1);
|
||||
return (OUTPUT_SEARCH_FIELDS.indexOf(field) > -1);
|
||||
};
|
||||
|
||||
function removeSearchTag (index) {
|
||||
@ -94,9 +96,9 @@ function JobSearchController (_$state_, _qs_, _strings_, { subscribe }) {
|
||||
vm = this || {};
|
||||
vm.strings = strings;
|
||||
|
||||
vm.examples = searchKeyExamples;
|
||||
vm.fields = searchKeyFields;
|
||||
vm.docLink = searchKeyDocLink;
|
||||
vm.examples = OUTPUT_SEARCH_KEY_EXAMPLES;
|
||||
vm.fields = OUTPUT_SEARCH_FIELDS;
|
||||
vm.docLink = OUTPUT_SEARCH_DOCLINK;
|
||||
vm.relatedFields = [];
|
||||
|
||||
vm.clearSearch = clearSearch;
|
||||
|
||||
@ -1,97 +1,57 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const PAGE_SIZE = 50;
|
||||
const PAGE_LIMIT = 5;
|
||||
const EVENT_LIMIT = PAGE_LIMIT * PAGE_SIZE;
|
||||
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 }) => {
|
||||
const { prepend, append, shift, pop, deleteRecord } = storage;
|
||||
const { getMaxCounter, getRange, getFirst, getLast } = api;
|
||||
const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage;
|
||||
const { getRange, getFirst, getLast, getMaxCounter } = api;
|
||||
|
||||
this.api = {
|
||||
getMaxCounter,
|
||||
getRange,
|
||||
getFirst,
|
||||
getLast,
|
||||
getMaxCounter,
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
clear,
|
||||
prepend,
|
||||
append,
|
||||
shift,
|
||||
pop,
|
||||
getRecord,
|
||||
deleteRecord,
|
||||
};
|
||||
|
||||
@ -99,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 => {
|
||||
@ -112,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();
|
||||
});
|
||||
@ -128,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();
|
||||
});
|
||||
@ -148,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();
|
||||
@ -177,190 +195,220 @@ 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;
|
||||
}
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.getNext = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||
const next = this.getNextRange(displacement);
|
||||
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();
|
||||
.then(() => this.api.getRange(next))
|
||||
.then(events => {
|
||||
const results = getContinuous(events);
|
||||
const min = Math.min(...results.map(({ counter }) => counter));
|
||||
|
||||
return $q.resolve();
|
||||
if (min > tail + 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
return $q.resolve(results);
|
||||
})
|
||||
.then(results => {
|
||||
const count = (tail - head + results.length);
|
||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
||||
|
||||
return this.popBack(excess)
|
||||
.then(() => {
|
||||
const popHeight = this.hooks.getScrollHeight();
|
||||
|
||||
return this.pushFront(results).then(() => $q.resolve(popHeight));
|
||||
});
|
||||
});
|
||||
|
||||
if (overlap && overlap[0] > 0) {
|
||||
const pushBackRange = [head - overlap[0], head];
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
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.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||
const previous = this.getPreviousRange(displacement);
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => $q.resolve(popHeight));
|
||||
.then(() => this.api.getRange(previous))
|
||||
.then(events => {
|
||||
const results = getContinuous(events, true);
|
||||
const max = Math.max(...results.map(({ counter }) => counter));
|
||||
|
||||
if (head > max + 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
return $q.resolve(results);
|
||||
})
|
||||
.then(results => {
|
||||
const count = (tail - head + results.length);
|
||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
||||
|
||||
return this.popFront(excess)
|
||||
.then(() => {
|
||||
const popHeight = this.hooks.getScrollHeight();
|
||||
|
||||
return this.pushBack(results).then(() => $q.resolve(popHeight));
|
||||
});
|
||||
});
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.getNext = (displacement = PAGE_SIZE) => {
|
||||
const [head, tail] = this.getRange();
|
||||
this.getFirst = () => {
|
||||
this.chain = this.chain
|
||||
.then(() => this.clear())
|
||||
.then(() => {
|
||||
if (this.cache.first) {
|
||||
return $q.resolve(this.cache.first);
|
||||
}
|
||||
|
||||
const tailRoom = this.getMaxCounter() - tail;
|
||||
const tailDisplacement = Math.min(tailRoom, displacement);
|
||||
return this.api.getFirst();
|
||||
})
|
||||
.then(events => {
|
||||
if (events.length === OUTPUT_PAGE_SIZE) {
|
||||
this.cache.first = events;
|
||||
}
|
||||
|
||||
const newTail = tail + tailDisplacement;
|
||||
return this.pushFront(events);
|
||||
});
|
||||
|
||||
let headDisplacement = 0;
|
||||
|
||||
if (newTail - head > EVENT_LIMIT) {
|
||||
headDisplacement = (newTail - EVENT_LIMIT) - head;
|
||||
}
|
||||
|
||||
return this.move([head + headDisplacement, tail + tailDisplacement]);
|
||||
return this.chain
|
||||
.then(() => this.getNext());
|
||||
};
|
||||
|
||||
this.getPrevious = (displacement = PAGE_SIZE) => {
|
||||
const [head, tail] = this.getRange();
|
||||
this.getLast = () => {
|
||||
this.chain = this.chain
|
||||
.then(() => this.getFrames())
|
||||
.then(frames => {
|
||||
if (frames.length > 0) {
|
||||
return $q.resolve(frames);
|
||||
}
|
||||
|
||||
const headRoom = head - 1;
|
||||
const headDisplacement = Math.min(headRoom, displacement);
|
||||
return this.api.getLast();
|
||||
})
|
||||
.then(events => {
|
||||
const min = Math.min(...events.map(({ counter }) => counter));
|
||||
|
||||
const newHead = head - headDisplacement;
|
||||
if (min <= this.getTailCounter() + 1) {
|
||||
return this.pushFront(events);
|
||||
}
|
||||
|
||||
let tailDisplacement = 0;
|
||||
return this.clear()
|
||||
.then(() => this.pushBack(events));
|
||||
});
|
||||
|
||||
if (tail - newHead > EVENT_LIMIT) {
|
||||
tailDisplacement = tail - (newHead + EVENT_LIMIT);
|
||||
}
|
||||
|
||||
return this.move([newHead, tail - tailDisplacement]);
|
||||
return this.chain
|
||||
.then(() => this.getPrevious());
|
||||
};
|
||||
|
||||
this.moveHead = displacement => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const headRoom = head - 1;
|
||||
const headDisplacement = Math.min(headRoom, displacement);
|
||||
|
||||
return this.move([head + headDisplacement, tail]);
|
||||
};
|
||||
|
||||
this.moveTail = displacement => {
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
const tailRoom = this.getMaxCounter() - tail;
|
||||
const tailDisplacement = Math.max(tailRoom, displacement);
|
||||
|
||||
return this.move([head, tail + tailDisplacement]);
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const count = this.getRecordCount();
|
||||
|
||||
if (count > 0) {
|
||||
this.chain = this.chain
|
||||
.then(() => this.popBack(count));
|
||||
}
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.getFirst = () => this.clear()
|
||||
.then(() => this.api.getFirst())
|
||||
.then(events => this.pushFront(events))
|
||||
.then(() => this.moveTail(PAGE_SIZE));
|
||||
|
||||
this.getLast = () => this.clear()
|
||||
.then(() => this.api.getLast())
|
||||
.then(events => this.pushBack(events))
|
||||
.then(() => this.moveHead(-PAGE_SIZE));
|
||||
|
||||
this.getTailCounter = () => {
|
||||
const tail = Math.max(...Object.keys(this.records));
|
||||
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 = () => {
|
||||
const counter = this.api.getMaxCounter();
|
||||
const tail = this.getTailCounter();
|
||||
if (this.buffer.min) {
|
||||
return this.buffer.min;
|
||||
}
|
||||
|
||||
return Number.isFinite(counter) ? Math.max(tail, counter) : tail;
|
||||
return this.api.getMaxCounter();
|
||||
};
|
||||
|
||||
this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE);
|
||||
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
|
||||
this.getRecordCount = () => Object.keys(this.records).length;
|
||||
this.getCapacity = () => EVENT_LIMIT - this.getRecordCount();
|
||||
this.getRecordCount = () => Object.keys(this.lines).length;
|
||||
this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount();
|
||||
}
|
||||
|
||||
SlidingWindowService.$inject = ['$q'];
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const JOB_START = 'playbook_on_start';
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
const PLAY_START = 'playbook_on_play_start';
|
||||
const TASK_START = 'playbook_on_task_start';
|
||||
|
||||
const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped'];
|
||||
const COMPLETE = ['successful', 'failed', 'unknown'];
|
||||
const INCOMPLETE = ['canceled', 'error'];
|
||||
const UNSUCCESSFUL = ['failed'].concat(INCOMPLETE);
|
||||
const FINISHED = COMPLETE.concat(INCOMPLETE);
|
||||
import {
|
||||
EVENT_START_PLAYBOOK,
|
||||
EVENT_STATS_PLAY,
|
||||
EVENT_START_PLAY,
|
||||
EVENT_START_TASK,
|
||||
HOST_STATUS_KEYS,
|
||||
JOB_STATUS_COMPLETE,
|
||||
JOB_STATUS_INCOMPLETE,
|
||||
JOB_STATUS_UNSUCCESSFUL,
|
||||
JOB_STATUS_FINISHED,
|
||||
} from './constants';
|
||||
|
||||
function JobStatusService (moment, message) {
|
||||
this.dispatch = () => message.dispatch('status', this.state);
|
||||
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');
|
||||
@ -43,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 {
|
||||
@ -50,23 +60,22 @@ 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 => {
|
||||
if (UNSUCCESSFUL.includes(status)) {
|
||||
if (JOB_STATUS_UNSUCCESSFUL.includes(status)) {
|
||||
return { failures: 1 };
|
||||
}
|
||||
|
||||
if (COMPLETE.includes(status)) {
|
||||
if (JOB_STATUS_COMPLETE.includes(status)) {
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
@ -92,7 +101,7 @@ function JobStatusService (moment, message) {
|
||||
|
||||
let changed = false;
|
||||
|
||||
if (!this.active && !(data.event === JOB_END)) {
|
||||
if (!this.active && !(data.event === EVENT_STATS_PLAY)) {
|
||||
this.active = true;
|
||||
this.setJobStatus('running');
|
||||
changed = true;
|
||||
@ -105,22 +114,22 @@ function JobStatusService (moment, message) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (data.event === JOB_START) {
|
||||
if (data.event === EVENT_START_PLAYBOOK) {
|
||||
this.setStarted(this.state.started || data.created);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (data.event === PLAY_START) {
|
||||
if (data.event === EVENT_START_PLAY) {
|
||||
this.state.counts.plays++;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (data.event === TASK_START) {
|
||||
if (data.event === EVENT_START_TASK) {
|
||||
this.state.counts.tasks++;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (data.event === JOB_END) {
|
||||
if (data.event === EVENT_STATS_PLAY) {
|
||||
this.setStatsEvent(data);
|
||||
changed = true;
|
||||
}
|
||||
@ -193,17 +202,20 @@ function JobStatusService (moment, message) {
|
||||
|
||||
this.setJobStatus = status => {
|
||||
const isExpectingStats = this.isExpectingStatsEvent();
|
||||
const isIncomplete = INCOMPLETE.includes(status);
|
||||
const isFinished = FINISHED.includes(status);
|
||||
const isAlreadyFinished = FINISHED.includes(this.state.status);
|
||||
const isIncomplete = JOB_STATUS_INCOMPLETE.includes(status);
|
||||
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'));
|
||||
@ -216,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();
|
||||
};
|
||||
@ -233,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;
|
||||
};
|
||||
|
||||
@ -266,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 = [
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const PAGE_SIZE = 50;
|
||||
const MAX_LAG = 120;
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
import {
|
||||
EVENT_STATS_PLAY,
|
||||
OUTPUT_MAX_LAG,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
function OutputStream ($q) {
|
||||
this.init = ({ bufferAdd, bufferEmpty, onFrames, onStop }) => {
|
||||
@ -22,13 +24,13 @@ function OutputStream ($q) {
|
||||
|
||||
this.state = {
|
||||
ending: false,
|
||||
ended: false
|
||||
ended: false,
|
||||
};
|
||||
|
||||
this.lag = 0;
|
||||
this.chain = $q.resolve();
|
||||
|
||||
this.factors = this.calcFactors(PAGE_SIZE);
|
||||
this.factors = this.calcFactors(OUTPUT_PAGE_SIZE);
|
||||
this.setFramesPerRender();
|
||||
};
|
||||
|
||||
@ -47,7 +49,7 @@ function OutputStream ($q) {
|
||||
};
|
||||
|
||||
this.setFramesPerRender = () => {
|
||||
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
|
||||
const index = Math.floor((this.lag / OUTPUT_MAX_LAG) * this.factors.length);
|
||||
const boundedIndex = Math.min(this.factors.length - 1, index);
|
||||
|
||||
this.framesPerRender = this.factors[boundedIndex];
|
||||
@ -96,7 +98,7 @@ function OutputStream ($q) {
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
if (data.event === JOB_END) {
|
||||
if (data.event === EVENT_STATS_PLAY) {
|
||||
this.state.ending = true;
|
||||
this.counters.final = data.counter;
|
||||
}
|
||||
@ -104,7 +106,7 @@ function OutputStream ($q) {
|
||||
const [minReady, maxReady] = this.updateCounterState(data);
|
||||
const count = this.hooks.bufferAdd(data);
|
||||
|
||||
if (count % PAGE_SIZE === 0) {
|
||||
if (count % OUTPUT_PAGE_SIZE === 0) {
|
||||
this.setFramesPerRender();
|
||||
}
|
||||
|
||||
@ -158,6 +160,8 @@ function OutputStream ($q) {
|
||||
this.counters.ready.length = 0;
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.getMaxCounter = () => this.counters.max;
|
||||
}
|
||||
|
||||
OutputStream.$inject = ['$q'];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user