Add host event modal

This commit is contained in:
Marliana Lara 2019-09-06 12:43:53 -04:00
parent 5babab7af4
commit 7480baf256
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
4 changed files with 280 additions and 2 deletions

View File

@ -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 (
<Modal
isLarge
isOpen={isOpen}
onClose={handleClose}
title={i18n._(t`Host Details`)}
actions={[
<Button key="cancel" variant="secondary" onClick={handleClose}>
{i18n._(t`Close`)}
</Button>,
]}
>
<Tabs activeKey={activeTabKey} onSelect={handleTabClick}>
<Tab eventKey={0} title={i18n._(t`Details`)}>
<DetailList style={{ alignItems: 'center' }} gutter="sm">
<Detail
label={i18n._(t`Host Name`)}
value={
<HostNameDetailValue>
{hostStatus && <HostStatusIcon status={hostStatus} />}
{hostEvent.host_name}
</HostNameDetailValue>
}
/>
<Detail label={i18n._(t`Play`)} value={hostEvent.play} />
<Detail label={i18n._(t`Task`)} value={hostEvent.task} />
<Detail
label={i18n._(t`Module`)}
value={taskAction || i18n._(t`No result found`)}
/>
<Detail
label={i18n._(t`Command`)}
value={hostEvent.event_data.res.cmd}
/>
</DetailList>
</Tab>
<Tab eventKey={1} title={i18n._(t`JSON`)}>
{activeTabKey === 1 && JSONObj ? (
<CodeMirrorInput
mode="javascript"
readOnly
value={JSON.stringify(JSONObj, null, 2)}
onChange={() => {}}
rows={20}
hasErrors={false}
/>
) : (
<ContentEmpty title={i18n._(t`No JSON Found`)} />
)}
</Tab>
<Tab eventKey={2} title={i18n._(t`Standard Out`)}>
{activeTabKey === 2 && StdOut ? (
<CodeMirrorInput
mode="javascript"
readOnly
value={StdOut}
onChange={() => {}}
rows={20}
hasErrors={false}
/>
) : (
<ContentEmpty title={i18n._(t`No Standard Out Found`)} />
)}
</Tab>
<Tab eventKey={3} title={i18n._(t`Standard Error`)}>
{activeTabKey === 3 && StdErr ? (
<CodeMirrorInput
mode="javascript"
readOnly
onChange={() => {}}
value={StdErr}
hasErrors={false}
rows={20}
/>
) : (
<ContentEmpty title={i18n._(t`No Standard Error Found`)} />
)}
</Tab>
</Tabs>
</Modal>
);
}
export default withI18n()(HostEventModal);

View File

@ -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 && (
<JobEventLine
onClick={isClickable ? onJobEventClick : undefined}
key={`${counter}-${lineNumber}`}
isFirst={lineNumber === 0}
isClickable={isClickable}
>
<JobEventLineToggle />
<JobEventLineNumber>{lineNumber}</JobEventLineNumber>

View File

@ -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 (
<CellMeasurer
key={key}
@ -161,7 +197,13 @@ class JobOutput extends Component {
columnIndex={0}
>
{results[index] ? (
<JobEvent className="row" style={style} {...results[index]} />
<JobEvent
isClickable={isHostEvent(results[index])}
onJobEventClick={() => this.handleHostEventClick(results[index])}
className="row"
style={style}
{...results[index]}
/>
) : (
<JobEventSkeleton
className="row"
@ -242,7 +284,13 @@ class JobOutput extends Component {
render() {
const { job } = this.props;
const { hasContentLoading, contentError, remoteRowCount } = this.state;
const {
contentError,
hasContentLoading,
hostEvent,
isHostModalOpen,
remoteRowCount,
} = this.state;
if (hasContentLoading) {
return <ContentLoading />;
@ -254,6 +302,13 @@ class JobOutput extends Component {
return (
<CardBody>
{isHostModalOpen && (
<HostEventModal
handleClose={this.handleHostModalClose}
isOpen={isHostModalOpen}
hostEvent={hostEvent}
/>
)}
<OutputHeader>{job.name}</OutputHeader>
<OutputToolbar>
<MenuControls

View File

@ -5,6 +5,7 @@ export default styled.div`
&:hover {
background-color: white;
cursor: ${props => (props.isClickable ? 'pointer' : 'default')};
}
&:hover div {