import Ansi from 'ansi-to-html'; import Entities from 'html-entities'; import { EVENT_START_PLAY, EVENT_STATS_PLAY, EVENT_START_TASK, OUTPUT_ANSI_COLORMAP, OUTPUT_ELEMENT_TBODY, OUTPUT_EVENT_LIMIT, } from './constants'; const EVENT_GROUPS = [ EVENT_START_TASK, EVENT_START_PLAY, ]; const TIME_EVENTS = [ EVENT_START_TASK, EVENT_START_PLAY, EVENT_STATS_PLAY, ]; const ansi = new Ansi({ stream: true, colors: OUTPUT_ANSI_COLORMAP }); const entities = new Entities.AllHtmlEntities(); // https://github.com/chalk/ansi-regex const pattern = [ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)', '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))' ].join('|'); const re = new RegExp(pattern); const hasAnsi = input => re.test(input); let $scope; function JobRenderService ($q, $compile, $sce, $window) { this.init = (_$scope_, { toggles }) => { $scope = _$scope_; this.setScope(); this.el = $(OUTPUT_ELEMENT_TBODY); this.parent = null; this.state = { head: 0, tail: 0, collapseAll: false, toggleMode: toggles, }; this.records = {}; this.uuids = {}; }; this.setCollapseAll = value => { this.state.collapseAll = value; Object.keys(this.records).forEach(key => { this.records[key].isCollapsed = value; }); }; this.sortByCounter = (a, b) => { if (a.counter > b.counter) { return 1; } if (a.counter < b.counter) { return -1; } return 0; }; // // Event Data Transformation / HTML Building // this.appendEventGroup = events => { let lines = 0; let html = ''; events.sort(this.sortByCounter); for (let i = 0; i <= events.length - 1; i++) { const current = events[i]; if (this.state.tail && current.counter !== this.state.tail + 1) { const missing = this.appendMissingEventGroup(current); html += missing.html; lines += missing.count; } const eventLines = this.transformEvent(current); html += eventLines.html; lines += eventLines.count; } return { html, lines }; }; this.appendMissingEventGroup = event => { const tailUUID = this.uuids[this.state.tail]; const tailRecord = this.records[tailUUID]; if (!tailRecord) { return { html: '', count: 0 }; } let uuid; if (tailRecord.isMissing) { uuid = tailUUID; } else { uuid = `${event.counter}-${tailUUID}`; this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true }; } for (let i = this.state.tail + 1; i < event.counter; i++) { this.records[uuid].counters.push(i); this.uuids[i] = uuid; } if (tailRecord.isMissing) { return { html: '', count: 0 }; } if (tailRecord.end === event.start_line) { return { html: '', count: 0 }; } const html = this.buildRowHTML(this.records[uuid]); const count = 1; return { html, count }; }; this.prependEventGroup = events => { let lines = 0; let html = ''; events.sort(this.sortByCounter); for (let i = events.length - 1; i >= 0; i--) { const current = events[i]; if (this.state.head && current.counter !== this.state.head - 1) { const missing = this.prependMissingEventGroup(current); html = missing.html + html; lines += missing.count; } const eventLines = this.transformEvent(current); html = eventLines.html + html; lines += eventLines.count; } return { html, lines }; }; this.prependMissingEventGroup = event => { const headUUID = this.uuids[this.state.head]; const headRecord = this.records[headUUID]; if (!headRecord) { return { html: '', count: 0 }; } let uuid; if (headRecord.isMissing) { uuid = headUUID; } else { uuid = `${headUUID}-${event.counter}`; this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true }; } for (let i = this.state.head - 1; i > event.counter; i--) { this.records[uuid].counters.unshift(i); this.uuids[i] = uuid; } if (headRecord.isMissing) { return { html: '', count: 0 }; } if (event.end_line === headRecord.start) { return { html: '', count: 0 }; } const html = this.buildRowHTML(this.records[uuid]); const count = 1; return { html, count }; }; this.transformEvent = event => { if (!event || event.stdout === null || event.stdout === undefined) { return { html: '', count: 0 }; } if (event.uuid && this.records[event.uuid] && !this.records[event.uuid]._isIncomplete) { return { html: '', count: 0 }; } const stdout = this.sanitize(event.stdout); const lines = stdout.split('\r\n'); const record = this.createRecord(event, lines); if (lines.length === 1 && lines[0] === '') { return { html: '', count: 0 }; } let html = ''; let count = lines.length; let ln = event.start_line; for (let i = 0; i <= lines.length - 1; i++) { ln++; const line = lines[i]; const isLastLine = i === lines.length - 1; let row = this.buildRowHTML(record, ln, line); if (record && record.isTruncated && isLastLine) { row += this.buildRowHTML(record); count++; } html += row; } if (this.records[event.uuid]) { this.records[event.uuid].lineCount = count; } return { html, count }; }; this.createRecord = (event, lines) => { if (!event.counter) { return null; } if (!this.state.head || event.counter < this.state.head) { this.state.head = event.counter; } if (!this.state.tail || event.counter > this.state.tail) { this.state.tail = event.counter; } if (!event.uuid) { this.uuids[event.counter] = event.counter; this.records[event.counter] = { counters: [event.counter], lineCount: lines.length }; return this.records[event.counter]; } let isClickable = false; if (typeof event.host === 'number' || event.event_data && event.event_data.res) { isClickable = true; } else if (event.type === 'project_update_event' && event.event !== 'runner_on_skipped' && event.event_data.host) { isClickable = true; } const children = (this.records[event.uuid] && this.records[event.uuid].children) ? this.records[event.uuid].children : []; const record = { isClickable, id: event.id, line: event.start_line + 1, name: event.event, uuid: event.uuid, level: event.event_level, start: event.start_line, end: event.end_line, isTruncated: (event.end_line - event.start_line) > lines.length, lineCount: lines.length, isCollapsed: this.state.collapseAll, counters: [event.counter], children }; if (event.parent_uuid) { record.parents = this.getParentEvents(event.parent_uuid); if (this.records[event.parent_uuid]) { record.isCollapsed = this.records[event.parent_uuid].isCollapsed; } } if (record.isTruncated) { record.truncatedAt = event.start_line + lines.length; } if (EVENT_GROUPS.includes(event.event)) { record.isParent = true; if (event.event_level === 1) { this.parent = event.uuid; } if (event.parent_uuid) { if (this.records[event.parent_uuid]) { if (this.records[event.parent_uuid].children) { if (!this.records[event.parent_uuid].children.includes(event.uuid)) { this.records[event.parent_uuid].children.push(event.uuid); } } else { this.records[event.parent_uuid].children = [event.uuid]; } } else { this.records[event.parent_uuid] = { _isIncomplete: true, children: [event.uuid] }; } } } if (TIME_EVENTS.includes(event.event)) { record.time = this.getTimestamp(event.created); record.line++; } this.records[event.uuid] = record; this.uuids[event.counter] = event.uuid; return record; }; this.getParentEvents = (uuid, list) => { list = list || []; // always push its parent if exists list.push(uuid); // if we can get grandparent in current visible lines, we also push it if (this.records[uuid] && this.records[uuid].parents) { list = list.concat(this.records[uuid].parents); } return list; }; this.buildRowHTML = (record, ln, content) => { let id = ''; let icon = ''; let timestamp = ''; let tdToggle = ''; let tdEvent = ''; let classList = ''; let directives = ''; if (record.isMissing) { return `