Jenkins 36131 "Show all" link required anywhere a log is displayed (#378)

* [JENKINS-36131] WIP testing with content length

* [JENKINS-36131] WIP add parameter fetchAll and refetch the logs when present. Broken for me ATM unknown reason. will merge master now in here

* [JENKINS-36131] WIP first working version with steps.

* [JENKINS-36131] WIP first working version as well for freestyle, but I need to refactor the thing. using the hash is not optimal, will switch to the backend hook start=0

* [JENKINS-36131] Switch to trigger ?start=0 which enables us to link in full extended logs. Now writing AT for it

* [JENKINS-36131] Make the link to the full log more visible

* [JENKINS-36131] easier matching for AT

* [JENKINS-36131] in follow along the Full Log button should not be shown, since you see everything already
This commit is contained in:
Thorsten Scherler 2016-07-28 13:01:40 +02:00 committed by GitHub
parent e84f3204e0
commit 3958b8aaa3
8 changed files with 153 additions and 28 deletions

View File

@ -108,7 +108,7 @@ export class LogConsole extends Component {
render() {
const lines = this.state.lines;
const { prefix = '' } = this.props;
const { prefix = '', hasMore = false } = this.props; // if hasMore true then show link to full log
if (!lines) {
return null;
}
@ -116,8 +116,21 @@ export class LogConsole extends Component {
return (<code
className="block"
>
{ hasMore && <div key={0} id={`${prefix}log-${0}`} className="fullLog">
<a
key={0}
href={`?start=0#${prefix || ''}log-${1}`}
>
Show complete log
</a>
</div>}
{ lines.map((line, index) => <p key={index + 1} id={`${prefix}log-${index + 1}`}>
<a key={index + 1} href={`#${prefix || ''}log-${index + 1}`} name={`${prefix}log-${index + 1}`}>{line}</a>
<a
key={index + 1}
href={`#${prefix || ''}log-${index + 1}`}
name={`${prefix}log-${index + 1}`}
>{line}
</a>
</p>)}</code>);
}
}
@ -129,6 +142,7 @@ LogConsole.propTypes = {
scrollToAnchorTimeOut: func,
scrollBottom: func,
prefix: string,
hasMore: bool,
};
export default scrollHelper(LogConsole);

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { Icon } from 'react-material-icons-blue';
import { fetchAllSuffix as suffix } from '../util/UrlUtils';
const { string } = PropTypes;
@ -12,6 +13,7 @@ export default class LogToolbar extends Component {
if (!url) {
return null;
}
const logUrl = url.includes(suffix) ? url : `${url}${suffix}`;
const style = { fill: '#4a4a4a' };
return (<div className="log-header">
<div className="log-header__section">
@ -21,13 +23,13 @@ export default class LogToolbar extends Component {
<a {...{
title: 'Display the log in new window',
target: '_blank',
href: `${url}?start=0`,
href: logUrl,
}}>
<Icon size={24} {...{ style, icon: 'launch' }} />
</a>
<a {...{
title: 'Download the log file',
href: `${url}?start=0&download=true`,
href: `${logUrl}&download=true`,
}}>
<Icon size={24} {...{ style, icon: 'file_download' }} />
</a>

View File

@ -17,7 +17,7 @@ import {
createSelector,
} from '../redux';
import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl } from '../util/UrlUtils';
import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl, calculateFetchAll } from '../util/UrlUtils';
import { calculateNode } from '../util/KaraokeHelper';
@ -45,7 +45,9 @@ export class RunDetailsPipeline extends Component {
} else {
// console.log('fetch the log directly')
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
fetchLog({ ...logGeneral });
// fetchAll indicates whether we want all logs
const fetchAll = this.mergedConfig.fetchAll;
fetchLog({ ...logGeneral, fetchAll });
}
// Listen for pipeline flow node events.
@ -127,10 +129,13 @@ export class RunDetailsPipeline extends Component {
// if we have actions we fire them
this.props[nodeAction.action](this.mergedConfig);
}
const fetchAll = this.mergedConfig.fetchAll;
// console.log('this.mergedConfig.fetchAll', fetchAll)
// if we only interested in logs (in case of e.g. freestyle)
const { logs, fetchLog } = nextProps;
if (logs !== this.props.logs) {
if (logs !== this.props.logs || fetchAll) {
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
// console.log('logGenralReceive', logGeneral)
const log = logs ? logs[logGeneral.url] : null;
if (log && log !== null) {
// we may have a streaming log
@ -144,20 +149,26 @@ export class RunDetailsPipeline extends Component {
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, newStart }), 1000);
}
}
} else if (fetchAll) {
// kill current timeout if any
clearTimeout(this.timeout);
// we need to get mpre input from the log stream
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, fetchAll }), 1000);
}
}
}
componentWillUnmount() {
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
domNode.removeEventListener('wheel', this._onScrollHandler);
document.removeEventListener('keydown', this._handleKeys);
if (this.listener.sse) {
sse.unsubscribe(this.listener.sse);
delete this.listener.sse;
}
this.props.cleanNodePointer();
clearTimeout(this.timeout);
domNode.removeEventListener('wheel', this._onScrollHandler);
document.removeEventListener('keydown', this._handleKeys);
}
// need to register handler to step out of karaoke mode
@ -183,6 +194,7 @@ export class RunDetailsPipeline extends Component {
isMultiBranch,
params: { pipeline: name, branch, runId, node: nodeParam },
} = props;
const fetchAll = calculateFetchAll(props);
// we would use default properties however the node can be null so no default properties will be triggered
let { nodeReducer } = props;
if (!nodeReducer) {
@ -191,7 +203,7 @@ export class RunDetailsPipeline extends Component {
// if we have a node param we do not want the calculation of the focused node
const node = nodeParam || nodeReducer.id;
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong };
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong, fetchAll };
return mergedConfig;
}
@ -267,6 +279,19 @@ export class RunDetailsPipeline extends Component {
};
const noSteps = !log && currentSteps && currentSteps.model && currentSteps.model.length === 0;
const shouldShowLogHeader = log !== null || !noSteps;
const logProps = {
scrollToBottom,
key: logGeneral.url,
};
if (log) {
// in follow along the Full Log button should not be shown, since you see everything already
if (followAlong) {
logProps.hasMore = false;
} else {
logProps.hasMore = log.hasMore;
}
logProps.logArray = log.logArray;
}
return (
<div ref="scrollArea">
{ nodes && nodes[nodeKey] && <Extensions.Renderer
@ -298,7 +323,7 @@ export class RunDetailsPipeline extends Component {
</EmptyStateView>
}
{ log && <LogConsole key={logGeneral.url} logArray={log.logArray} scrollToBottom={scrollToBottom} /> }
{ log && <LogConsole {...logProps} /> }
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { ResultItem } from '@jenkins-cd/design-language';
import { calculateLogUrl } from '../util/UrlUtils';
import { calculateFetchAll, calculateLogUrl } from '../util/UrlUtils';
import LogConsole from './LogConsole';
@ -18,7 +18,8 @@ export default class Node extends Component {
const { config = {} } = this.context;
const node = this.expandAnchor(this.props);
if (node && node.isFocused) {
const mergedConfig = { ...config, node, nodesBaseUrl };
const fetchAll = node.fetchAll;
const mergedConfig = { ...config, node, nodesBaseUrl, fetchAll };
fetchLog(mergedConfig);
}
}
@ -33,8 +34,9 @@ export default class Node extends Component {
}
const { config = {} } = this.context;
const node = this.expandAnchor(nextProps);
const mergedConfig = { ...config, node, nodesBaseUrl };
if (logs && logs !== this.props.logs) {
const fetchAll = node.fetchAll;
const mergedConfig = { ...config, node, nodesBaseUrl, fetchAll };
if (logs && logs !== this.props.logs || fetchAll) {
const key = calculateLogUrl(mergedConfig);
const log = logs ? logs[key] : null;
if (log && log !== null) {
@ -47,6 +49,9 @@ export default class Node extends Component {
this.clearThisTimeout();
this.timeout = setTimeout(() => fetchLog({ ...mergedConfig }), 1000);
}
} else if (!log && fetchAll) { // in case the link "full log" is clicked we need to trigger a refetch
this.clearThisTimeout();
this.timeout = setTimeout(() => fetchLog({ ...mergedConfig }), 1000);
}
}
}
@ -60,26 +65,33 @@ export default class Node extends Component {
clearTimeout(this.timeout);
}
}
// Calculate whether we need to expand the step due to linking
/*
* Calculate whether we need to expand the step due to linking.
* When we trigger a log-0 that means we want to see the full log
*/
expandAnchor(props) {
const { node, location: { hash: anchorName } } = props;
const isFocused = true;
const fetchAll = calculateFetchAll(props);
const general = { ...node, fetchAll };
// e.g. #step-10-log-1 or #step-10
if (anchorName) {
const stepReg = /step-([0-9]{1,})?($|-log-([0-9]{1,})$)/;
const match = stepReg.exec(anchorName);
if (match && match[1] && match[1] === node.id) {
return { ...node, isFocused };
return { ...general, isFocused };
}
} else if (this.state && this.state.isFocused) {
return { ...node, isFocused };
return { ...general, isFocused };
}
return { ...node };
return general;
}
render() {
const { logs, nodesBaseUrl, fetchLog, followAlong } = this.props;
const node = this.expandAnchor(this.props);
const fetchAll = node.fetchAll;
// Early out
if (!node || !fetchLog) {
return null;
@ -95,7 +107,7 @@ export default class Node extends Component {
} = node;
const resultRun = result === 'UNKNOWN' || !result ? state : result;
const log = logs ? logs[calculateLogUrl({ ...config, node, nodesBaseUrl })] : null;
const log = logs ? logs[calculateLogUrl({ ...config, node, nodesBaseUrl, fetchAll })] : null;
const getLogForNode = () => {
// in case we do not have logs, or the logs are have no information attached we refetch them
if (!log || !log.logArray) {
@ -108,6 +120,20 @@ export default class Node extends Component {
resultRun.toLowerCase() === 'failure'
|| (resultRun.toLowerCase() === 'running' && followAlong)
;
const logProps = {
scrollToBottom,
key: id,
prefix: `step-${id}-`,
};
if (log) {
// in follow along the Full Log button should not be shown, since you see everything already
if (followAlong) {
logProps.hasMore = false;
} else {
logProps.hasMore = log.hasMore;
}
logProps.logArray = log.logArray;
}
return (<div className="logConsole">
<ResultItem
key={id}
@ -117,12 +143,7 @@ export default class Node extends Component {
onExpand={getLogForNode}
durationMillis={durationInMillis}
>
{ log && <LogConsole
key={id}
logArray={log.logArray}
scrollToBottom={scrollToBottom}
prefix={`step-${id}-`}
/> }
{ log && <LogConsole {...logProps} /> }
{ !log && <span>
&nbsp;

View File

@ -94,6 +94,7 @@ export const actionHandlers = {
[ACTION_TYPES.SET_LOGS](state, { payload }): State {
const logs = { ...state.logs } || {};
logs[payload.logUrl] = payload;
return state.set('logs', logs);
},
@ -768,6 +769,7 @@ export const actions = {
const data = getState().adminStore.logs;
const logUrl = calculateLogUrl(config);
if (
config.fetchAll ||
!data || !data[logUrl] ||
config.newStart > 0 ||
(data && data[logUrl] && data[logUrl].newStart > 0 || !data[logUrl].logArray)
@ -777,10 +779,21 @@ export const actions = {
config.newStart || null,
response => response.response.text()
.then(text => {
// By default only last 150 KB log data is returned in the response.
const maxLength = 150000;
const contentLength = Number(response.response.headers.get('X-Text-Size'));
// set flag that there are more logs then we deliver
let hasMore = contentLength > maxLength;
// when we came from ?start=0, hasMore has to be false since there is no more
// console.log(config.fetchAll, 'inner')
if (config.fetchAll) {
hasMore = false;
}
const { newStart } = response;
const payload = {
logUrl,
newStart,
hasMore,
};
if (text && !!text.trim()) {
payload.logArray = text.trim().split('\n');

View File

@ -40,16 +40,43 @@ export const buildRunDetailsUrl = (organization, pipeline, branch, runId, tabNam
*/
export const uriString = (input) => encodeURIComponent(input).replace(/%2F/g, '%252F');
// general fetchAllTrigger
export const fetchAllSuffix = '?start=0';
// Add fetchAllSuffix in case it is needed
export const applyFetchAll = function (config, url) {
// if we pass fetchAll means we want the full log -> start=0 will trigger that on the server
if (config.fetchAll && !url.includes(fetchAllSuffix)) {
return `${url}${fetchAllSuffix}`;
}
return url;
};
// using the hook 'location.search'.includes('start=0') to trigger fetchAll
export const calculateFetchAll = function (props) {
const { location: { search } } = props;
if (search) {
const stepReg = /start=([0-9]{1,})/;
const match = stepReg.exec(search);
if (match && match[1] && Number(match[1]) === 0) {
return true;
}
}
return false;
};
/*
* helper to calculate log url. When we have a node we get create a special url, otherwise we use the url passed to us
* @param config { nodesBaseUrl, node, url}
*/
export const calculateLogUrl = (config) => {
let returnUrl = config.url;
if (config.node) {
const { nodesBaseUrl, node } = config;
return `${nodesBaseUrl}/${node.id}/log/`;
returnUrl = `${nodesBaseUrl}/${node.id}/log/`;
}
return config.url;
return applyFetchAll(config, returnUrl);
};
/*
@ -105,6 +132,7 @@ export function calculateRunLogURLObject(config) {
url = `${baseUrl}/runs/${runId}/log/`;
fileName = `${runId}.txt`;
}
url = applyFetchAll(config, url);
return {
url,
fileName,

View File

@ -170,3 +170,23 @@
border: none;
padding: 0;
}
code div {
font-size: 12px;
margin: 5px;
display: flex;
align-items: center;
justify-content: center;
}
code div a{
border: solid 1px @pre-color;
padding: 3px 10px;
margin: 5px;
color: @pre-color;
}
code div a:hover{
background-color: @pre-color-hover;
}

View File

@ -2,3 +2,5 @@
@blueocean-blue-darkened: darken(@blueocean-blue, 20%);
@gray-base: #000;
@pre-bg: lighten(@gray-base, 20%); // #333
@pre-color: #f5f5f5;
@pre-color-hover: #444!important;