import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; let vm; let ansi; let resource; let related; let container; let $timeout; let $sce; let $compile; let $scope; let $q; const record = {}; const meta = { scroll: {}, page: {} }; const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; const SCROLL_LOAD_DELAY = 250; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; const ELEMENT_TBODY = '#atStdoutResultTable'; const ELEMENT_CONTAINER = '.at-Stdout-container'; const EVENT_GROUPS = [ EVENT_START_TASK, EVENT_START_PLAY ]; const TIME_EVENTS = [ EVENT_START_TASK, EVENT_START_PLAY, EVENT_STATS_PLAY ]; function JobsIndexController ( _resource_, _$sce_, _$timeout_, _$scope_, _$compile_, _$q_ ) { $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; $q = _$q_; resource = _resource_; ansi = new Ansi(); related = getRelated(); const events = resource.get(`related.${related}.results`); const parsed = parseEvents(events); const html = $sce.trustAsHtml(parsed.html); vm = this || {}; $scope.ns = 'jobs'; $scope.jobs = { modal: {} }; vm.toggle = toggle; vm.showHostDetails = showHostDetails; vm.menu = { scroll: { display: false, to: scrollTo }, top: { expand, isExpanded: true }, bottom: { next } }; meta.page.cache = [{ page: 1, lines: parsed.lines }]; $timeout(() => { const table = $(ELEMENT_TBODY); container = $(ELEMENT_CONTAINER); table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); container.scroll(onScroll); }); } function getRelated () { const name = resource.constructor.name; switch (name) { case 'ProjectUpdateModel': return 'events'; case 'JobModel': return 'job_events'; } } function next () { const config = { related, page: meta.page.cache[meta.page.cache.length - 1].page + 1, params: { order_by: 'start_line' } }; console.log('[2] getting next page', config.page, meta.page.cache); return resource.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } meta.page.cache.push({ page: data.page }); return shift() .then(() => append(data.results)); }); } function prev () { const config = { related, page: meta.page.cache[0].page - 1, params: { order_by: 'start_line' } }; console.log('[2] getting previous page', config.page, meta.page.cache); return resource.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } meta.page.cache.unshift({ page: data.page }); return pop() .then(() => prepend(data.results)); }); } function getRowCount () { return $(ELEMENT_TBODY).children().length; } function getRowHeight () { return $(ELEMENT_TBODY).children()[0].offsetHeight; } function getViewHeight () { return $(ELEMENT_CONTAINER)[0].offsetHeight; } function getScrollPosition () { return $(ELEMENT_CONTAINER)[0].scrollTop; } function getScrollHeight () { return $(ELEMENT_CONTAINER)[0].scrollHeight; } function getRowsAbove () { const top = getScrollPosition(); if (top === 0) { return 0; } return Math.floor(top / getRowHeight()); } function getRowsBelow () { const bottom = getScrollPosition() + getViewHeight(); return Math.floor((getScrollHeight() - bottom) / getRowHeight()); } function getRowsInView () { const rowHeight = getRowHeight(); const viewHeight = getViewHeight(); return Math.floor(viewHeight / rowHeight); } function append (events) { console.log('[4] appending next page'); return $q(resolve => { const parsed = parseEvents(events); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); const index = meta.page.cache.length - 1; meta.page.cache[index].lines = parsed.lines; table.append(rows); $compile(rows.contents())($scope); $timeout(() => { resolve(); }); }); } function prepend (events) { console.log('[4] prepending next page'); return $q(resolve => { const parsed = parseEvents(events); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); meta.page.cache[0].lines = parsed.lines; table.prepend(rows); $compile(rows.contents())($scope); $timeout(() => { resolve(); }); }); } function pop () { console.log('[3] popping old page'); return $q(resolve => { if (meta.page.cache.length <= PAGE_LIMIT) { console.log('[3.1] nothing to pop'); return resolve(); } const ejected = meta.page.cache.pop(); console.log('[3.1] popping', ejected); const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); rows.empty(); rows.remove(); $timeout(() => { return resolve(); }); }); } function shift () { console.log('[3] shifting old page'); return $q(resolve => { if (meta.page.cache.length <= PAGE_LIMIT) { console.log('[3.1] nothing to shift'); return resolve(); } const ejected = meta.page.cache.shift(); console.log('[3.1] shifting', ejected); const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); rows.empty(); rows.remove(); $timeout(() => { return resolve(); }); }); } function expand () { vm.toggle(meta.parent, true); } function scrollTo (direction) { if (direction === 'top') { container[0].scrollTop = 0; } else { container[0].scrollTop = container[0].scrollHeight; } } function parseEvents (events) { let lines = 0; let html = ''; events.sort(orderByLineNumber); events.forEach(event => { const line = parseLine(event); html += line.html; lines += line.count; }); return { html, lines }; } function orderByLineNumber (a, b) { if (a.start_line > b.start_line) { return 1; } if (a.start_line < b.start_line) { return -1; } return 0; } function parseLine (event) { if (!event || !event.stdout) { return { html: '', count: 0 }; } const { stdout } = event; const lines = stdout.split('\r\n'); let count = lines.length; let ln = event.start_line; const current = createRecord(ln, lines, event); const html = lines.reduce((html, line, i) => { ln++; const isLastLine = i === lines.length - 1; let row = createRow(current, ln, line); if (current && current.isTruncated && isLastLine) { row += createRow(current); count++; } return `${html}${row}`; }, ''); return { html, count }; } function createRecord (ln, lines, event) { if (!event.uuid) { return null; } const info = { id: event.id, line: ln + 1, uuid: event.uuid, level: event.event_level, start: event.start_line, end: event.end_line, isTruncated: (event.end_line - event.start_line) > lines.length, isHost: typeof event.host === 'number' }; if (event.parent_uuid) { info.parents = getParentEvents(event.parent_uuid); } if (info.isTruncated) { info.truncatedAt = event.start_line + lines.length; } if (EVENT_GROUPS.includes(event.event)) { info.isParent = true; if (event.event_level === 1) { meta.parent = event.uuid; } if (event.parent_uuid) { if (record[event.parent_uuid]) { if (record[event.parent_uuid].children && !record[event.parent_uuid].children.includes(event.uuid)) { record[event.parent_uuid].children.push(event.uuid); } else { record[event.parent_uuid].children = [event.uuid]; } } } } if (TIME_EVENTS.includes(event.event)) { info.time = getTime(event.created); info.line++; } record[event.uuid] = info; return info; } function getParentEvents (uuid, list) { list = list || []; if (record[uuid]) { list.push(uuid); if (record[uuid].parents) { list = list.concat(record[uuid].parents); } } return list; } function createRow (current, ln, content) { let id = ''; let timestamp = ''; let tdToggle = ''; let tdEvent = ''; let classList = ''; content = content || ''; if (hasAnsi(content)) { content = ansi.toHtml(content); } if (current) { if (current.isParent && current.line === ln) { id = current.uuid; tdToggle = `