diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx
index 59908f695c..29f02e8245 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx
@@ -1,6 +1,3 @@
-import Ansi from 'ansi-to-html';
-import hasAnsi from 'has-ansi';
-import { AllHtmlEntities } from 'html-entities';
import React from 'react';
import {
JobEventLine,
@@ -9,84 +6,18 @@ import {
JobEventLineText,
} from './shared';
-const EVENT_START_TASK = 'playbook_on_task_start';
-const EVENT_START_PLAY = 'playbook_on_play_start';
-const EVENT_STATS_PLAY = 'playbook_on_stats';
-const TIME_EVENTS = [EVENT_START_TASK, EVENT_START_PLAY, EVENT_STATS_PLAY];
-
-const ansi = new Ansi({
- stream: true,
- colors: {
- 0: '#000',
- 1: '#A30000',
- 2: '#486B00',
- 3: '#795600',
- 4: '#00A',
- 5: '#A0A',
- 6: '#004368',
- 7: '#AAA',
- 8: '#555',
- 9: '#F55',
- 10: '#5F5',
- 11: '#FF5',
- 12: '#55F',
- 13: '#F5F',
- 14: '#5FF',
- 15: '#FFF',
- },
-});
-const entities = new AllHtmlEntities();
-
-function getTimestamp({ created }) {
- const date = new Date(created);
-
- const dateHours = date.getHours();
- const dateMinutes = date.getMinutes();
- const dateSeconds = date.getSeconds();
-
- const stampHours = dateHours < 10 ? `0${dateHours}` : dateHours;
- const stampMinutes = dateMinutes < 10 ? `0${dateMinutes}` : dateMinutes;
- const stampSeconds = dateSeconds < 10 ? `0${dateSeconds}` : dateSeconds;
-
- return `${stampHours}:${stampMinutes}:${stampSeconds}`;
-}
-
-function getLineTextHtml({ created, event, start_line, stdout }) {
- const sanitized = entities.encode(stdout);
- return sanitized.split('\r\n').map((lineText, index) => {
- let html;
- if (hasAnsi(lineText)) {
- html = ansi.toHtml(lineText);
- } else {
- html = lineText;
- }
-
- if (index === 1 && TIME_EVENTS.includes(event)) {
- const time = getTimestamp({ created });
- html += `${time}`;
- }
-
- return {
- lineNumber: start_line + index,
- html,
- };
- });
-}
-
function JobEvent({
counter,
- created,
- event,
- isClickable,
- onJobEventClick,
stdout,
- start_line,
style,
type,
+ lineTextHtml,
+ isClickable,
+ onJobEventClick,
}) {
return !stdout ? null : (
- {getLineTextHtml({ created, event, start_line, stdout }).map(
+ {lineTextHtml.map(
({ lineNumber, html }) =>
lineNumber >= 0 && (
{
+ let html;
+ if (hasAnsi(lineText)) {
+ const { cssMap, result } = replaceStyleAttrs(ansi.toHtml(lineText));
+ html = result;
+ lineCssMap = { ...lineCssMap, ...cssMap };
+ } else {
+ html = lineText;
+ }
+
+ if (index === 1 && TIME_EVENTS.includes(event)) {
+ const time = getTimestamp({ created });
+ html += `${time}`;
+ }
+
+ lineTextHtml.push({
+ lineNumber: start_line + index,
+ html,
+ });
+ });
+
+ return {
+ lineCssMap,
+ lineTextHtml,
+ };
+}
+
const HeaderTitle = styled.div`
display: inline-flex;
align-items: center;
@@ -54,6 +157,8 @@ const OutputWrapper = styled.div`
font-size: 15px;
height: calc(100vh - 350px);
outline: 1px solid #d7d7d7;
+ ${({ cssMap }) =>
+ Object.keys(cssMap).map(className => `.${className}{${cssMap[className]}}`)}
`;
const OutputFooter = styled.div`
@@ -122,6 +227,7 @@ class JobOutput extends Component {
remoteRowCount: 0,
isHostModalOpen: false,
hostEvent: {},
+ cssMap: {},
};
this.cache = new CellMeasurerCache({
@@ -164,7 +270,7 @@ class JobOutput extends Component {
componentDidUpdate(prevProps, prevState) {
// recompute row heights for any job events that have transitioned
// from loading to loaded
- const { currentlyLoading } = this.state;
+ const { currentlyLoading, cssMap } = this.state;
let shouldRecomputeRowHeights = false;
prevState.currentlyLoading
.filter(n => !currentlyLoading.includes(n))
@@ -172,6 +278,9 @@ class JobOutput extends Component {
shouldRecomputeRowHeights = true;
this.cache.clear(n);
});
+ if (Object.keys(cssMap).length !== Object.keys(prevState.cssMap).length) {
+ shouldRecomputeRowHeights = true;
+ }
if (shouldRecomputeRowHeights) {
if (this.listRef.recomputeRowHeights) {
this.listRef.recomputeRowHeights();
@@ -300,6 +409,13 @@ class JobOutput extends Component {
return isHost;
};
+ let actualLineTextHtml = [];
+ if (results[index]) {
+ const { lineTextHtml, lineCssMap } = getLineTextHtml(results[index]);
+ this.setState(({ cssMap }) => ({ cssMap: { ...cssMap, ...lineCssMap } }));
+ actualLineTextHtml = lineTextHtml;
+ }
+
return (
this.handleHostEventClick(results[index])}
className="row"
style={style}
+ lineTextHtml={actualLineTextHtml}
{...results[index]}
/>
) : (
@@ -389,7 +506,9 @@ class JobOutput extends Component {
handleResize({ width }) {
if (width !== this._previousWidth) {
this.cache.clearAll();
- this.listRef.recomputeRowHeights();
+ if (this.listRef) {
+ this.listRef.recomputeRowHeights();
+ }
}
this._previousWidth = width;
}
@@ -404,6 +523,7 @@ class JobOutput extends Component {
hostEvent,
isHostModalOpen,
remoteRowCount,
+ cssMap,
} = this.state;
if (hasContentLoading) {
@@ -415,60 +535,62 @@ class JobOutput extends Component {
}
return (
-
- {isHostModalOpen && (
-
+
+ {isHostModalOpen && (
+
+ )}
+
+
+
+ {job.name}
+
+
+
+
+
- )}
-
-
-
- {job.name}
-
-
-
-
-
-
-
- {({ onRowsRendered, registerChild }) => (
-
- {({ width, height }) => {
- return (
- {
- this.listRef = ref;
- registerChild(ref);
- }}
- deferredMeasurementCache={this.cache}
- height={height || 1}
- onRowsRendered={onRowsRendered}
- rowCount={remoteRowCount}
- rowHeight={this.cache.rowHeight}
- rowRenderer={this.rowRenderer}
- scrollToAlignment="start"
- width={width || 1}
- overscanRowCount={20}
- />
- );
- }}
-
- )}
-
-
-
+
+
+ {({ onRowsRendered, registerChild }) => (
+
+ {({ width, height }) => {
+ return (
+ {
+ this.listRef = ref;
+ registerChild(ref);
+ }}
+ deferredMeasurementCache={this.cache}
+ height={height || 1}
+ onRowsRendered={onRowsRendered}
+ rowCount={remoteRowCount}
+ rowHeight={this.cache.rowHeight}
+ rowRenderer={this.rowRenderer}
+ scrollToAlignment="start"
+ width={width || 1}
+ overscanRowCount={20}
+ />
+ );
+ }}
+
+ )}
+
+
+
+
{deletionError && (
)}
-
+
);
}
}