From ab61675c2d0f304830b7d5c6c1732fcd67230b27 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 7 Dec 2020 08:27:53 -0500 Subject: [PATCH] Add system for strict-csp stdout html generation --- .../src/screens/Job/JobOutput/JobEvent.jsx | 77 +----- .../src/screens/Job/JobOutput/JobOutput.jsx | 236 +++++++++++++----- 2 files changed, 183 insertions(+), 130 deletions(-) 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 && ( )} -
+ ); } }