From 5473e54219e8983ef5f62f3cb144ebaafde74fcc Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Thu, 12 Aug 2021 11:19:03 -0700 Subject: [PATCH 1/5] JobOutput: extract JobOutputSearch bar --- awx/ui/src/screens/Job/JobOutput/JobOutput.js | 177 ++--------------- .../screens/Job/JobOutput/JobOutputSearch.js | 188 ++++++++++++++++++ .../src/screens/Job/JobOutput/PageControls.js | 9 +- 3 files changed, 203 insertions(+), 171 deletions(-) create mode 100644 awx/ui/src/screens/Job/JobOutput/JobOutputSearch.js diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 47f4fddd3f..f59d53a429 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -12,39 +12,25 @@ import { import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; import { encode } from 'html-entities'; -import { - Button, - Toolbar, - ToolbarContent, - ToolbarItem, - ToolbarToggleGroup, - Tooltip, -} from '@patternfly/react-core'; -import { SearchIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; import AlertModal from 'components/AlertModal'; import { CardBody as _CardBody } from 'components/Card'; import ContentError from 'components/ContentError'; import ContentLoading from 'components/ContentLoading'; import ErrorDetail from 'components/ErrorDetail'; -import Search from 'components/Search'; import StatusIcon from 'components/StatusIcon'; import { getJobModel, isJobRunning } from 'util/jobs'; import useRequest, { useDismissableError } from 'hooks/useRequest'; import useInterval from 'hooks/useInterval'; -import { - parseQueryString, - mergeParams, - removeParams, - getQSConfig, - updateQueryString, -} from 'util/qs'; +import { parseQueryString, getQSConfig } from 'util/qs'; import useIsMounted from 'hooks/useIsMounted'; import JobEvent from './JobEvent'; import JobEventSkeleton from './JobEventSkeleton'; import PageControls from './PageControls'; import HostEventModal from './HostEventModal'; +import JobOutputSearch from './JobOutputSearch'; import { HostStatusBar, OutputToolbar } from './shared'; import getRowRangePageSize from './shared/jobOutputUtils'; @@ -192,15 +178,6 @@ const OutputFooter = styled.div` flex: 1; `; -const SearchToolbar = styled(Toolbar)` - position: inherit !important; -`; - -const SearchToolbarContent = styled(ToolbarContent)` - padding-left: 0px !important; - padding-right: 0px !important; -`; - let ws; function connectJobSocket({ type, id }, onMessage) { ws = new WebSocket( @@ -665,55 +642,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { previousWidth.current = width; }; - const handleSearch = (key, value) => { - const params = parseQueryString(QS_CONFIG, location.search); - const qs = updateQueryString( - QS_CONFIG, - location.search, - mergeParams(params, { [key]: value }) - ); - pushHistoryState(qs); - }; - - const handleReplaceSearch = (key, value) => { - const qs = updateQueryString(QS_CONFIG, location.search, { - [key]: value, - }); - pushHistoryState(qs); - }; - - const handleRemoveSearchTerm = (key, value) => { - const oldParams = parseQueryString(QS_CONFIG, location.search); - const updatedParams = removeParams(QS_CONFIG, oldParams, { - [key]: value, - }); - const qs = updateQueryString(QS_CONFIG, location.search, updatedParams); - pushHistoryState(qs); - }; - - const handleRemoveAllSearchTerms = () => { - const oldParams = parseQueryString(QS_CONFIG, location.search); - Object.keys(oldParams).forEach((key) => { - oldParams[key] = null; - }); - const qs = updateQueryString(QS_CONFIG, location.search, oldParams); - pushHistoryState(qs); - }; - - const pushHistoryState = (qs) => { - const { pathname } = history.location; - history.push(qs ? `${pathname}?${qs}` : pathname); - }; - - const handleFollowToggle = () => { - if (isFollowModeEnabled) { - setIsFollowModeEnabled(false); - } else { - setIsFollowModeEnabled(true); - scrollToRow(remoteRowCount - 1); - } - }; - const handleScroll = (e) => { if ( isFollowModeEnabled && @@ -726,64 +654,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { scrollHeight.current = e.scrollHeight; }; - const renderSearchComponent = () => ( - {}} - onRemove={handleRemoveSearchTerm} - isDisabled={isJobRunning(job.status)} - /> - ); - if (contentError) { return ; } @@ -812,36 +682,16 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { /> - - - } breakpoint="lg"> - - {isJobRunning(job.status) ? ( - - {renderSearchComponent()} - - ) : ( - renderSearchComponent() - )} - - - {isJobRunning(job.status) ? ( - - ) : null} - - + { + const params = parseQueryString(qsConfig, location.search); + const qs = updateQueryString( + qsConfig, + location.search, + mergeParams(params, { [key]: value }) + ); + pushHistoryState(qs); + }; + + const handleReplaceSearch = (key, value) => { + const qs = updateQueryString(qsConfig, location.search, { + [key]: value, + }); + pushHistoryState(qs); + }; + + const handleRemoveSearchTerm = (key, value) => { + const oldParams = parseQueryString(qsConfig, location.search); + const updatedParams = removeParams(qsConfig, oldParams, { + [key]: value, + }); + const qs = updateQueryString(qsConfig, location.search, updatedParams); + pushHistoryState(qs); + }; + + const handleRemoveAllSearchTerms = () => { + const oldParams = parseQueryString(qsConfig, location.search); + Object.keys(oldParams).forEach((key) => { + oldParams[key] = null; + }); + const qs = updateQueryString(qsConfig, location.search, oldParams); + pushHistoryState(qs); + }; + + const pushHistoryState = (qs) => { + const { pathname } = history.location; + history.push(qs ? `${pathname}?${qs}` : pathname); + }; + + const handleFollowToggle = () => { + if (isFollowModeEnabled) { + setIsFollowModeEnabled(false); + } else { + setIsFollowModeEnabled(true); + scrollToRow(remoteRowCount - 1); + } + }; + + const columns = [ + { + name: t`Stdout`, + key: 'stdout__icontains', + isDefault: true, + }, + { + name: t`Event`, + key: 'event', + options: [ + ['runner_on_failed', t`Host Failed`], + ['runner_on_start', t`Host Started`], + ['runner_on_ok', t`Host OK`], + ['runner_on_error', t`Host Failure`], + ['runner_on_skipped', t`Host Skipped`], + ['runner_on_unreachable', t`Host Unreachable`], + ['runner_on_no_hosts', t`No Hosts Remaining`], + ['runner_on_async_poll', t`Host Polling`], + ['runner_on_async_ok', t`Host Async OK`], + ['runner_on_async_failed', t`Host Async Failure`], + ['runner_item_on_ok', t`Item OK`], + ['runner_item_on_failed', t`Item Failed`], + ['runner_item_on_skipped', t`Item Skipped`], + ['runner_retry', t`Host Retry`], + ['runner_on_file_diff', t`File Difference`], + ['playbook_on_start', t`Playbook Started`], + ['playbook_on_notify', t`Running Handlers`], + ['playbook_on_include', t`Including File`], + ['playbook_on_no_hosts_matched', t`No Hosts Matched`], + ['playbook_on_no_hosts_remaining', t`No Hosts Remaining`], + ['playbook_on_task_start', t`Task Started`], + ['playbook_on_vars_prompt', t`Variables Prompted`], + ['playbook_on_setup', t`Gathering Facts`], + ['playbook_on_play_start', t`Play Started`], + ['playbook_on_stats', t`Playbook Complete`], + ['debug', t`Debug`], + ['verbose', t`Verbose`], + ['deprecated', t`Deprecated`], + ['warning', t`Warning`], + ['system_warning', t`System Warning`], + ['error', t`Error`], + ], + }, + { name: t`Advanced`, key: 'advanced' }, + ]; + const isDisabled = isJobRunning(job.status); + + return ( + + + } breakpoint="lg"> + + {isDisabled ? ( + + {}} + onRemove={handleRemoveSearchTerm} + isDisabled + /> + + ) : ( + {}} + onRemove={handleRemoveSearchTerm} + /> + )} + + + {isJobRunning(job.status) ? ( + + ) : null} + + + ); +} + +export default JobOutputSearch; diff --git a/awx/ui/src/screens/Job/JobOutput/PageControls.js b/awx/ui/src/screens/Job/JobOutput/PageControls.js index 70d6ab07a2..025859f54f 100644 --- a/awx/ui/src/screens/Job/JobOutput/PageControls.js +++ b/awx/ui/src/screens/Job/JobOutput/PageControls.js @@ -2,7 +2,7 @@ import 'styled-components/macro'; import React from 'react'; import { t } from '@lingui/macro'; -import { Button as PFButton } from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; import { AngleDoubleUpIcon, AngleDoubleDownIcon, @@ -14,16 +14,11 @@ import styled from 'styled-components'; const Wrapper = styled.div` display: flex; height: 35px; - outline: 1px solid #d7d7d7; + border: 1px solid #d7d7d7; width: 100%; justify-content: flex-end; `; -const Button = styled(PFButton)` - position: relative; - z-index: 1; -`; - const PageControls = ({ onScrollFirst, onScrollLast, From 949c2b92af3368df8360b2dabcbba905868ab87d Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Thu, 12 Aug 2021 14:30:35 -0700 Subject: [PATCH 2/5] JobOutput: extract helper funcs into separate file --- awx/ui/src/screens/Job/JobOutput/JobOutput.js | 103 +---------------- .../screens/Job/JobOutput/getLineTextHtml.js | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+), 102 deletions(-) create mode 100644 awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index f59d53a429..5d4e06807c 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -9,9 +9,6 @@ import { InfiniteLoader, List, } from 'react-virtualized'; -import Ansi from 'ansi-to-html'; -import hasAnsi from 'has-ansi'; -import { encode } from 'html-entities'; import { Button } from '@patternfly/react-core'; import AlertModal from 'components/AlertModal'; @@ -33,110 +30,12 @@ import HostEventModal from './HostEventModal'; import JobOutputSearch from './JobOutputSearch'; import { HostStatusBar, OutputToolbar } from './shared'; import getRowRangePageSize from './shared/jobOutputUtils'; +import getLineTextHtml from './getLineTextHtml'; const QS_CONFIG = getQSConfig('job_output', { order_by: 'counter', }); -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', - }, -}); - -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}`; -} - -const styleAttrPattern = new RegExp('style="[^"]*"', 'g'); - -function createStyleAttrHash(styleAttr) { - let hash = 0; - for (let i = 0; i < styleAttr.length; i++) { - hash = (hash << 5) - hash; // eslint-disable-line no-bitwise - hash += styleAttr.charCodeAt(i); - hash &= hash; // eslint-disable-line no-bitwise - } - return `${hash}`; -} - -function replaceStyleAttrs(html) { - const allStyleAttrs = [...new Set(html.match(styleAttrPattern))]; - const cssMap = {}; - let result = html; - for (let i = 0; i < allStyleAttrs.length; i++) { - const styleAttr = allStyleAttrs[i]; - const cssClassName = `output-${createStyleAttrHash(styleAttr)}`; - - cssMap[cssClassName] = styleAttr.replace('style="', '').slice(0, -1); - result = result.split(styleAttr).join(`class="${cssClassName}"`); - } - return { cssMap, result }; -} - -function getLineTextHtml({ created, event, start_line, stdout }) { - const sanitized = encode(stdout); - let lineCssMap = {}; - const lineTextHtml = []; - - sanitized.split('\r\n').forEach((lineText, index) => { - 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 CardBody = styled(_CardBody)` display: flex; flex-flow: column; diff --git a/awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js b/awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js new file mode 100644 index 0000000000..1ac82bd0c4 --- /dev/null +++ b/awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js @@ -0,0 +1,107 @@ +import Ansi from 'ansi-to-html'; +import hasAnsi from 'has-ansi'; +import { encode } from 'html-entities'; + +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', + }, +}); + +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 createStyleAttrHash(styleAttr) { + let hash = 0; + for (let i = 0; i < styleAttr.length; i++) { + hash = (hash << 5) - hash; // eslint-disable-line no-bitwise + hash += styleAttr.charCodeAt(i); + hash &= hash; // eslint-disable-line no-bitwise + } + return `${hash}`; +} + +const styleAttrPattern = new RegExp('style="[^"]*"', 'g'); + +function replaceStyleAttrs(html) { + const allStyleAttrs = [...new Set(html.match(styleAttrPattern))]; + const cssMap = {}; + let result = html; + for (let i = 0; i < allStyleAttrs.length; i++) { + const styleAttr = allStyleAttrs[i]; + const cssClassName = `output-${createStyleAttrHash(styleAttr)}`; + + cssMap[cssClassName] = styleAttr.replace('style="', '').slice(0, -1); + result = result.split(styleAttr).join(`class="${cssClassName}"`); + } + return { cssMap, result }; +} + +export default function getLineTextHtml({ + created, + event, + start_line, + stdout, +}) { + const sanitized = encode(stdout); + let lineCssMap = {}; + const lineTextHtml = []; + + sanitized.split('\r\n').forEach((lineText, index) => { + 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, + }; +} From c8604c73a96c77aa2fc8735a71e7ebe227d279af Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Tue, 17 Aug 2021 12:48:51 -0700 Subject: [PATCH 3/5] JobOutput: extract JobOutputPane --- awx/ui/src/screens/Job/JobOutput/JobOutput.js | 285 +--------------- .../screens/Job/JobOutput/JobOutputPane.js | 308 ++++++++++++++++++ 2 files changed, 324 insertions(+), 269 deletions(-) create mode 100644 awx/ui/src/screens/Job/JobOutput/JobOutputPane.js diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 5d4e06807c..21510db48c 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -2,19 +2,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { t } from '@lingui/macro'; import styled from 'styled-components'; -import { - AutoSizer, - CellMeasurer, - CellMeasurerCache, - InfiniteLoader, - List, -} from 'react-virtualized'; +import { CellMeasurerCache } from 'react-virtualized'; import { Button } from '@patternfly/react-core'; import AlertModal from 'components/AlertModal'; import { CardBody as _CardBody } from 'components/Card'; import ContentError from 'components/ContentError'; -import ContentLoading from 'components/ContentLoading'; import ErrorDetail from 'components/ErrorDetail'; import StatusIcon from 'components/StatusIcon'; @@ -23,11 +16,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest'; import useInterval from 'hooks/useInterval'; import { parseQueryString, getQSConfig } from 'util/qs'; import useIsMounted from 'hooks/useIsMounted'; -import JobEvent from './JobEvent'; -import JobEventSkeleton from './JobEventSkeleton'; -import PageControls from './PageControls'; -import HostEventModal from './HostEventModal'; -import JobOutputSearch from './JobOutputSearch'; +import JobOutputPane from './JobOutputPane'; import { HostStatusBar, OutputToolbar } from './shared'; import getRowRangePageSize from './shared/jobOutputUtils'; import getLineTextHtml from './getLineTextHtml'; @@ -56,27 +45,6 @@ const OutputHeader = styled.div` justify-content: space-between; `; -const OutputWrapper = styled.div` - background-color: #ffffff; - display: flex; - flex-direction: column; - flex: 1 1 auto; - font-family: monospace; - font-size: 15px; - outline: 1px solid #d7d7d7; - ${({ cssMap }) => - Object.keys(cssMap).map( - (className) => `.${className}{${cssMap[className]}}` - )} -`; - -const OutputFooter = styled.div` - background-color: #ebebeb; - border-right: 1px solid #d7d7d7; - width: 75px; - flex: 1; -`; - let ws; function connectJobSocket({ type, id }, onMessage) { ws = new WebSocket( @@ -129,23 +97,6 @@ function range(low, high) { return numbers; } -function isHostEvent(jobEvent) { - const { event, event_data, host, type } = jobEvent; - let isHost; - if (typeof host === 'number' || (event_data && event_data.res)) { - isHost = true; - } else if ( - type === 'project_update_event' && - event !== 'runner_on_skipped' && - event_data.host - ) { - isHost = true; - } else { - isHost = false; - } - return isHost; -} - const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 25, @@ -175,18 +126,13 @@ const getEventRequestParams = (job, remoteRowCount, requestRange) => { function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const location = useLocation(); const listRef = useRef(null); - const previousWidth = useRef(0); const jobSocketCounter = useRef(0); const isMounted = useIsMounted(); - const scrollTop = useRef(0); - const scrollHeight = useRef(0); const history = useHistory(); const [contentError, setContentError] = useState(null); const [cssMap, setCssMap] = useState({}); const [currentlyLoading, setCurrentlyLoading] = useState([]); const [hasContentLoading, setHasContentLoading] = useState(true); - const [hostEvent, setHostEvent] = useState({}); - const [isHostModalOpen, setIsHostModalOpen] = useState(false); const [jobStatus, setJobStatus] = useState(job.status ?? 'waiting'); const [showCancelModal, setShowCancelModal] = useState(false); const [remoteRowCount, setRemoteRowCount] = useState(0); @@ -203,6 +149,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { isMonitoringWebsocket ? 5000 : null ); + // A useEffect(() => { loadJobEvents(); @@ -234,12 +181,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { }; }, [location.search]); // eslint-disable-line react-hooks/exhaustive-deps + // B useEffect(() => { if (listRef.current?.recomputeRowHeights) { listRef.current.recomputeRowHeights(); } }, [currentlyLoading, cssMap, remoteRowCount]); + // C useEffect(() => { if (jobStatus && !isJobRunning(jobStatus)) { if (jobSocketCounter.current > remoteRowCount && isMounted.current) { @@ -394,165 +343,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } }; - const isRowLoaded = ({ index }) => { - if (results[index]) { - return true; - } - return currentlyLoading.includes(index); - }; - - const handleHostEventClick = (hostEventToOpen) => { - setHostEvent(hostEventToOpen); - setIsHostModalOpen(true); - }; - - const handleHostModalClose = () => { - setIsHostModalOpen(false); - }; - - const rowRenderer = ({ index, parent, key, style }) => { - if (listRef.current && isFollowModeEnabled) { - setTimeout(() => scrollToRow(remoteRowCount - 1), 0); - } - let actualLineTextHtml = []; - if (results[index]) { - const { lineTextHtml } = getLineTextHtml(results[index]); - actualLineTextHtml = lineTextHtml; - } - - return ( - - {results[index] ? ( - handleHostEventClick(results[index])} - className="row" - style={style} - lineTextHtml={actualLineTextHtml} - index={index} - {...results[index]} - /> - ) : ( - - )} - - ); - }; - - const loadMoreRows = ({ startIndex, stopIndex }) => { - if (startIndex === 0 && stopIndex === 0) { - return Promise.resolve(null); - } - - if (stopIndex > startIndex + 50) { - stopIndex = startIndex + 50; - } - - const [requestParams, loadRange, firstIndex] = getEventRequestParams( - job, - remoteRowCount, - [startIndex, stopIndex] - ); - - if (isMounted.current) { - setCurrentlyLoading((prevCurrentlyLoading) => - prevCurrentlyLoading.concat(loadRange) - ); - } - - const params = { - ...requestParams, - ...parseQueryString(QS_CONFIG, location.search), - }; - - return getJobModel(job.type) - .readEvents(job.id, params) - .then((response) => { - if (isMounted.current) { - const newResults = {}; - let newResultsCssMap = {}; - response.data.results.forEach((jobEvent, index) => { - newResults[firstIndex + index] = jobEvent; - const { lineCssMap } = getLineTextHtml(jobEvent); - newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; - }); - setResults((prevResults) => ({ - ...prevResults, - ...newResults, - })); - setCssMap((prevCssMap) => ({ - ...prevCssMap, - ...newResultsCssMap, - })); - setCurrentlyLoading((prevCurrentlyLoading) => - prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) - ); - loadRange.forEach((n) => { - cache.clear(n); - }); - } - }); - }; - - const scrollToRow = (rowIndex) => { - if (listRef.current) { - listRef.current.scrollToRow(rowIndex); - } - }; - - const handleScrollPrevious = () => { - const startIndex = listRef.current.Grid._renderedRowStartIndex; - const stopIndex = listRef.current.Grid._renderedRowStopIndex; - const scrollRange = stopIndex - startIndex + 1; - scrollToRow(Math.max(0, startIndex - scrollRange)); - }; - - const handleScrollNext = () => { - const stopIndex = listRef.current.Grid._renderedRowStopIndex; - scrollToRow(stopIndex - 1); - }; - - const handleScrollFirst = () => { - scrollToRow(0); - }; - - const handleScrollLast = () => { - scrollToRow(remoteRowCount - 1); - }; - - const handleResize = ({ width }) => { - if (width !== previousWidth) { - cache.clearAll(); - if (listRef.current?.recomputeRowHeights) { - listRef.current.recomputeRowHeights(); - } - } - previousWidth.current = width; - }; - - const handleScroll = (e) => { - if ( - isFollowModeEnabled && - scrollTop.current > e.scrollTop && - scrollHeight.current === e.scrollHeight - ) { - setIsFollowModeEnabled(false); - } - scrollTop.current = e.scrollTop; - scrollHeight.current = e.scrollHeight; - }; - if (contentError) { return ; } @@ -560,13 +350,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { return ( <> - {isHostModalOpen && ( - - )} @@ -581,61 +364,25 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { /> - - - - - {({ onRowsRendered, registerChild }) => ( - - {({ width, height }) => ( - <> - {hasContentLoading ? ( -
- -
- ) : ( - { - registerChild(ref); - listRef.current = ref; - }} - deferredMeasurementCache={cache} - height={height || 1} - onRowsRendered={onRowsRendered} - rowCount={remoteRowCount} - rowHeight={cache.rowHeight} - rowRenderer={rowRenderer} - scrollToAlignment="start" - width={width || 1} - overscanRowCount={20} - onScroll={handleScroll} - /> - )} - - )} -
- )} -
- -
{showCancelModal && isJobRunning(job.status) && ( + Object.keys(cssMap).map( + (className) => `.${className}{${cssMap[className]}}` + )} +`; + +const OutputFooter = styled.div` + background-color: #ebebeb; + border-right: 1px solid #d7d7d7; + width: 75px; + flex: 1; +`; + +function isHostEvent(jobEvent) { + const { event, event_data, host, type } = jobEvent; + let isHost; + if (typeof host === 'number' || (event_data && event_data.res)) { + isHost = true; + } else if ( + type === 'project_update_event' && + event !== 'runner_on_skipped' && + event_data.host + ) { + isHost = true; + } else { + isHost = false; + } + return isHost; +} + +export default function JobOutputPane({ + qsConfig, + job, + eventRelatedSearchableKeys, + eventSearchableKeys, + results, + setResults, + currentlyLoading, + setCurrentlyLoading, + hasContentLoading, + listRef, + remoteRowCount, + isFollowModeEnabled, + setIsFollowModeEnabled, + cache, + cssMap, + setCssMap, + getEventRequestParams, +}) { + const previousWidth = useRef(0); + const scrollTop = useRef(0); + const scrollHeight = useRef(0); + const isMounted = useIsMounted(); + const location = useLocation(); + + const [hostEvent, setHostEvent] = useState({}); + const [isHostModalOpen, setIsHostModalOpen] = useState(false); + + const isRowLoaded = ({ index }) => { + if (results[index]) { + return true; + } + return currentlyLoading.includes(index); + }; + + const handleHostEventClick = (hostEventToOpen) => { + setHostEvent(hostEventToOpen); + setIsHostModalOpen(true); + }; + + const scrollToRow = (rowIndex) => { + if (listRef.current) { + listRef.current.scrollToRow(rowIndex); + } + }; + + const rowRenderer = ({ index, parent, key, style }) => { + if (listRef.current && isFollowModeEnabled) { + setTimeout(() => scrollToRow(remoteRowCount - 1), 0); + } + let actualLineTextHtml = []; + if (results[index]) { + const { lineTextHtml } = getLineTextHtml(results[index]); + actualLineTextHtml = lineTextHtml; + } + + return ( + + {results[index] ? ( + handleHostEventClick(results[index])} + className="row" + style={style} + lineTextHtml={actualLineTextHtml} + index={index} + {...results[index]} + /> + ) : ( + + )} + + ); + }; + + const loadMoreRows = ({ startIndex, stopIndex }) => { + if (startIndex === 0 && stopIndex === 0) { + return Promise.resolve(null); + } + + if (stopIndex > startIndex + 50) { + stopIndex = startIndex + 50; + } + + const [requestParams, loadRange, firstIndex] = getEventRequestParams( + job, + remoteRowCount, + [startIndex, stopIndex] + ); + + if (isMounted.current) { + setCurrentlyLoading((prevCurrentlyLoading) => + prevCurrentlyLoading.concat(loadRange) + ); + } + + const params = { + ...requestParams, + ...parseQueryString(qsConfig, location.search), + }; + + return getJobModel(job.type) + .readEvents(job.id, params) + .then((response) => { + if (isMounted.current) { + const newResults = {}; + let newResultsCssMap = {}; + response.data.results.forEach((jobEvent, index) => { + newResults[firstIndex + index] = jobEvent; + const { lineCssMap } = getLineTextHtml(jobEvent); + newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; + }); + setResults((prevResults) => ({ + ...prevResults, + ...newResults, + })); + setCssMap((prevCssMap) => ({ + ...prevCssMap, + ...newResultsCssMap, + })); + setCurrentlyLoading((prevCurrentlyLoading) => + prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) + ); + loadRange.forEach((n) => { + cache.clear(n); + }); + } + }); + }; + + const handleResize = ({ width }) => { + if (width !== previousWidth) { + cache.clearAll(); + if (listRef.current?.recomputeRowHeights) { + listRef.current.recomputeRowHeights(); + } + } + previousWidth.current = width; + }; + + const handleScroll = (e) => { + if ( + isFollowModeEnabled && + scrollTop.current > e.scrollTop && + scrollHeight.current === e.scrollHeight + ) { + setIsFollowModeEnabled(false); + } + scrollTop.current = e.scrollTop; + scrollHeight.current = e.scrollHeight; + }; + + const handleScrollPrevious = () => { + const startIndex = listRef.current.Grid._renderedRowStartIndex; + const stopIndex = listRef.current.Grid._renderedRowStopIndex; + const scrollRange = stopIndex - startIndex + 1; + scrollToRow(Math.max(0, startIndex - scrollRange)); + }; + + const handleScrollNext = () => { + const stopIndex = listRef.current.Grid._renderedRowStopIndex; + scrollToRow(stopIndex - 1); + }; + + const handleScrollFirst = () => { + scrollToRow(0); + }; + + const handleScrollLast = () => { + scrollToRow(remoteRowCount - 1); + }; + + return ( + <> + + + + {isHostModalOpen && ( + setIsHostModalOpen(false)} + isOpen={isHostModalOpen} + hostEvent={hostEvent} + /> + )} + + {({ onRowsRendered, registerChild }) => ( + + {({ width, height }) => ( + <> + {hasContentLoading ? ( +
+ +
+ ) : ( + { + registerChild(ref); + listRef.current = ref; + }} + deferredMeasurementCache={cache} + height={height || 1} + onRowsRendered={onRowsRendered} + rowCount={remoteRowCount} + rowHeight={cache.rowHeight} + rowRenderer={rowRenderer} + scrollToAlignment="start" + width={width || 1} + overscanRowCount={20} + onScroll={handleScroll} + /> + )} + + )} +
+ )} +
+ +
+ + ); +} From b5708a8cc47adc4e979d2c4271c9c5e3b63072eb Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Tue, 17 Aug 2021 13:27:02 -0700 Subject: [PATCH 4/5] Revert "JobOutput: extract JobOutputPane" This reverts commit 903de92969bf931cf0c01eb2fbfb703842c5ea83. --- awx/ui/src/screens/Job/JobOutput/JobOutput.js | 285 +++++++++++++++- .../screens/Job/JobOutput/JobOutputPane.js | 308 ------------------ 2 files changed, 269 insertions(+), 324 deletions(-) delete mode 100644 awx/ui/src/screens/Job/JobOutput/JobOutputPane.js diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 21510db48c..5d4e06807c 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -2,12 +2,19 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { t } from '@lingui/macro'; import styled from 'styled-components'; -import { CellMeasurerCache } from 'react-virtualized'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + InfiniteLoader, + List, +} from 'react-virtualized'; import { Button } from '@patternfly/react-core'; import AlertModal from 'components/AlertModal'; import { CardBody as _CardBody } from 'components/Card'; import ContentError from 'components/ContentError'; +import ContentLoading from 'components/ContentLoading'; import ErrorDetail from 'components/ErrorDetail'; import StatusIcon from 'components/StatusIcon'; @@ -16,7 +23,11 @@ import useRequest, { useDismissableError } from 'hooks/useRequest'; import useInterval from 'hooks/useInterval'; import { parseQueryString, getQSConfig } from 'util/qs'; import useIsMounted from 'hooks/useIsMounted'; -import JobOutputPane from './JobOutputPane'; +import JobEvent from './JobEvent'; +import JobEventSkeleton from './JobEventSkeleton'; +import PageControls from './PageControls'; +import HostEventModal from './HostEventModal'; +import JobOutputSearch from './JobOutputSearch'; import { HostStatusBar, OutputToolbar } from './shared'; import getRowRangePageSize from './shared/jobOutputUtils'; import getLineTextHtml from './getLineTextHtml'; @@ -45,6 +56,27 @@ const OutputHeader = styled.div` justify-content: space-between; `; +const OutputWrapper = styled.div` + background-color: #ffffff; + display: flex; + flex-direction: column; + flex: 1 1 auto; + font-family: monospace; + font-size: 15px; + outline: 1px solid #d7d7d7; + ${({ cssMap }) => + Object.keys(cssMap).map( + (className) => `.${className}{${cssMap[className]}}` + )} +`; + +const OutputFooter = styled.div` + background-color: #ebebeb; + border-right: 1px solid #d7d7d7; + width: 75px; + flex: 1; +`; + let ws; function connectJobSocket({ type, id }, onMessage) { ws = new WebSocket( @@ -97,6 +129,23 @@ function range(low, high) { return numbers; } +function isHostEvent(jobEvent) { + const { event, event_data, host, type } = jobEvent; + let isHost; + if (typeof host === 'number' || (event_data && event_data.res)) { + isHost = true; + } else if ( + type === 'project_update_event' && + event !== 'runner_on_skipped' && + event_data.host + ) { + isHost = true; + } else { + isHost = false; + } + return isHost; +} + const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 25, @@ -126,13 +175,18 @@ const getEventRequestParams = (job, remoteRowCount, requestRange) => { function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const location = useLocation(); const listRef = useRef(null); + const previousWidth = useRef(0); const jobSocketCounter = useRef(0); const isMounted = useIsMounted(); + const scrollTop = useRef(0); + const scrollHeight = useRef(0); const history = useHistory(); const [contentError, setContentError] = useState(null); const [cssMap, setCssMap] = useState({}); const [currentlyLoading, setCurrentlyLoading] = useState([]); const [hasContentLoading, setHasContentLoading] = useState(true); + const [hostEvent, setHostEvent] = useState({}); + const [isHostModalOpen, setIsHostModalOpen] = useState(false); const [jobStatus, setJobStatus] = useState(job.status ?? 'waiting'); const [showCancelModal, setShowCancelModal] = useState(false); const [remoteRowCount, setRemoteRowCount] = useState(0); @@ -149,7 +203,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { isMonitoringWebsocket ? 5000 : null ); - // A useEffect(() => { loadJobEvents(); @@ -181,14 +234,12 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { }; }, [location.search]); // eslint-disable-line react-hooks/exhaustive-deps - // B useEffect(() => { if (listRef.current?.recomputeRowHeights) { listRef.current.recomputeRowHeights(); } }, [currentlyLoading, cssMap, remoteRowCount]); - // C useEffect(() => { if (jobStatus && !isJobRunning(jobStatus)) { if (jobSocketCounter.current > remoteRowCount && isMounted.current) { @@ -343,6 +394,165 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } }; + const isRowLoaded = ({ index }) => { + if (results[index]) { + return true; + } + return currentlyLoading.includes(index); + }; + + const handleHostEventClick = (hostEventToOpen) => { + setHostEvent(hostEventToOpen); + setIsHostModalOpen(true); + }; + + const handleHostModalClose = () => { + setIsHostModalOpen(false); + }; + + const rowRenderer = ({ index, parent, key, style }) => { + if (listRef.current && isFollowModeEnabled) { + setTimeout(() => scrollToRow(remoteRowCount - 1), 0); + } + let actualLineTextHtml = []; + if (results[index]) { + const { lineTextHtml } = getLineTextHtml(results[index]); + actualLineTextHtml = lineTextHtml; + } + + return ( + + {results[index] ? ( + handleHostEventClick(results[index])} + className="row" + style={style} + lineTextHtml={actualLineTextHtml} + index={index} + {...results[index]} + /> + ) : ( + + )} + + ); + }; + + const loadMoreRows = ({ startIndex, stopIndex }) => { + if (startIndex === 0 && stopIndex === 0) { + return Promise.resolve(null); + } + + if (stopIndex > startIndex + 50) { + stopIndex = startIndex + 50; + } + + const [requestParams, loadRange, firstIndex] = getEventRequestParams( + job, + remoteRowCount, + [startIndex, stopIndex] + ); + + if (isMounted.current) { + setCurrentlyLoading((prevCurrentlyLoading) => + prevCurrentlyLoading.concat(loadRange) + ); + } + + const params = { + ...requestParams, + ...parseQueryString(QS_CONFIG, location.search), + }; + + return getJobModel(job.type) + .readEvents(job.id, params) + .then((response) => { + if (isMounted.current) { + const newResults = {}; + let newResultsCssMap = {}; + response.data.results.forEach((jobEvent, index) => { + newResults[firstIndex + index] = jobEvent; + const { lineCssMap } = getLineTextHtml(jobEvent); + newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; + }); + setResults((prevResults) => ({ + ...prevResults, + ...newResults, + })); + setCssMap((prevCssMap) => ({ + ...prevCssMap, + ...newResultsCssMap, + })); + setCurrentlyLoading((prevCurrentlyLoading) => + prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) + ); + loadRange.forEach((n) => { + cache.clear(n); + }); + } + }); + }; + + const scrollToRow = (rowIndex) => { + if (listRef.current) { + listRef.current.scrollToRow(rowIndex); + } + }; + + const handleScrollPrevious = () => { + const startIndex = listRef.current.Grid._renderedRowStartIndex; + const stopIndex = listRef.current.Grid._renderedRowStopIndex; + const scrollRange = stopIndex - startIndex + 1; + scrollToRow(Math.max(0, startIndex - scrollRange)); + }; + + const handleScrollNext = () => { + const stopIndex = listRef.current.Grid._renderedRowStopIndex; + scrollToRow(stopIndex - 1); + }; + + const handleScrollFirst = () => { + scrollToRow(0); + }; + + const handleScrollLast = () => { + scrollToRow(remoteRowCount - 1); + }; + + const handleResize = ({ width }) => { + if (width !== previousWidth) { + cache.clearAll(); + if (listRef.current?.recomputeRowHeights) { + listRef.current.recomputeRowHeights(); + } + } + previousWidth.current = width; + }; + + const handleScroll = (e) => { + if ( + isFollowModeEnabled && + scrollTop.current > e.scrollTop && + scrollHeight.current === e.scrollHeight + ) { + setIsFollowModeEnabled(false); + } + scrollTop.current = e.scrollTop; + scrollHeight.current = e.scrollHeight; + }; + if (contentError) { return ; } @@ -350,6 +560,13 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { return ( <> + {isHostModalOpen && ( + + )} @@ -364,25 +581,61 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { /> - + + + + {({ onRowsRendered, registerChild }) => ( + + {({ width, height }) => ( + <> + {hasContentLoading ? ( +
+ +
+ ) : ( + { + registerChild(ref); + listRef.current = ref; + }} + deferredMeasurementCache={cache} + height={height || 1} + onRowsRendered={onRowsRendered} + rowCount={remoteRowCount} + rowHeight={cache.rowHeight} + rowRenderer={rowRenderer} + scrollToAlignment="start" + width={width || 1} + overscanRowCount={20} + onScroll={handleScroll} + /> + )} + + )} +
+ )} +
+ +
{showCancelModal && isJobRunning(job.status) && ( - Object.keys(cssMap).map( - (className) => `.${className}{${cssMap[className]}}` - )} -`; - -const OutputFooter = styled.div` - background-color: #ebebeb; - border-right: 1px solid #d7d7d7; - width: 75px; - flex: 1; -`; - -function isHostEvent(jobEvent) { - const { event, event_data, host, type } = jobEvent; - let isHost; - if (typeof host === 'number' || (event_data && event_data.res)) { - isHost = true; - } else if ( - type === 'project_update_event' && - event !== 'runner_on_skipped' && - event_data.host - ) { - isHost = true; - } else { - isHost = false; - } - return isHost; -} - -export default function JobOutputPane({ - qsConfig, - job, - eventRelatedSearchableKeys, - eventSearchableKeys, - results, - setResults, - currentlyLoading, - setCurrentlyLoading, - hasContentLoading, - listRef, - remoteRowCount, - isFollowModeEnabled, - setIsFollowModeEnabled, - cache, - cssMap, - setCssMap, - getEventRequestParams, -}) { - const previousWidth = useRef(0); - const scrollTop = useRef(0); - const scrollHeight = useRef(0); - const isMounted = useIsMounted(); - const location = useLocation(); - - const [hostEvent, setHostEvent] = useState({}); - const [isHostModalOpen, setIsHostModalOpen] = useState(false); - - const isRowLoaded = ({ index }) => { - if (results[index]) { - return true; - } - return currentlyLoading.includes(index); - }; - - const handleHostEventClick = (hostEventToOpen) => { - setHostEvent(hostEventToOpen); - setIsHostModalOpen(true); - }; - - const scrollToRow = (rowIndex) => { - if (listRef.current) { - listRef.current.scrollToRow(rowIndex); - } - }; - - const rowRenderer = ({ index, parent, key, style }) => { - if (listRef.current && isFollowModeEnabled) { - setTimeout(() => scrollToRow(remoteRowCount - 1), 0); - } - let actualLineTextHtml = []; - if (results[index]) { - const { lineTextHtml } = getLineTextHtml(results[index]); - actualLineTextHtml = lineTextHtml; - } - - return ( - - {results[index] ? ( - handleHostEventClick(results[index])} - className="row" - style={style} - lineTextHtml={actualLineTextHtml} - index={index} - {...results[index]} - /> - ) : ( - - )} - - ); - }; - - const loadMoreRows = ({ startIndex, stopIndex }) => { - if (startIndex === 0 && stopIndex === 0) { - return Promise.resolve(null); - } - - if (stopIndex > startIndex + 50) { - stopIndex = startIndex + 50; - } - - const [requestParams, loadRange, firstIndex] = getEventRequestParams( - job, - remoteRowCount, - [startIndex, stopIndex] - ); - - if (isMounted.current) { - setCurrentlyLoading((prevCurrentlyLoading) => - prevCurrentlyLoading.concat(loadRange) - ); - } - - const params = { - ...requestParams, - ...parseQueryString(qsConfig, location.search), - }; - - return getJobModel(job.type) - .readEvents(job.id, params) - .then((response) => { - if (isMounted.current) { - const newResults = {}; - let newResultsCssMap = {}; - response.data.results.forEach((jobEvent, index) => { - newResults[firstIndex + index] = jobEvent; - const { lineCssMap } = getLineTextHtml(jobEvent); - newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; - }); - setResults((prevResults) => ({ - ...prevResults, - ...newResults, - })); - setCssMap((prevCssMap) => ({ - ...prevCssMap, - ...newResultsCssMap, - })); - setCurrentlyLoading((prevCurrentlyLoading) => - prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) - ); - loadRange.forEach((n) => { - cache.clear(n); - }); - } - }); - }; - - const handleResize = ({ width }) => { - if (width !== previousWidth) { - cache.clearAll(); - if (listRef.current?.recomputeRowHeights) { - listRef.current.recomputeRowHeights(); - } - } - previousWidth.current = width; - }; - - const handleScroll = (e) => { - if ( - isFollowModeEnabled && - scrollTop.current > e.scrollTop && - scrollHeight.current === e.scrollHeight - ) { - setIsFollowModeEnabled(false); - } - scrollTop.current = e.scrollTop; - scrollHeight.current = e.scrollHeight; - }; - - const handleScrollPrevious = () => { - const startIndex = listRef.current.Grid._renderedRowStartIndex; - const stopIndex = listRef.current.Grid._renderedRowStopIndex; - const scrollRange = stopIndex - startIndex + 1; - scrollToRow(Math.max(0, startIndex - scrollRange)); - }; - - const handleScrollNext = () => { - const stopIndex = listRef.current.Grid._renderedRowStopIndex; - scrollToRow(stopIndex - 1); - }; - - const handleScrollFirst = () => { - scrollToRow(0); - }; - - const handleScrollLast = () => { - scrollToRow(remoteRowCount - 1); - }; - - return ( - <> - - - - {isHostModalOpen && ( - setIsHostModalOpen(false)} - isOpen={isHostModalOpen} - hostEvent={hostEvent} - /> - )} - - {({ onRowsRendered, registerChild }) => ( - - {({ width, height }) => ( - <> - {hasContentLoading ? ( -
- -
- ) : ( - { - registerChild(ref); - listRef.current = ref; - }} - deferredMeasurementCache={cache} - height={height || 1} - onRowsRendered={onRowsRendered} - rowCount={remoteRowCount} - rowHeight={cache.rowHeight} - rowRenderer={rowRenderer} - scrollToAlignment="start" - width={width || 1} - overscanRowCount={20} - onScroll={handleScroll} - /> - )} - - )} -
- )} -
- -
- - ); -} From b5bc9bb3f4e461bfabcd7d89a5ba555bb6efce9c Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Tue, 17 Aug 2021 16:20:54 -0700 Subject: [PATCH 5/5] JobOutput: extract multiple helper functions --- awx/ui/src/screens/Job/JobOutput/JobOutput.js | 219 ++++-------------- .../screens/Job/JobOutput/connectJobSocket.js | 50 ++++ .../Job/JobOutput/getEventRequestParams.js | 35 +++ .../src/screens/Job/JobOutput/isHostEvent.js | 16 ++ .../screens/Job/JobOutput/loadJobEvents.js | 46 ++++ 5 files changed, 191 insertions(+), 175 deletions(-) create mode 100644 awx/ui/src/screens/Job/JobOutput/connectJobSocket.js create mode 100644 awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js create mode 100644 awx/ui/src/screens/Job/JobOutput/isHostEvent.js create mode 100644 awx/ui/src/screens/Job/JobOutput/loadJobEvents.js diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 5d4e06807c..15bf7e83af 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -29,8 +29,11 @@ import PageControls from './PageControls'; import HostEventModal from './HostEventModal'; import JobOutputSearch from './JobOutputSearch'; import { HostStatusBar, OutputToolbar } from './shared'; -import getRowRangePageSize from './shared/jobOutputUtils'; import getLineTextHtml from './getLineTextHtml'; +import connectJobSocket, { closeWebSocket } from './connectJobSocket'; +import getEventRequestParams from './getEventRequestParams'; +import isHostEvent from './isHostEvent'; +import { fetchCount, normalizeEvents } from './loadJobEvents'; const QS_CONFIG = getQSConfig('job_output', { order_by: 'counter', @@ -77,101 +80,11 @@ const OutputFooter = styled.div` flex: 1; `; -let ws; -function connectJobSocket({ type, id }, onMessage) { - ws = new WebSocket( - `${window.location.protocol === 'http:' ? 'ws:' : 'wss:'}//${ - window.location.host - }/websocket/` - ); - - ws.onopen = () => { - const xrftoken = `; ${document.cookie}` - .split('; csrftoken=') - .pop() - .split(';') - .shift(); - const eventGroup = `${type}_events`; - ws.send( - JSON.stringify({ - xrftoken, - groups: { jobs: ['summary', 'status_changed'], [eventGroup]: [id] }, - }) - ); - }; - - ws.onmessage = (e) => { - onMessage(JSON.parse(e.data)); - }; - - ws.onclose = (e) => { - if (e.code !== 1000) { - // eslint-disable-next-line no-console - console.debug('Socket closed. Reconnecting...', e); - setTimeout(() => { - connectJobSocket({ type, id }, onMessage); - }, 1000); - } - }; - - ws.onerror = (err) => { - // eslint-disable-next-line no-console - console.debug('Socket error: ', err, 'Disconnecting...'); - ws.close(); - }; -} - -function range(low, high) { - const numbers = []; - for (let n = low; n <= high; n++) { - numbers.push(n); - } - return numbers; -} - -function isHostEvent(jobEvent) { - const { event, event_data, host, type } = jobEvent; - let isHost; - if (typeof host === 'number' || (event_data && event_data.res)) { - isHost = true; - } else if ( - type === 'project_update_event' && - event !== 'runner_on_skipped' && - event_data.host - ) { - isHost = true; - } else { - isHost = false; - } - return isHost; -} - const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 25, }); -const getEventRequestParams = (job, remoteRowCount, requestRange) => { - const [startIndex, stopIndex] = requestRange; - if (isJobRunning(job?.status)) { - return [ - { counter__gte: startIndex, limit: stopIndex - startIndex + 1 }, - range(startIndex, Math.min(stopIndex, remoteRowCount)), - startIndex, - ]; - } - const { page, pageSize, firstIndex } = getRowRangePageSize( - startIndex, - stopIndex - ); - const loadRange = range( - firstIndex, - Math.min(firstIndex + pageSize, remoteRowCount) - ); - - return [{ page, page_size: pageSize }, loadRange, firstIndex]; -}; - function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const location = useLocation(); const listRef = useRef(null); @@ -226,9 +139,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } return function cleanup() { - if (ws) { - ws.close(); - } + closeWebSocket(); setIsMonitoringWebsocket(false); isMounted.current = false; }; @@ -312,73 +223,29 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { ...parseQueryString(QS_CONFIG, location.search), }); - let countRequest; - if (isJobRunning(job?.status)) { - // If the job is running, it means we're using limit-offset pagination. Requests - // with limit-offset pagination won't return a total event count for performance - // reasons. In this situation, we derive the remote row count by using the highest - // counter available in the database. - countRequest = async () => { - const { - data: { results: lastEvents = [] }, - } = await getJobModel(job.type).readEvents(job.id, { - order_by: '-counter', - limit: 1, - }); - return lastEvents.length >= 1 ? lastEvents[0].counter : 0; - }; - } else { - countRequest = async () => { - const { - data: { count: eventCount }, - } = await eventPromise; - return eventCount; - }; - } - try { const [ { data: { results: fetchedEvents = [] }, }, count, - ] = await Promise.all([eventPromise, countRequest()]); + ] = await Promise.all([eventPromise, fetchCount(job, eventPromise)]); - if (isMounted.current) { - let countOffset = 0; - if (job?.result_traceback) { - const tracebackEvent = { - counter: 1, - created: null, - event: null, - type: null, - stdout: job?.result_traceback, - start_line: 0, - }; - const firstIndex = fetchedEvents.findIndex( - (jobEvent) => jobEvent.counter === 1 - ); - if (firstIndex && fetchedEvents[firstIndex]?.stdout) { - const stdoutLines = fetchedEvents[firstIndex].stdout.split('\r\n'); - stdoutLines[0] = tracebackEvent.stdout; - fetchedEvents[firstIndex].stdout = stdoutLines.join('\r\n'); - } else { - countOffset += 1; - fetchedEvents.unshift(tracebackEvent); - } - } - - const newResults = {}; - let newResultsCssMap = {}; - fetchedEvents.forEach((jobEvent, index) => { - newResults[index] = jobEvent; - const { lineCssMap } = getLineTextHtml(jobEvent); - newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; - }); - setResults(newResults); - setRemoteRowCount(count + countOffset); - setCssMap(newResultsCssMap); + if (!isMounted.current) { + return; } + const { events, countOffset } = normalizeEvents(job, fetchedEvents); + + const newResults = {}; + let newResultsCssMap = {}; + events.forEach((jobEvent, index) => { + newResults[index] = jobEvent; + const { lineCssMap } = getLineTextHtml(jobEvent); + newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; + }); + setResults(newResults); + setRemoteRowCount(count + countOffset); + setCssMap(newResultsCssMap); } catch (err) { setContentError(err); } finally { @@ -479,29 +346,31 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { return getJobModel(job.type) .readEvents(job.id, params) .then((response) => { - if (isMounted.current) { - const newResults = {}; - let newResultsCssMap = {}; - response.data.results.forEach((jobEvent, index) => { - newResults[firstIndex + index] = jobEvent; - const { lineCssMap } = getLineTextHtml(jobEvent); - newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; - }); - setResults((prevResults) => ({ - ...prevResults, - ...newResults, - })); - setCssMap((prevCssMap) => ({ - ...prevCssMap, - ...newResultsCssMap, - })); - setCurrentlyLoading((prevCurrentlyLoading) => - prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) - ); - loadRange.forEach((n) => { - cache.clear(n); - }); + if (!isMounted.current) { + return; } + + const newResults = {}; + let newResultsCssMap = {}; + response.data.results.forEach((jobEvent, index) => { + newResults[firstIndex + index] = jobEvent; + const { lineCssMap } = getLineTextHtml(jobEvent); + newResultsCssMap = { ...newResultsCssMap, ...lineCssMap }; + }); + setResults((prevResults) => ({ + ...prevResults, + ...newResults, + })); + setCssMap((prevCssMap) => ({ + ...prevCssMap, + ...newResultsCssMap, + })); + setCurrentlyLoading((prevCurrentlyLoading) => + prevCurrentlyLoading.filter((n) => !loadRange.includes(n)) + ); + loadRange.forEach((n) => { + cache.clear(n); + }); }); }; diff --git a/awx/ui/src/screens/Job/JobOutput/connectJobSocket.js b/awx/ui/src/screens/Job/JobOutput/connectJobSocket.js new file mode 100644 index 0000000000..58c6fb4ac6 --- /dev/null +++ b/awx/ui/src/screens/Job/JobOutput/connectJobSocket.js @@ -0,0 +1,50 @@ +let ws; + +export default function connectJobSocket({ type, id }, onMessage) { + ws = new WebSocket( + `${window.location.protocol === 'http:' ? 'ws:' : 'wss:'}//${ + window.location.host + }/websocket/` + ); + + ws.onopen = () => { + const xrftoken = `; ${document.cookie}` + .split('; csrftoken=') + .pop() + .split(';') + .shift(); + const eventGroup = `${type}_events`; + ws.send( + JSON.stringify({ + xrftoken, + groups: { jobs: ['summary', 'status_changed'], [eventGroup]: [id] }, + }) + ); + }; + + ws.onmessage = (e) => { + onMessage(JSON.parse(e.data)); + }; + + ws.onclose = (e) => { + if (e.code !== 1000) { + // eslint-disable-next-line no-console + console.debug('Socket closed. Reconnecting...', e); + setTimeout(() => { + connectJobSocket({ type, id }, onMessage); + }, 1000); + } + }; + + ws.onerror = (err) => { + // eslint-disable-next-line no-console + console.debug('Socket error: ', err, 'Disconnecting...'); + ws.close(); + }; +} + +export function closeWebSocket() { + if (ws) { + ws.close(); + } +} diff --git a/awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js b/awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js new file mode 100644 index 0000000000..07ac49a768 --- /dev/null +++ b/awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js @@ -0,0 +1,35 @@ +import { isJobRunning } from 'util/jobs'; +import getRowRangePageSize from './shared/jobOutputUtils'; + +export default function getEventRequestParams( + job, + remoteRowCount, + requestRange +) { + const [startIndex, stopIndex] = requestRange; + if (isJobRunning(job?.status)) { + return [ + { counter__gte: startIndex, limit: stopIndex - startIndex + 1 }, + range(startIndex, Math.min(stopIndex, remoteRowCount)), + startIndex, + ]; + } + const { page, pageSize, firstIndex } = getRowRangePageSize( + startIndex, + stopIndex + ); + const loadRange = range( + firstIndex, + Math.min(firstIndex + pageSize, remoteRowCount) + ); + + return [{ page, page_size: pageSize }, loadRange, firstIndex]; +} + +function range(low, high) { + const numbers = []; + for (let n = low; n <= high; n++) { + numbers.push(n); + } + return numbers; +} diff --git a/awx/ui/src/screens/Job/JobOutput/isHostEvent.js b/awx/ui/src/screens/Job/JobOutput/isHostEvent.js new file mode 100644 index 0000000000..30922b626f --- /dev/null +++ b/awx/ui/src/screens/Job/JobOutput/isHostEvent.js @@ -0,0 +1,16 @@ +export default function isHostEvent(jobEvent) { + const { event, event_data, host, type } = jobEvent; + let isHost; + if (typeof host === 'number' || (event_data && event_data.res)) { + isHost = true; + } else if ( + type === 'project_update_event' && + event !== 'runner_on_skipped' && + event_data.host + ) { + isHost = true; + } else { + isHost = false; + } + return isHost; +} diff --git a/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js b/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js new file mode 100644 index 0000000000..9c9033e927 --- /dev/null +++ b/awx/ui/src/screens/Job/JobOutput/loadJobEvents.js @@ -0,0 +1,46 @@ +import { getJobModel, isJobRunning } from 'util/jobs'; + +export async function fetchCount(job, eventPromise) { + if (isJobRunning(job?.status)) { + const { + data: { results: lastEvents = [] }, + } = await getJobModel(job.type).readEvents(job.id, { + order_by: '-counter', + limit: 1, + }); + return lastEvents.length >= 1 ? lastEvents[0].counter : 0; + } + + const { + data: { count: eventCount }, + } = await eventPromise; + return eventCount; +} + +export function normalizeEvents(job, events) { + let countOffset = 0; + if (job?.result_traceback) { + const tracebackEvent = { + counter: 1, + created: null, + event: null, + type: null, + stdout: job?.result_traceback, + start_line: 0, + }; + const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1); + if (firstIndex && events[firstIndex]?.stdout) { + const stdoutLines = events[firstIndex].stdout.split('\r\n'); + stdoutLines[0] = tracebackEvent.stdout; + events[firstIndex].stdout = stdoutLines.join('\r\n'); + } else { + countOffset += 1; + events.unshift(tracebackEvent); + } + } + + return { + events, + countOffset, + }; +}