Merge pull request #2306 from jakemcdermott/job-results/static-pager

improve job output search
This commit is contained in:
Jake McDermott 2018-06-26 13:12:21 -04:00 committed by GitHub
commit e07eba266d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 386 additions and 74 deletions

View File

@ -14,10 +14,22 @@ function JobEventsApiService ($http, $q) {
this.endpoint = endpoint;
this.params = merge(BASE_PARAMS, params);
this.state = { current: 0, count: 0 };
this.state = { count: 0, maxCounter: 0 };
this.cache = {};
};
this.fetch = () => this.getLast().then(() => this);
this.clearCache = () => {
Object.keys(this.cache).forEach(key => {
delete this.cache[key];
});
};
this.fetch = () => this.getLast()
.then(results => {
this.cache.last = results;
return this;
});
this.getFirst = () => {
const page = 1;
@ -26,14 +38,72 @@ 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;
this.state.current = page;
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return results;
});
};
this.getPage = number => {
if (number < 1 || number > this.getLastPageNumber()) {
return $q.resolve([]);
}
const params = merge(this.params, { page: number });
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;
}
return results;
});
};
this.getLast = () => {
if (this.cache.last) {
return $q.resolve(this.cache.last);
}
const params = merge(this.params, { page: 1, order_by: `-${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 (results.length > 0) {
rotated = results;
}
}
this.state.count = count;
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return rotated;
});
};
this.getRange = range => {
if (!range) {
return $q.resolve([]);
@ -47,46 +117,18 @@ function JobEventsApiService ($http, $q) {
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results } = data;
const maxCounter = Math.max(results.map(({ counter }) => counter));
const maxCounter = Math.max(...results.map(({ counter }) => counter));
this.state.current = Math.ceil(maxCounter / PAGE_SIZE);
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return results;
});
};
this.getLast = () => {
const params = merge(this.params, { page: 1, order_by: `-${ORDER_BY}` });
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results } = data;
const count = Math.max(...results.map(({ counter }) => counter));
let rotated = results;
if (count > PAGE_SIZE) {
rotated = results.splice(count % PAGE_SIZE);
if (results.length > 0) {
rotated = results;
}
}
this.state.count = count;
this.state.current = Math.ceil(count / PAGE_SIZE);
return rotated;
});
};
this.getCurrentPageNumber = () => this.state.current;
this.getLastPageNumber = () => Math.ceil(this.state.count / PAGE_SIZE);
this.getPreviousPageNumber = () => Math.max(1, this.state.current - 1);
this.getNextPageNumber = () => Math.min(this.state.current + 1, this.getLastPageNumber());
this.getMaxCounter = () => this.state.count;
this.getNext = () => this.getPage(this.getNextPageNumber());
this.getPrevious = () => this.getPage(this.getPreviousPageNumber());
this.getMaxCounter = () => this.state.maxCounter;
}
JobEventsApiService.$inject = ['$http', '$q'];

View File

@ -92,7 +92,7 @@ function first () {
}
function next () {
return slide.slideDown();
return slide.getNext();
}
function previous () {
@ -100,7 +100,7 @@ function previous () {
const initialPosition = scroll.getScrollPosition();
return slide.slideUp()
return slide.getPrevious()
.then(popHeight => {
const currentHeight = scroll.getScrollHeight();
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
@ -255,6 +255,7 @@ function OutputIndexController (
_$state_,
_resource_,
_scroll_,
_page_,
_render_,
_status_,
_slide_,
@ -263,6 +264,8 @@ function OutputIndexController (
strings,
$stateParams,
) {
const { isPanelExpanded } = $stateParams;
$compile = _$compile_;
$q = _$q_;
$scope = _$scope_;
@ -271,9 +274,9 @@ function OutputIndexController (
resource = _resource_;
scroll = _scroll_;
render = _render_;
slide = _slide_;
status = _status_;
stream = _stream_;
slide = resource.model.get('event_processing_finished') ? _page_ : _slide_;
vm = this || {};
@ -281,8 +284,6 @@ function OutputIndexController (
vm.title = $filter('sanitize')(resource.model.get('name'));
vm.strings = strings;
vm.resource = resource;
const { isPanelExpanded } = $stateParams;
vm.reloadState = reloadState;
vm.isPanelExpanded = isPanelExpanded;
vm.togglePanelExpand = togglePanelExpand;
@ -334,6 +335,7 @@ OutputIndexController.$inject = [
'$state',
'resource',
'OutputScrollService',
'OutputPageService',
'OutputRenderService',
'OutputStatusService',
'OutputSlideService',

View File

@ -9,6 +9,7 @@ import StreamService from '~features/output/stream.service';
import StatusService from '~features/output/status.service';
import MessageService from '~features/output/message.service';
import EventsApiService from '~features/output/api.events.service';
import PageService from '~features/output/page.service';
import SlideService from '~features/output/slide.service';
import LegacyRedirect from '~features/output/legacy.route';
@ -108,11 +109,6 @@ function resolveResource (
status: `${WS_PREFIX}-${name}`,
summary: `${WS_PREFIX}-${name}-summary`,
},
page: {
cache: PAGE_CACHE,
size: PAGE_SIZE,
pageLimit: PAGE_LIMIT
}
}));
if (!handleErrors) {
@ -250,6 +246,7 @@ angular
.service('OutputStatusService', StatusService)
.service('OutputMessageService', MessageService)
.service('JobEventsApiService', EventsApiService)
.service('OutputPageService', PageService)
.service('OutputSlideService', SlideService)
.component('atJobSearch', SearchComponent)
.component('atJobStats', StatsComponent)

View File

@ -0,0 +1,239 @@
/* eslint camelcase: 0 */
const PAGE_LIMIT = 5;
function PageService ($q) {
this.init = (storage, api, { getScrollHeight }) => {
const { prepend, append, shift, pop, deleteRecord } = storage;
const { getPage, getFirst, getLast, getLastPageNumber } = api;
this.api = {
getPage,
getFirst,
getLast,
getLastPageNumber,
};
this.storage = {
prepend,
append,
shift,
pop,
deleteRecord,
};
this.hooks = {
getScrollHeight,
};
this.records = {};
this.uuids = {};
this.state = {
head: 0,
tail: 0,
};
this.chain = $q.resolve();
};
this.pushFront = results => {
if (!results) {
return $q.resolve();
}
return this.storage.append(results)
.then(() => {
this.records[++this.state.tail] = {};
results.forEach(({ counter, start_line, end_line, uuid }) => {
this.records[this.state.tail][counter] = { start_line, end_line };
this.uuids[counter] = uuid;
});
return $q.resolve();
});
};
this.pushBack = results => {
if (!results) {
return $q.resolve();
}
return this.storage.prepend(results)
.then(() => {
this.records[--this.state.head] = {};
results.forEach(({ counter, start_line, end_line, uuid }) => {
this.records[this.state.head][counter] = { start_line, end_line };
this.uuids[counter] = uuid;
});
return $q.resolve();
});
};
this.popBack = () => {
if (this.getRecordCount() === 0) {
return $q.resolve();
}
const pageRecord = this.records[this.state.head] || {};
let lines = 0;
const counters = [];
Object.keys(pageRecord)
.forEach(counter => {
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
counters.push(counter);
});
return this.storage.shift(lines)
.then(() => {
counters.forEach(counter => {
this.storage.deleteRecord(this.uuids[counter]);
delete this.uuids[counter];
});
delete this.records[this.state.head++];
return $q.resolve();
});
};
this.popFront = () => {
if (this.getRecordCount() === 0) {
return $q.resolve();
}
const pageRecord = this.records[this.state.tail] || {};
let lines = 0;
const counters = [];
Object.keys(pageRecord)
.forEach(counter => {
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
counters.push(counter);
});
return this.storage.pop(lines)
.then(() => {
counters.forEach(counter => {
this.storage.deleteRecord(this.uuids[counter]);
delete this.uuids[counter];
});
delete this.records[this.state.tail--];
return $q.resolve();
});
};
this.getNext = () => {
const lastPageNumber = this.api.getLastPageNumber();
const number = Math.min(this.state.tail + 1, lastPageNumber);
const isLoaded = (number >= this.state.head && number <= this.state.tail);
const isValid = (number >= 1 && number <= lastPageNumber);
let popHeight = this.hooks.getScrollHeight();
if (!isValid || isLoaded) {
this.chain = this.chain
.then(() => $q.resolve(popHeight));
return this.chain;
}
const pageCount = this.state.head - this.state.tail;
if (pageCount >= PAGE_LIMIT) {
this.chain = this.chain
.then(() => this.popBack())
.then(() => {
popHeight = this.hooks.getScrollHeight();
return $q.resolve();
});
}
this.chain = this.chain
.then(() => this.api.getPage(number))
.then(events => this.pushFront(events))
.then(() => $q.resolve(popHeight));
return this.chain;
};
this.getPrevious = () => {
const number = Math.max(this.state.head - 1, 1);
const isLoaded = (number >= this.state.head && number <= this.state.tail);
const isValid = (number >= 1 && number <= this.api.getLastPageNumber());
let popHeight = this.hooks.getScrollHeight();
if (!isValid || isLoaded) {
this.chain = this.chain
.then(() => $q.resolve(popHeight));
return this.chain;
}
const pageCount = this.state.head - this.state.tail;
if (pageCount >= PAGE_LIMIT) {
this.chain = this.chain
.then(() => this.popFront())
.then(() => {
popHeight = this.hooks.getScrollHeight();
return $q.resolve();
});
}
this.chain = this.chain
.then(() => this.api.getPage(number))
.then(events => this.pushBack(events))
.then(() => $q.resolve(popHeight));
return this.chain;
};
this.clear = () => {
const count = this.getRecordCount();
for (let i = 0; i <= count; ++i) {
this.chain = this.chain.then(() => this.popBack());
}
return this.chain;
};
this.getLast = () => this.clear()
.then(() => this.api.getLast())
.then(events => {
const lastPage = this.api.getLastPageNumber();
this.state.head = lastPage;
this.state.tail = lastPage;
return this.pushBack(events);
})
.then(() => this.getPrevious());
this.getFirst = () => this.clear()
.then(() => this.api.getFirst())
.then(events => {
this.state.head = 1;
this.state.tail = 1;
return this.pushBack(events);
})
.then(() => this.getNext());
this.getRecordCount = () => Object.keys(this.records).length;
this.getTailCounter = () => this.state.tail;
}
PageService.$inject = ['$q'];
export default PageService;

View File

@ -77,7 +77,6 @@ function submitSearch () {
}
const searchInputQueryset = qs.getSearchInputQueryset(vm.value, isFilterable);
const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset);
reloadQueryset(modifiedQueryset, strings.get('search.REJECT_INVALID'));

View File

@ -58,6 +58,23 @@ function getOverlapArray (range, other) {
return [range[0] - other[0], other[1] - range[1]];
}
/**
* 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])];
}
function SlidingWindowService ($q) {
this.init = (storage, api, { getScrollHeight }) => {
const { prepend, append, shift, pop, deleteRecord } = storage;
@ -67,7 +84,7 @@ function SlidingWindowService ($q) {
getMaxCounter,
getRange,
getFirst,
getLast
getLast,
};
this.storage = {
@ -85,6 +102,8 @@ function SlidingWindowService ($q) {
this.records = {};
this.uuids = {};
this.chain = $q.resolve();
api.clearCache();
};
this.pushFront = events => {
@ -176,47 +195,54 @@ function SlidingWindowService ($q) {
});
};
this.getBoundedRange = ([low, high]) => {
const bounds = [1, this.getMaxCounter()];
return [Math.max(low, bounds[0]), Math.min(high, bounds[1])];
};
this.move = ([low, high]) => {
const [head, tail] = this.getRange();
const [newHead, newTail] = this.getBoundedRange([low, high]);
const bounds = [1, this.getMaxCounter()];
const [newHead, newTail] = getBoundedRange([low, high], bounds);
let popHeight = this.hooks.getScrollHeight();
if (newHead > newTail) {
return $q.resolve([0, 0]);
this.chain = this.chain
.then(() => $q.resolve(popHeight));
return this.chain;
}
if (!Number.isFinite(newHead) || !Number.isFinite(newTail)) {
return $q.resolve([0, 0]);
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.popBack(this.getRecordCount()))
.then(() => this.clear())
.then(() => this.api.getRange([newHead, newTail]))
.then(events => this.pushFront(events));
}
if (overlap && overlap[0] < 0) {
this.chain = this.chain.then(() => this.popBack(Math.abs(overlap[0])));
const popBackCount = Math.abs(overlap[0]);
this.chain = this.chain.then(() => this.popBack(popBackCount));
}
if (overlap && overlap[1] < 0) {
this.chain = this.chain.then(() => this.popFront(Math.abs(overlap[1])));
const popFrontCount = Math.abs(overlap[1]);
this.chain = this.chain.then(() => this.popFront(popFrontCount));
}
let popHeight;
this.chain = this.chain.then(() => {
popHeight = this.hooks.getScrollHeight();
this.chain = this.chain
.then(() => {
popHeight = this.hooks.getScrollHeight();
return $q.resolve();
});
return $q.resolve();
});
if (overlap && overlap[0] > 0) {
const pushBackRange = [head - overlap[0], head];
@ -240,7 +266,7 @@ function SlidingWindowService ($q) {
return this.chain;
};
this.slideDown = (displacement = PAGE_SIZE) => {
this.getNext = (displacement = PAGE_SIZE) => {
const [head, tail] = this.getRange();
const tailRoom = this.getMaxCounter() - tail;
@ -257,7 +283,7 @@ function SlidingWindowService ($q) {
return this.move([head + headDisplacement, tail + tailDisplacement]);
};
this.slideUp = (displacement = PAGE_SIZE) => {
this.getPrevious = (displacement = PAGE_SIZE) => {
const [head, tail] = this.getRange();
const headRoom = head - 1;

View File

@ -23,6 +23,13 @@ function atRelaunchCtrl (
const jobObj = new Job();
const jobTemplate = new JobTemplate();
const transitionOptions = { reload: true };
if ($state.includes('output')) {
transitionOptions.inherit = false;
transitionOptions.location = 'replace';
}
const updateTooltip = () => {
if (vm.job.type === 'job' && vm.job.status === 'failed') {
vm.tooltip = strings.get('relaunch.HOSTS');
@ -128,7 +135,7 @@ function atRelaunchCtrl (
.then((launchRes) => {
if (!$state.is('jobs')) {
const relaunchType = launchRes.data.type === 'job' ? 'playbook' : launchRes.data.type;
$state.go('output', { id: launchRes.data.id, type: relaunchType }, { reload: true });
$state.go('output', { id: launchRes.data.id, type: relaunchType }, transitionOptions);
}
}).catch(({ data, status, config }) => {
ProcessErrors($scope, data, status, null, {
@ -173,7 +180,7 @@ function atRelaunchCtrl (
inventorySource.postUpdate(vm.job.inventory_source)
.then((postUpdateRes) => {
if (!$state.is('jobs')) {
$state.go('output', { id: postUpdateRes.data.id, type: 'inventory' }, { reload: true });
$state.go('output', { id: postUpdateRes.data.id, type: 'inventory' }, transitionOptions);
}
}).catch(({ data, status, config }) => {
ProcessErrors($scope, data, status, null, {
@ -197,7 +204,7 @@ function atRelaunchCtrl (
project.postUpdate(vm.job.project)
.then((postUpdateRes) => {
if (!$state.is('jobs')) {
$state.go('output', { id: postUpdateRes.data.id, type: 'project' }, { reload: true });
$state.go('output', { id: postUpdateRes.data.id, type: 'project' }, transitionOptions);
}
}).catch(({ data, status, config }) => {
ProcessErrors($scope, data, status, null, {
@ -219,7 +226,7 @@ function atRelaunchCtrl (
id: vm.job.id
}).then((launchRes) => {
if (!$state.is('jobs')) {
$state.go('workflowResults', { id: launchRes.data.id }, { reload: true });
$state.go('workflowResults', { id: launchRes.data.id }, transitionOptions);
}
}).catch(({ data, status, config }) => {
ProcessErrors($scope, data, status, null, {
@ -243,7 +250,7 @@ function atRelaunchCtrl (
id: vm.job.id
}).then((launchRes) => {
if (!$state.is('jobs')) {
$state.go('output', { id: launchRes.data.id, type: 'command' }, { reload: true });
$state.go('output', { id: launchRes.data.id, type: 'command' }, transitionOptions);
}
}).catch(({ data, status, config }) => {
ProcessErrors($scope, data, status, null, {
@ -268,7 +275,7 @@ function atRelaunchCtrl (
relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData)
}).then((launchRes) => {
if (!$state.is('jobs')) {
$state.go('output', { id: launchRes.data.job, type: 'playbook' }, { reload: true });
$state.go('output', { id: launchRes.data.job, type: 'playbook' }, transitionOptions);
}
}).catch(({ data, status }) => {
ProcessErrors($scope, data, status, null, {