diff --git a/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx new file mode 100644 index 0000000000..52297e50de --- /dev/null +++ b/awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx @@ -0,0 +1,218 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Modal as PFModal, + Tab, + Tabs as PFTabs, +} from '@patternfly/react-core'; +import CodeMirrorInput from '@components/CodeMirrorInput'; +import ContentEmpty from '@components/ContentEmpty'; +import { DetailList, Detail } from '@components/DetailList'; +import { HostStatusIcon } from '@components/Sparkline'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import Entities from 'html-entities'; + +const entities = new Entities.AllHtmlEntities(); + +const Modal = styled(PFModal)` + --pf-c-modal-box__footer--MarginTop: 0; + .pf-c-modal-box__body { + overflow-y: hidden; + } + .pf-c-tab-content { + padding: 24px 0; + } +`; + +const HostNameDetailValue = styled.div` + align-items: center; + display: inline-grid; + grid-gap: 10px; + grid-template-columns: min-content auto; +`; + +const Tabs = styled(PFTabs)` + --pf-c-tabs__button--PaddingLeft: 20px; + --pf-c-tabs__button--PaddingRight: 20px; + + .pf-c-tabs__list { + li:first-of-type .pf-c-tabs__button { + &::after { + margin-left: 0; + } + } + } + + &:not(.pf-c-tabs__item)::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + border-bottom: solid var(--pf-c-tabs__item--BorderColor); + border-width: var(--pf-c-tabs__item--BorderWidth) 0 + var(--pf-c-tabs__item--BorderWidth) 0; + } +`; + +function HostEventModal({ handleClose, hostEvent, isOpen, i18n }) { + const [hostStatus, setHostStatus] = useState(null); + const [activeTabKey, setActiveTabKey] = useState(0); + + useEffect(() => { + processEventStatus(hostEvent); + }, []); + + const handleTabClick = (event, tabIndex) => { + setActiveTabKey(tabIndex); + }; + + function processEventStatus(event) { + let status = null; + if (event.event === 'runner_on_unreachable') { + status = 'unreachable'; + } + // equiv to 'runner_on_error' && 'runner_on_failed' + if (event.failed) { + status = 'failed'; + } + // catch the 'changed' case before 'ok', because both can be true + if (event.changed) { + status = 'changed'; + } + if ( + event.event === 'runner_on_ok' || + event.event === 'runner_on_async_ok' || + event.event === 'runner_item_on_ok' + ) { + status = 'ok'; + } + if (event.event === 'runner_on_skipped') { + status = 'skipped'; + } + setHostStatus(status); + } + + function processStdOutValue() { + const { res } = hostEvent.event_data; + let stdOut; + if (taskAction === 'debug' && res.result && res.result.stdout) { + stdOut = processCodeMirrorValue(res.result.stdout); + } else if ( + taskAction === 'yum' && + res.results && + Array.isArray(res.results) + ) { + stdOut = processCodeMirrorValue(res.results[0]); + } else { + stdOut = processCodeMirrorValue(res.stdout); + } + return stdOut; + } + + const processCodeMirrorValue = value => { + let codeMirrorValue; + if (value === undefined) { + codeMirrorValue = false; + } else if (value === '') { + codeMirrorValue = ' '; + } else if (typeof value === 'string') { + codeMirrorValue = entities.encode(value); + } else { + codeMirrorValue = value; + } + return codeMirrorValue; + }; + + const taskAction = hostEvent.event_data.task_action; + const JSONObj = processCodeMirrorValue(hostEvent.event_data.res); + const StdErr = processCodeMirrorValue(hostEvent.event_data.res.stderr); + const StdOut = processStdOutValue(); + + return ( + + {i18n._(t`Close`)} + , + ]} + > + + + + + {hostStatus && } + {hostEvent.host_name} + + } + /> + + + + + + + + {activeTabKey === 1 && JSONObj ? ( + {}} + rows={20} + hasErrors={false} + /> + ) : ( + + )} + + + {activeTabKey === 2 && StdOut ? ( + {}} + rows={20} + hasErrors={false} + /> + ) : ( + + )} + + + {activeTabKey === 3 && StdErr ? ( + {}} + value={StdErr} + hasErrors={false} + rows={20} + /> + ) : ( + + )} + + + + ); +} + +export default withI18n()(HostEventModal); diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx index 4e41d50a90..769ccf42a4 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobEvent.jsx @@ -77,6 +77,8 @@ function JobEvent({ counter, created, event, + isClickable, + onJobEventClick, stdout, start_line, style, @@ -88,8 +90,10 @@ function JobEvent({ ({ lineNumber, html }) => lineNumber >= 0 && ( {lineNumber} diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx index 18508c1372..d5c928a99c 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx @@ -16,6 +16,7 @@ import ContentLoading from '@components/ContentLoading'; import JobEvent from './JobEvent'; import JobEventSkeleton from './JobEventSkeleton'; import MenuControls from './MenuControls'; +import HostEventModal from './HostEventModal'; const OutputHeader = styled.div` font-weight: var(--pf-global--FontWeight--bold); @@ -59,6 +60,8 @@ class JobOutput extends Component { results: {}, currentlyLoading: [], remoteRowCount: 0, + isHostModalOpen: false, + hostEvent: {}, }; this.cache = new CellMeasurerCache({ @@ -69,6 +72,8 @@ class JobOutput extends Component { this._isMounted = false; this.loadJobEvents = this.loadJobEvents.bind(this); this.rowRenderer = this.rowRenderer.bind(this); + this.handleHostEventClick = this.handleHostEventClick.bind(this); + this.handleHostModalClose = this.handleHostModalClose.bind(this); this.handleScrollFirst = this.handleScrollFirst.bind(this); this.handleScrollLast = this.handleScrollLast.bind(this); this.handleScrollNext = this.handleScrollNext.bind(this); @@ -150,8 +155,39 @@ class JobOutput extends Component { return currentlyLoading.includes(index); } + handleHostEventClick(hostEvent) { + this.setState({ + isHostModalOpen: true, + hostEvent, + }); + } + + handleHostModalClose() { + this.setState({ + isHostModalOpen: false, + }); + } + rowRenderer({ index, parent, key, style }) { const { results } = this.state; + + const 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; + }; + return ( {results[index] ? ( - + this.handleHostEventClick(results[index])} + className="row" + style={style} + {...results[index]} + /> ) : ( ; @@ -254,6 +302,13 @@ class JobOutput extends Component { return ( + {isHostModalOpen && ( + + )} {job.name} (props.isClickable ? 'pointer' : 'default')}; } &:hover div {