diff --git a/blueocean-dashboard/package.json b/blueocean-dashboard/package.json index 4838c70f..5e493d59 100644 --- a/blueocean-dashboard/package.json +++ b/blueocean-dashboard/package.json @@ -35,7 +35,7 @@ "skin-deep": "^0.16.0" }, "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", "@jenkins-cd/sse-gateway": "0.0.6", diff --git a/blueocean-dashboard/src/main/js/components/LogConsole.jsx b/blueocean-dashboard/src/main/js/components/LogConsole.jsx index 6478edc7..32033379 100644 --- a/blueocean-dashboard/src/main/js/components/LogConsole.jsx +++ b/blueocean-dashboard/src/main/js/components/LogConsole.jsx @@ -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 ( + { hasMore &&
+ + Show complete log + +
} { lines.map((line, index) =>

- {line} + {line} +

)}
); } } @@ -129,6 +142,7 @@ LogConsole.propTypes = { scrollToAnchorTimeOut: func, scrollBottom: func, prefix: string, + hasMore: bool, }; export default scrollHelper(LogConsole); diff --git a/blueocean-dashboard/src/main/js/components/LogToolbar.jsx b/blueocean-dashboard/src/main/js/components/LogToolbar.jsx index ba244529..09329963 100644 --- a/blueocean-dashboard/src/main/js/components/LogToolbar.jsx +++ b/blueocean-dashboard/src/main/js/components/LogToolbar.jsx @@ -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 (
@@ -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",