mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 09:27:36 -02:30
Add host event modal
This commit is contained in:
218
awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx
Normal file
218
awx/ui_next/src/screens/Job/JobOutput/HostEventModal.jsx
Normal 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);
|
||||||
@@ -77,6 +77,8 @@ function JobEvent({
|
|||||||
counter,
|
counter,
|
||||||
created,
|
created,
|
||||||
event,
|
event,
|
||||||
|
isClickable,
|
||||||
|
onJobEventClick,
|
||||||
stdout,
|
stdout,
|
||||||
start_line,
|
start_line,
|
||||||
style,
|
style,
|
||||||
@@ -88,8 +90,10 @@ function JobEvent({
|
|||||||
({ lineNumber, html }) =>
|
({ lineNumber, html }) =>
|
||||||
lineNumber >= 0 && (
|
lineNumber >= 0 && (
|
||||||
<JobEventLine
|
<JobEventLine
|
||||||
|
onClick={isClickable ? onJobEventClick : undefined}
|
||||||
key={`${counter}-${lineNumber}`}
|
key={`${counter}-${lineNumber}`}
|
||||||
isFirst={lineNumber === 0}
|
isFirst={lineNumber === 0}
|
||||||
|
isClickable={isClickable}
|
||||||
>
|
>
|
||||||
<JobEventLineToggle />
|
<JobEventLineToggle />
|
||||||
<JobEventLineNumber>{lineNumber}</JobEventLineNumber>
|
<JobEventLineNumber>{lineNumber}</JobEventLineNumber>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ContentLoading from '@components/ContentLoading';
|
|||||||
import JobEvent from './JobEvent';
|
import JobEvent from './JobEvent';
|
||||||
import JobEventSkeleton from './JobEventSkeleton';
|
import JobEventSkeleton from './JobEventSkeleton';
|
||||||
import MenuControls from './MenuControls';
|
import MenuControls from './MenuControls';
|
||||||
|
import HostEventModal from './HostEventModal';
|
||||||
|
|
||||||
const OutputHeader = styled.div`
|
const OutputHeader = styled.div`
|
||||||
font-weight: var(--pf-global--FontWeight--bold);
|
font-weight: var(--pf-global--FontWeight--bold);
|
||||||
@@ -59,6 +60,8 @@ class JobOutput extends Component {
|
|||||||
results: {},
|
results: {},
|
||||||
currentlyLoading: [],
|
currentlyLoading: [],
|
||||||
remoteRowCount: 0,
|
remoteRowCount: 0,
|
||||||
|
isHostModalOpen: false,
|
||||||
|
hostEvent: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cache = new CellMeasurerCache({
|
this.cache = new CellMeasurerCache({
|
||||||
@@ -69,6 +72,8 @@ class JobOutput extends Component {
|
|||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
this.loadJobEvents = this.loadJobEvents.bind(this);
|
this.loadJobEvents = this.loadJobEvents.bind(this);
|
||||||
this.rowRenderer = this.rowRenderer.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.handleScrollFirst = this.handleScrollFirst.bind(this);
|
||||||
this.handleScrollLast = this.handleScrollLast.bind(this);
|
this.handleScrollLast = this.handleScrollLast.bind(this);
|
||||||
this.handleScrollNext = this.handleScrollNext.bind(this);
|
this.handleScrollNext = this.handleScrollNext.bind(this);
|
||||||
@@ -150,8 +155,39 @@ class JobOutput extends Component {
|
|||||||
return currentlyLoading.includes(index);
|
return currentlyLoading.includes(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHostEventClick(hostEvent) {
|
||||||
|
this.setState({
|
||||||
|
isHostModalOpen: true,
|
||||||
|
hostEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHostModalClose() {
|
||||||
|
this.setState({
|
||||||
|
isHostModalOpen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
rowRenderer({ index, parent, key, style }) {
|
rowRenderer({ index, parent, key, style }) {
|
||||||
const { results } = this.state;
|
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 (
|
return (
|
||||||
<CellMeasurer
|
<CellMeasurer
|
||||||
key={key}
|
key={key}
|
||||||
@@ -161,7 +197,13 @@ class JobOutput extends Component {
|
|||||||
columnIndex={0}
|
columnIndex={0}
|
||||||
>
|
>
|
||||||
{results[index] ? (
|
{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
|
<JobEventSkeleton
|
||||||
className="row"
|
className="row"
|
||||||
@@ -242,7 +284,13 @@ class JobOutput extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { job } = this.props;
|
const { job } = this.props;
|
||||||
const { hasContentLoading, contentError, remoteRowCount } = this.state;
|
const {
|
||||||
|
contentError,
|
||||||
|
hasContentLoading,
|
||||||
|
hostEvent,
|
||||||
|
isHostModalOpen,
|
||||||
|
remoteRowCount,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
@@ -254,6 +302,13 @@ class JobOutput extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
{isHostModalOpen && (
|
||||||
|
<HostEventModal
|
||||||
|
handleClose={this.handleHostModalClose}
|
||||||
|
isOpen={isHostModalOpen}
|
||||||
|
hostEvent={hostEvent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<OutputHeader>{job.name}</OutputHeader>
|
<OutputHeader>{job.name}</OutputHeader>
|
||||||
<OutputToolbar>
|
<OutputToolbar>
|
||||||
<MenuControls
|
<MenuControls
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default styled.div`
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
cursor: ${props => (props.isClickable ? 'pointer' : 'default')};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover div {
|
&:hover div {
|
||||||
|
|||||||
Reference in New Issue
Block a user