mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
Merge pull request #10909 from keithjgrant/6613-job-output-collapsing
Refactor Job Output component
This commit is contained in:
@@ -9,148 +9,36 @@ import {
|
|||||||
InfiniteLoader,
|
InfiniteLoader,
|
||||||
List,
|
List,
|
||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
import Ansi from 'ansi-to-html';
|
import { Button } from '@patternfly/react-core';
|
||||||
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 AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import { CardBody as _CardBody } from 'components/Card';
|
import { CardBody as _CardBody } from 'components/Card';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import Search from 'components/Search';
|
|
||||||
import StatusIcon from 'components/StatusIcon';
|
import StatusIcon from 'components/StatusIcon';
|
||||||
|
|
||||||
import { getJobModel, isJobRunning } from 'util/jobs';
|
import { getJobModel, isJobRunning } from 'util/jobs';
|
||||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||||
import useInterval from 'hooks/useInterval';
|
import useInterval from 'hooks/useInterval';
|
||||||
import {
|
import { parseQueryString, getQSConfig } from 'util/qs';
|
||||||
parseQueryString,
|
|
||||||
mergeParams,
|
|
||||||
removeParams,
|
|
||||||
getQSConfig,
|
|
||||||
updateQueryString,
|
|
||||||
} from 'util/qs';
|
|
||||||
import useIsMounted from 'hooks/useIsMounted';
|
import useIsMounted from 'hooks/useIsMounted';
|
||||||
import JobEvent from './JobEvent';
|
import JobEvent from './JobEvent';
|
||||||
import JobEventSkeleton from './JobEventSkeleton';
|
import JobEventSkeleton from './JobEventSkeleton';
|
||||||
import PageControls from './PageControls';
|
import PageControls from './PageControls';
|
||||||
import HostEventModal from './HostEventModal';
|
import HostEventModal from './HostEventModal';
|
||||||
|
import JobOutputSearch from './JobOutputSearch';
|
||||||
import { HostStatusBar, OutputToolbar } from './shared';
|
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', {
|
const QS_CONFIG = getQSConfig('job_output', {
|
||||||
order_by: 'counter',
|
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 += `<span class="time">${time}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
lineTextHtml.push({
|
|
||||||
lineNumber: start_line + index,
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
lineCssMap,
|
|
||||||
lineTextHtml,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CardBody = styled(_CardBody)`
|
const CardBody = styled(_CardBody)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
@@ -192,110 +80,11 @@ const OutputFooter = styled.div`
|
|||||||
flex: 1;
|
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(
|
|
||||||
`${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({
|
const cache = new CellMeasurerCache({
|
||||||
fixedWidth: true,
|
fixedWidth: true,
|
||||||
defaultHeight: 25,
|
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 }) {
|
function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
@@ -350,9 +139,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function cleanup() {
|
return function cleanup() {
|
||||||
if (ws) {
|
closeWebSocket();
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
setIsMonitoringWebsocket(false);
|
setIsMonitoringWebsocket(false);
|
||||||
isMounted.current = false;
|
isMounted.current = false;
|
||||||
};
|
};
|
||||||
@@ -436,73 +223,29 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
...parseQueryString(QS_CONFIG, location.search),
|
...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 {
|
try {
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
data: { results: fetchedEvents = [] },
|
data: { results: fetchedEvents = [] },
|
||||||
},
|
},
|
||||||
count,
|
count,
|
||||||
] = await Promise.all([eventPromise, countRequest()]);
|
] = await Promise.all([eventPromise, fetchCount(job, eventPromise)]);
|
||||||
|
|
||||||
if (isMounted.current) {
|
if (!isMounted.current) {
|
||||||
let countOffset = 0;
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} catch (err) {
|
||||||
setContentError(err);
|
setContentError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -603,29 +346,31 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
return getJobModel(job.type)
|
return getJobModel(job.type)
|
||||||
.readEvents(job.id, params)
|
.readEvents(job.id, params)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (isMounted.current) {
|
if (!isMounted.current) {
|
||||||
const newResults = {};
|
return;
|
||||||
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 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -665,55 +410,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
previousWidth.current = width;
|
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) => {
|
const handleScroll = (e) => {
|
||||||
if (
|
if (
|
||||||
isFollowModeEnabled &&
|
isFollowModeEnabled &&
|
||||||
@@ -726,64 +422,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
scrollHeight.current = e.scrollHeight;
|
scrollHeight.current = e.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSearchComponent = () => (
|
|
||||||
<Search
|
|
||||||
qsConfig={QS_CONFIG}
|
|
||||||
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' },
|
|
||||||
]}
|
|
||||||
searchableKeys={eventSearchableKeys}
|
|
||||||
relatedSearchableKeys={eventRelatedSearchableKeys}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
onReplaceSearch={handleReplaceSearch}
|
|
||||||
onShowAdvancedSearch={() => {}}
|
|
||||||
onRemove={handleRemoveSearchTerm}
|
|
||||||
isDisabled={isJobRunning(job.status)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contentError) {
|
if (contentError) {
|
||||||
return <ContentError error={contentError} />;
|
return <ContentError error={contentError} />;
|
||||||
}
|
}
|
||||||
@@ -812,36 +450,16 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
/>
|
/>
|
||||||
</OutputHeader>
|
</OutputHeader>
|
||||||
<HostStatusBar counts={job.host_status_counts} />
|
<HostStatusBar counts={job.host_status_counts} />
|
||||||
<SearchToolbar
|
<JobOutputSearch
|
||||||
id="job_output-toolbar"
|
qsConfig={QS_CONFIG}
|
||||||
clearAllFilters={handleRemoveAllSearchTerms}
|
job={job}
|
||||||
collapseListedFiltersBreakpoint="lg"
|
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
|
||||||
clearFiltersButtonText={t`Clear all filters`}
|
eventSearchableKeys={eventSearchableKeys}
|
||||||
>
|
remoteRowCount={remoteRowCount}
|
||||||
<SearchToolbarContent>
|
scrollToRow={scrollToRow}
|
||||||
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
|
isFollowModeEnabled={isFollowModeEnabled}
|
||||||
<ToolbarItem variant="search-filter">
|
setIsFollowModeEnabled={setIsFollowModeEnabled}
|
||||||
{isJobRunning(job.status) ? (
|
/>
|
||||||
<Tooltip
|
|
||||||
content={t`Search is disabled while the job is running`}
|
|
||||||
>
|
|
||||||
{renderSearchComponent()}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
renderSearchComponent()
|
|
||||||
)}
|
|
||||||
</ToolbarItem>
|
|
||||||
</ToolbarToggleGroup>
|
|
||||||
{isJobRunning(job.status) ? (
|
|
||||||
<Button
|
|
||||||
variant={isFollowModeEnabled ? 'secondary' : 'primary'}
|
|
||||||
onClick={handleFollowToggle}
|
|
||||||
>
|
|
||||||
{isFollowModeEnabled ? t`Unfollow` : t`Follow`}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</SearchToolbarContent>
|
|
||||||
</SearchToolbar>
|
|
||||||
<PageControls
|
<PageControls
|
||||||
onScrollFirst={handleScrollFirst}
|
onScrollFirst={handleScrollFirst}
|
||||||
onScrollLast={handleScrollLast}
|
onScrollLast={handleScrollLast}
|
||||||
@@ -946,5 +564,4 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { JobOutput as _JobOutput };
|
|
||||||
export default JobOutput;
|
export default JobOutput;
|
||||||
|
|||||||
188
awx/ui/src/screens/Job/JobOutput/JobOutputSearch.js
Normal file
188
awx/ui/src/screens/Job/JobOutput/JobOutputSearch.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Toolbar,
|
||||||
|
ToolbarContent,
|
||||||
|
ToolbarItem,
|
||||||
|
ToolbarToggleGroup,
|
||||||
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { SearchIcon } from '@patternfly/react-icons';
|
||||||
|
import Search from 'components/Search';
|
||||||
|
import {
|
||||||
|
parseQueryString,
|
||||||
|
mergeParams,
|
||||||
|
removeParams,
|
||||||
|
updateQueryString,
|
||||||
|
} from 'util/qs';
|
||||||
|
import { isJobRunning } from 'util/jobs';
|
||||||
|
|
||||||
|
const SearchToolbarContent = styled(ToolbarContent)`
|
||||||
|
padding-left: 0px !important;
|
||||||
|
padding-right: 0px !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function JobOutputSearch({
|
||||||
|
qsConfig,
|
||||||
|
job,
|
||||||
|
eventRelatedSearchableKeys,
|
||||||
|
eventSearchableKeys,
|
||||||
|
remoteRowCount,
|
||||||
|
scrollToRow,
|
||||||
|
isFollowModeEnabled,
|
||||||
|
setIsFollowModeEnabled,
|
||||||
|
}) {
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleSearch = (key, value) => {
|
||||||
|
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 (
|
||||||
|
<Toolbar
|
||||||
|
id="job_output-toolbar"
|
||||||
|
clearAllFilters={handleRemoveAllSearchTerms}
|
||||||
|
collapseListedFiltersBreakpoint="lg"
|
||||||
|
clearFiltersButtonText={t`Clear all filters`}
|
||||||
|
>
|
||||||
|
<SearchToolbarContent>
|
||||||
|
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
|
||||||
|
<ToolbarItem variant="search-filter">
|
||||||
|
{isDisabled ? (
|
||||||
|
<Tooltip content={t`Search is disabled while the job is running`}>
|
||||||
|
<Search
|
||||||
|
qsConfig={qsConfig}
|
||||||
|
columns={columns}
|
||||||
|
searchableKeys={eventSearchableKeys}
|
||||||
|
relatedSearchableKeys={eventRelatedSearchableKeys}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onReplaceSearch={handleReplaceSearch}
|
||||||
|
onShowAdvancedSearch={() => {}}
|
||||||
|
onRemove={handleRemoveSearchTerm}
|
||||||
|
isDisabled
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Search
|
||||||
|
qsConfig={qsConfig}
|
||||||
|
columns={columns}
|
||||||
|
searchableKeys={eventSearchableKeys}
|
||||||
|
relatedSearchableKeys={eventRelatedSearchableKeys}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onReplaceSearch={handleReplaceSearch}
|
||||||
|
onShowAdvancedSearch={() => {}}
|
||||||
|
onRemove={handleRemoveSearchTerm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarToggleGroup>
|
||||||
|
{isJobRunning(job.status) ? (
|
||||||
|
<Button
|
||||||
|
variant={isFollowModeEnabled ? 'secondary' : 'primary'}
|
||||||
|
onClick={handleFollowToggle}
|
||||||
|
>
|
||||||
|
{isFollowModeEnabled ? t`Unfollow` : t`Follow`}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</SearchToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobOutputSearch;
|
||||||
@@ -2,7 +2,7 @@ import 'styled-components/macro';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button as PFButton } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
AngleDoubleUpIcon,
|
AngleDoubleUpIcon,
|
||||||
AngleDoubleDownIcon,
|
AngleDoubleDownIcon,
|
||||||
@@ -14,16 +14,11 @@ import styled from 'styled-components';
|
|||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
outline: 1px solid #d7d7d7;
|
border: 1px solid #d7d7d7;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Button = styled(PFButton)`
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PageControls = ({
|
const PageControls = ({
|
||||||
onScrollFirst,
|
onScrollFirst,
|
||||||
onScrollLast,
|
onScrollLast,
|
||||||
|
|||||||
50
awx/ui/src/screens/Job/JobOutput/connectJobSocket.js
Normal file
50
awx/ui/src/screens/Job/JobOutput/connectJobSocket.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js
Normal file
35
awx/ui/src/screens/Job/JobOutput/getEventRequestParams.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
107
awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js
Normal file
107
awx/ui/src/screens/Job/JobOutput/getLineTextHtml.js
Normal file
@@ -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 += `<span class="time">${time}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineTextHtml.push({
|
||||||
|
lineNumber: start_line + index,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineCssMap,
|
||||||
|
lineTextHtml,
|
||||||
|
};
|
||||||
|
}
|
||||||
16
awx/ui/src/screens/Job/JobOutput/isHostEvent.js
Normal file
16
awx/ui/src/screens/Job/JobOutput/isHostEvent.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
46
awx/ui/src/screens/Job/JobOutput/loadJobEvents.js
Normal file
46
awx/ui/src/screens/Job/JobOutput/loadJobEvents.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user