mirror of
https://github.com/ansible/awx.git
synced 2026-01-29 07:14:43 -03:30
303 lines
8.0 KiB
JavaScript
303 lines
8.0 KiB
JavaScript
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';
|
|
|
|
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();
|
|
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);
|
|
|
|
function JobRenderService ($q, $sce, $window) {
|
|
this.init = ({ compile, apply, isStreamActive }) => {
|
|
this.parent = null;
|
|
this.record = {};
|
|
this.el = $(ELEMENT_TBODY);
|
|
this.hooks = { isStreamActive, compile, apply };
|
|
};
|
|
|
|
this.sortByLineNumber = (a, b) => {
|
|
if (a.start_line > b.start_line) {
|
|
return 1;
|
|
}
|
|
|
|
if (a.start_line < b.start_line) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
this.transformEventGroup = events => {
|
|
let lines = 0;
|
|
let html = '';
|
|
|
|
events.sort(this.sortByLineNumber);
|
|
|
|
events.forEach(event => {
|
|
const line = this.transformEvent(event);
|
|
|
|
html += line.html;
|
|
lines += line.count;
|
|
});
|
|
|
|
return { html, lines };
|
|
};
|
|
|
|
this.transformEvent = event => {
|
|
if (!event || !event.stdout) {
|
|
return { html: '', count: 0 };
|
|
}
|
|
|
|
const stdout = this.sanitize(event.stdout);
|
|
const lines = stdout.split('\r\n');
|
|
|
|
let count = lines.length;
|
|
let ln = event.start_line;
|
|
|
|
const current = this.createRecord(ln, lines, event);
|
|
|
|
const html = lines.reduce((concat, line, i) => {
|
|
ln++;
|
|
|
|
const isLastLine = i === lines.length - 1;
|
|
|
|
let row = this.createRow(current, ln, line);
|
|
|
|
if (current && current.isTruncated && isLastLine) {
|
|
row += this.createRow(current);
|
|
count++;
|
|
}
|
|
|
|
return `${concat}${row}`;
|
|
}, '');
|
|
|
|
return { html, count };
|
|
};
|
|
|
|
this.isHostEvent = (event) => {
|
|
if (typeof event.host === 'number') {
|
|
return true;
|
|
}
|
|
|
|
if (event.type === 'project_update_event' &&
|
|
event.event !== 'runner_on_skipped' &&
|
|
event.event_data.host) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
this.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: this.isHostEvent(event),
|
|
};
|
|
|
|
if (event.parent_uuid) {
|
|
info.parents = this.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) {
|
|
this.parent = event.uuid;
|
|
}
|
|
|
|
if (event.parent_uuid) {
|
|
if (this.record[event.parent_uuid]) {
|
|
if (this.record[event.parent_uuid].children &&
|
|
!this.record[event.parent_uuid].children.includes(event.uuid)) {
|
|
this.record[event.parent_uuid].children.push(event.uuid);
|
|
} else {
|
|
this.record[event.parent_uuid].children = [event.uuid];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TIME_EVENTS.includes(event.event)) {
|
|
info.time = this.getTimestamp(event.created);
|
|
info.line++;
|
|
}
|
|
|
|
this.record[event.uuid] = info;
|
|
|
|
return info;
|
|
};
|
|
|
|
this.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 (!this.hooks.isStreamActive() && current.isParent && current.line === ln) {
|
|
id = current.uuid;
|
|
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
|
|
}
|
|
|
|
if (current.isHost) {
|
|
tdEvent = `<td class="at-Stdout-event--host" ui-sref="jobz.host-event.json({eventId: ${current.id}, taskUuid: '${current.uuid}' })"><span ng-non-bindable>${content}</span></td>`;
|
|
}
|
|
|
|
if (current.time && current.line === ln) {
|
|
timestamp = `<span>${current.time}</span>`;
|
|
}
|
|
|
|
if (current.parents) {
|
|
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
|
}
|
|
}
|
|
|
|
if (!tdEvent) {
|
|
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
|
|
}
|
|
|
|
if (!tdToggle) {
|
|
tdToggle = '<td class="at-Stdout-toggle"></td>';
|
|
}
|
|
|
|
if (!ln) {
|
|
ln = '...';
|
|
}
|
|
|
|
return `
|
|
<tr id="${id}" class="${classList}">
|
|
${tdToggle}
|
|
<td class="at-Stdout-line">${ln}</td>
|
|
${tdEvent}
|
|
<td class="at-Stdout-time">${timestamp}</td>
|
|
</tr>`;
|
|
};
|
|
|
|
this.getTimestamp = created => {
|
|
const date = new Date(created);
|
|
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
|
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
|
|
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
|
|
|
|
return `${hour}:${minute}:${second}`;
|
|
};
|
|
|
|
this.getParentEvents = (uuid, list) => {
|
|
list = list || [];
|
|
|
|
if (this.record[uuid]) {
|
|
list.push(uuid);
|
|
|
|
if (this.record[uuid].parents) {
|
|
list = list.concat(this.record[uuid].parents);
|
|
}
|
|
}
|
|
|
|
return list;
|
|
};
|
|
|
|
this.getEvents = () => this.hooks.get();
|
|
|
|
this.insert = (events, insert) => {
|
|
const result = this.transformEventGroup(events);
|
|
const html = this.trustHtml(result.html);
|
|
|
|
return this.requestAnimationFrame(() => insert(html))
|
|
.then(() => this.compile(html))
|
|
.then(() => result.lines);
|
|
};
|
|
|
|
this.remove = elements => this.requestAnimationFrame(() => {
|
|
elements.remove();
|
|
});
|
|
|
|
this.requestAnimationFrame = fn => $q(resolve => {
|
|
$window.requestAnimationFrame(() => {
|
|
if (fn) {
|
|
fn();
|
|
}
|
|
|
|
return resolve();
|
|
});
|
|
});
|
|
|
|
this.compile = html => {
|
|
html = $(this.el);
|
|
this.hooks.compile(html);
|
|
|
|
return this.requestAnimationFrame();
|
|
};
|
|
|
|
this.clear = () => {
|
|
const elements = this.el.children();
|
|
return this.remove(elements);
|
|
};
|
|
|
|
this.shift = lines => {
|
|
const elements = this.el.children().slice(0, lines);
|
|
|
|
return this.remove(elements);
|
|
};
|
|
|
|
this.pop = lines => {
|
|
const elements = this.el.children().slice(-lines);
|
|
|
|
return this.remove(elements);
|
|
};
|
|
|
|
this.prepend = events => this.insert(events, html => this.el.prepend(html));
|
|
|
|
this.append = events => this.insert(events, html => this.el.append(html));
|
|
|
|
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
|
|
|
this.sanitize = html => entities.encode(html);
|
|
}
|
|
|
|
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
|
|
|
export default JobRenderService;
|