@@ -21,13 +23,13 @@ export default class LogToolbar extends Component {
diff --git a/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx b/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx
index b29b263b..d6315975 100644
--- a/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx
+++ b/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx
@@ -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 (
{ nodes && nodes[nodeKey] &&
}
- { log && }
+ { log && }
);
}
diff --git a/blueocean-dashboard/src/main/js/components/Step.jsx b/blueocean-dashboard/src/main/js/components/Step.jsx
index 22869df3..c3852314 100644
--- a/blueocean-dashboard/src/main/js/components/Step.jsx
+++ b/blueocean-dashboard/src/main/js/components/Step.jsx
@@ -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,7 +120,21 @@ export default class Node extends Component {
resultRun.toLowerCase() === 'failure'
|| (resultRun.toLowerCase() === 'running' && followAlong)
;
- return (
+ 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 (
- { log && }
+ { log && }
+
+ { !log &&
+
+ }
);
}
diff --git a/blueocean-dashboard/src/main/js/components/testing/TestResults.jsx b/blueocean-dashboard/src/main/js/components/testing/TestResults.jsx
index 8475c8d4..3c5c26ca 100644
--- a/blueocean-dashboard/src/main/js/components/testing/TestResults.jsx
+++ b/blueocean-dashboard/src/main/js/components/testing/TestResults.jsx
@@ -32,6 +32,7 @@ const TestCaseResultRow = (props) => {
let statusIndicator = null;
switch (t.status) {
+ case 'REGRESSION':
case 'FAILED':
statusIndicator = StatusIndicator.validResultValues.failure;
break;
@@ -67,12 +68,11 @@ export default class TestResult extends Component {
const suites = this.props.testResults.suites;
const tests = [].concat.apply([], suites.map(t => t.cases));
- // possible statuses: PASSED, FAILED, SKIPPED
- const failures = tests.filter(t => t.status === 'FAILED');
+ // one of 5 possible statuses: PASSED, FIXED, SKIPPED, FAILED, REGRESSION see: hudson.tasks.junit.CaseResult$Status :(
const fixed = tests.filter(t => t.status === 'FIXED');
const skipped = tests.filter(t => t.status === 'SKIPPED');
- const newFailures = failures.filter(t => t.age === 1);
- const existingFailures = failures.filter(t => t.age > 1);
+ const newFailures = tests.filter(t => (t.age <= 1 && t.status === 'FAILED') || t.status === 'REGRESSION');
+ const existingFailures = tests.filter(t => t.age > 1 && t.status === 'FAILED');
let passBlock = null;
let newFailureBlock = null;
@@ -129,13 +129,6 @@ export default class TestResult extends Component {
);
}
- if (fixed.length > 0) {
- fixedBlock = (
-
Fixed
- {fixed.map((t, i) => )}
- );
- }
-
if (skipped.length > 0) {
skippedBlock = (
Skipped - {skipped.length}
@@ -144,14 +137,22 @@ export default class TestResult extends Component {
}
}
+ // always show fixed, whether showing totals or the encouraging message
+ if (fixed.length > 0) {
+ fixedBlock = (
+
Fixed
+ {fixed.map((t, i) => )}
+ );
+ }
+
return (
+ {passBlock}
{summaryBlock}
{newFailureBlock}
{existingFailureBlock}
{fixedBlock}
{skippedBlock}
- {passBlock}
);
}
diff --git a/blueocean-dashboard/src/main/js/redux/actions.js b/blueocean-dashboard/src/main/js/redux/actions.js
index a654e47a..771b997f 100644
--- a/blueocean-dashboard/src/main/js/redux/actions.js
+++ b/blueocean-dashboard/src/main/js/redux/actions.js
@@ -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');
diff --git a/blueocean-dashboard/src/main/js/util/UrlUtils.js b/blueocean-dashboard/src/main/js/util/UrlUtils.js
index 834ed234..ae2ff650 100644
--- a/blueocean-dashboard/src/main/js/util/UrlUtils.js
+++ b/blueocean-dashboard/src/main/js/util/UrlUtils.js
@@ -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,
diff --git a/blueocean-dashboard/src/main/less/core.less b/blueocean-dashboard/src/main/less/core.less
index 85a40eed..c435de36 100644
--- a/blueocean-dashboard/src/main/less/core.less
+++ b/blueocean-dashboard/src/main/less/core.less
@@ -164,3 +164,29 @@
display: flex;
align-items: center;
}
+
+.logConsole .result-item-children {
+ background-color: @pre-bg;
+ 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;
+}
+
diff --git a/blueocean-dashboard/src/main/less/variables.less b/blueocean-dashboard/src/main/less/variables.less
index ba8357fc..5bc2e25b 100644
--- a/blueocean-dashboard/src/main/less/variables.less
+++ b/blueocean-dashboard/src/main/less/variables.less
@@ -1,2 +1,6 @@
@blueocean-blue: #4A90E2;
@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;
diff --git a/blueocean-dashboard/src/test/js/testResult-spec.js b/blueocean-dashboard/src/test/js/testResult-spec.js
index 5daf13b4..14560bf1 100644
--- a/blueocean-dashboard/src/test/js/testResult-spec.js
+++ b/blueocean-dashboard/src/test/js/testResult-spec.js
@@ -62,6 +62,25 @@ describe("TestResults", () => {
assert.equal(newFailed, 1);
});
+ it("Handles REGRESSION case", () => {
+ var failures = {
+ "_class":"hudson.tasks.junit.TestResult",
+ "duration":0.008, "empty":false, "failCount":3, "passCount":0, "skipCount":0, "suites":[
+ { "duration":0, "id":null, "name":"failure.TestThisWontFail", "stderr":null, "stdout":null, "timestamp":null, "cases": [
+ {"age":5,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest2","skipped":false,"skippedMessage":null,"status":"FAILED","stderr":null,"stdout":null},
+ {"age":2,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest3","skipped":false,"skippedMessage":null,"status":"REGRESSION","stderr":null,"stdout":null},
+ {"age":1,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest4","skipped":false,"skippedMessage":null,"status":"FAILED","stderr":null,"stdout":null},
+ ],
+ }]};
+
+ let wrapper = shallow(
);
+ const newFailed = wrapper.find('.new-failure-block h4').text();
+ assert.equal(newFailed, 'New failing - 2');
+
+ const failed = wrapper.find('.existing-failure-block h4').text();
+ assert.equal(failed, 'Existing failures - 1');
+ });
+
it("All passing shown", () => {
let wrapper = shallow(
);
let isDone = wrapper.html().indexOf('done_all') > 0;
@@ -78,7 +97,25 @@ describe("TestResults", () => {
}]};
wrapper = shallow(
);
- isDone = wrapper.html().indexOf('done_all') > 0;
- assert(isDone, "Done all not found, when should be");
+ let html = wrapper.html();
+ assert(html.indexOf('done_all') > 0, "Done all not found, when should be");
+ assert(html.indexOf('fixed-block') < 0, "No fixed tests!");
+ });
+
+ it("All passing and fixed shown", () => {
+ var successWithFixed = {
+ "_class":"hudson.tasks.junit.TestResult",
+ "duration":0.008, "empty":false, "failCount":0, "passCount":3, "skipCount":0, "suites":[
+ { "duration":0, "id":null, "name":"failure.TestThisWontFail", "stderr":null, "stdout":null, "timestamp":null, "cases": [
+ {"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest2","skipped":false,"skippedMessage":null,"status":"FIXED","stderr":null,"stdout":null},
+ {"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest3","skipped":false,"skippedMessage":null,"status":"PASSED","stderr":null,"stdout":null},
+ {"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest4","skipped":false,"skippedMessage":null,"status":"PASSED","stderr":null,"stdout":null},
+ ],
+ }]};
+
+ let wrapper = shallow(
);
+ let html = wrapper.html();
+ assert(html.indexOf('done_all') > 0, "Done all not found, when should be");
+ assert(html.indexOf('fixed-block') > 0, "Should have fixed tests!");
});
});
diff --git a/blueocean-personalization/src/test/js/components/PipelineCard-spec.jsx b/blueocean-personalization/src/test/js/components/PipelineCard-spec.jsx
index 632e000a..e1a201c7 100644
--- a/blueocean-personalization/src/test/js/components/PipelineCard-spec.jsx
+++ b/blueocean-personalization/src/test/js/components/PipelineCard-spec.jsx
@@ -26,7 +26,7 @@ describe('PipelineCard', () => {
assert.equal(wrapper.find('LiveStatusIndicator').length, 1);
assert.equal(wrapper.find('.name').length, 1);
- assert.equal(wrapper.find('.name').text(), 'Jenkins / blueocean');
+ assert.equal(wrapper.find('.name').text(), '
');
assert.equal(wrapper.find('.branch').length, 1);
assert.equal(wrapper.find('.branchText').text(), 'feature/JENKINS-123');
assert.equal(wrapper.find('.commit').length, 1);
diff --git a/blueocean-web/package.json b/blueocean-web/package.json
index be0f2894..4e4cdf69 100644
--- a/blueocean-web/package.json
+++ b/blueocean-web/package.json
@@ -25,7 +25,7 @@
"zombie": "^4.2.1"
},
"dependencies": {
- "@jenkins-cd/design-language": "0.0.64",
+ "@jenkins-cd/design-language": "0.0.65",
"@jenkins-cd/js-extensions": "0.0.19",
"@jenkins-cd/js-modules": "0.0.5",
"history": "2.0.2",