diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx index 63d7b80c1c..ab4be12950 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx @@ -98,11 +98,11 @@ function getTimestamp({ created }) { const dateMinutes = date.getMinutes(); const dateSeconds = date.getSeconds(); - const stampHour = dateHours < 10 ? `0${dateHours}` : dateHours; - const stampMinute = dateMinutes < 10 ? `0${dateMinutes}` : dateMinutes; - const stampSecond = dateSeconds < 10 ? `0${dateSeconds}` : dateSeconds; + const stampHours = dateHours < 10 ? `0${dateHours}` : dateHours; + const stampMinutes = dateMinutes < 10 ? `0${dateMinutes}` : dateMinutes; + const stampSeconds = dateSeconds < 10 ? `0${dateSeconds}` : dateSeconds; - return `${stampHour}:${stampMinute}:${stampSecond}`; + return `${stampHours}:${stampMinutes}:${stampSeconds}`; } function getLineTextHtml({ created, event, start_line, stdout }) { @@ -127,13 +127,16 @@ function getLineTextHtml({ created, event, start_line, stdout }) { }); } -function JobEvent({ created, event, stdout, start_line, ...rest }) { +function JobEvent({ counter, created, event, stdout, start_line, ...rest }) { return !stdout ? null : ( {getLineTextHtml({ created, event, start_line, stdout }).map( ({ lineNumber, html }) => - lineNumber > 0 && ( - + lineNumber !== 0 && ( + {lineNumber} i { + cursor: pointer; + } + + user-select: none; +`; +const JobEventSkeletonLineNumber = styled.div` + color: #161b1f; + background-color: #ebebeb; + flex: 0 0 45px; + text-align: right; + vertical-align: top; + padding-right: 5px; + border-right: 1px solid #b7b7b7; + user-select: none; +`; +const JobEventSkeletonContentWrapper = styled.div` + padding: 0 15px; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; + + .content { + background: var(--pf-global--disabled-color--200); + background: linear-gradient( + to right, + #f5f5f5 10%, + #e8e8e8 18%, + #f5f5f5 33% + ); + border-radius: 5px; + } +`; + +function JobEventSkeletonContent({ contentLength }) { + return ( + + {' '.repeat(contentLength)} + + ); +} + +function JobEventSkeleton({ counter, contentLength, ...rest }) { + return ( + counter > 1 && ( + + + + + + + + ) + ); +} + +export default JobEventSkeleton; diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobEventSkeleton.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobEventSkeleton.test.jsx new file mode 100644 index 0000000000..acf17ed168 --- /dev/null +++ b/awx/ui_next/src/screens/Job/JobOutput/JobEventSkeleton.test.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; + +import JobEventSkeleton from './JobEventSkeleton'; + +const contentSelector = 'JobEventSkeletonContent'; + +describe('', () => { + test('initially renders successfully', () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find(contentSelector).length).toEqual(1); + }); + + test('always skips first counter', () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find(contentSelector).length).toEqual(0); + }); +}); diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx index 4d5f425485..4a3ff30661 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx @@ -14,6 +14,7 @@ import { JobsAPI } from '@api'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import JobEvent from './JobEvent'; +import JobEventSkeleton from './JobEventSkeleton'; import MenuControls from './shared/MenuControls'; const OutputToolbar = styled.div` @@ -37,9 +38,15 @@ const OutputFooter = styled.div` flex: 1; `; -class JobOutput extends Component { - listRef = React.createRef(); +function range(low, high) { + const numbers = []; + for (let n = low; n <= high; n++) { + numbers.push(n); + } + return numbers; +} +class JobOutput extends Component { constructor(props) { super(props); @@ -47,10 +54,7 @@ class JobOutput extends Component { contentError: null, hasContentLoading: true, results: {}, - scrollToIndex: -1, - loadedRowCount: 0, - loadedRowsMap: {}, - loadingRowCount: 0, + currentlyLoading: [], remoteRowCount: 0, }; @@ -74,10 +78,32 @@ class JobOutput extends Component { this.loadJobEvents(); } + componentDidUpdate(prevProps, prevState) { + // recompute row heights for any job events that have transitioned + // from loading to loaded + const { currentlyLoading } = this.state; + let shouldRecomputeRowHeights = false; + prevState.currentlyLoading + .filter(n => !currentlyLoading.includes(n)) + .forEach(n => { + shouldRecomputeRowHeights = true; + this.cache.clear(n); + }); + if (shouldRecomputeRowHeights) { + this.listRef.recomputeRowHeights(); + } + } + + listRef = React.createRef(); + async loadJobEvents() { const { job } = this.props; - this.setState({ hasContentLoading: true }); + const loadRange = range(1, 50); + this.setState(({ currentlyLoading }) => ({ + hasContentLoading: true, + currentlyLoading: currentlyLoading.concat(loadRange), + })); try { const { data: { results: newResults = [], count }, @@ -85,7 +111,6 @@ class JobOutput extends Component { page_size: 50, order_by: 'start_line', }); - this.setState(({ results }) => { newResults.forEach(jobEvent => { results[jobEvent.counter] = jobEvent; @@ -95,21 +120,23 @@ class JobOutput extends Component { } catch (err) { this.setState({ contentError: err }); } finally { - this.setState({ hasContentLoading: false }); + this.setState(({ currentlyLoading }) => ({ + hasContentLoading: false, + currentlyLoading: currentlyLoading.filter(n => !loadRange.includes(n)), + })); } } isRowLoaded({ index }) { - const { results } = this.state; - return !!results[index]; + const { results, currentlyLoading } = this.state; + if (results[index]) { + return true; + } + return currentlyLoading.includes(index); } rowRenderer({ index, parent, key, style }) { const { results } = this.state; - if (!results[index]) { - return; - } - const { created, event, stdout, start_line } = results[index]; return ( - + {results[index] ? ( + + ) : ( + + )} ); } - async loadMoreRows({ startIndex, stopIndex }) { + loadMoreRows({ startIndex, stopIndex }) { const { job } = this.props; - let params = { + const loadRange = range(startIndex, stopIndex); + this.setState(({ currentlyLoading }) => ({ + currentlyLoading: currentlyLoading.concat(loadRange), + })); + const params = { counter__gte: startIndex, counter__lte: stopIndex, order_by: 'start_line', }; - - return await JobsAPI.readEvents(job.id, job.type, params).then(response => { - this.setState(({ results }) => { + return JobsAPI.readEvents(job.id, job.type, params).then(response => { + this.setState(({ results, currentlyLoading }) => { response.data.results.forEach(jobEvent => { results[jobEvent.counter] = jobEvent; }); - return { results }; + return { + results, + currentlyLoading: currentlyLoading.filter( + n => !loadRange.includes(n) + ), + }; }); }); } @@ -152,8 +189,8 @@ class JobOutput extends Component { handleScrollPrevious() { const startIndex = this.listRef.Grid._renderedRowStartIndex; const stopIndex = this.listRef.Grid._renderedRowStopIndex; - const range = stopIndex - startIndex + 1; - this.listRef.scrollToRow(Math.max(0, startIndex - range)); + const scrollRange = stopIndex - startIndex + 1; + this.listRef.scrollToRow(Math.max(0, startIndex - scrollRange)); } handleScrollNext() { @@ -168,7 +205,6 @@ class JobOutput extends Component { handleScrollBottom() { const { remoteRowCount } = this.state; this.listRef.scrollToRow(remoteRowCount - 1); - this.setState({ scrollToIndex: remoteRowCount - 1 }); } handleResize({ width }) { @@ -181,12 +217,7 @@ class JobOutput extends Component { render() { const { job } = this.props; - const { - hasContentLoading, - contentError, - scrollToIndex, - remoteRowCount, - } = this.state; + const { hasContentLoading, contentError, remoteRowCount } = this.state; if (hasContentLoading) { return ; @@ -229,8 +260,8 @@ class JobOutput extends Component { rowHeight={this.cache.rowHeight} rowRenderer={this.rowRenderer} scrollToAlignment="start" - scrollToIndex={scrollToIndex} width={width} + overscanRowCount={20} /> ); }}