import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import Extensions from '@jenkins-cd/js-extensions'; import LogConsole from './LogConsole'; import * as sse from '@jenkins-cd/sse-gateway'; import { EmptyStateView } from '@jenkins-cd/design-language'; import { Icon } from 'react-material-icons-blue'; import LogToolbar from './LogToolbar'; import Steps from './Steps'; import { steps as stepsSelector, logs as logSelector, node as nodeSelector, nodes as nodesSelector, actions, connect, createSelector, } from '../redux'; import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl, calculateFetchAll } from '../util/UrlUtils'; import { calculateNode } from '../util/KaraokeHelper'; const { string, object, any, func } = PropTypes; const queuedState = () => (

Waiting for run to start.

); export class RunDetailsPipeline extends Component { constructor(props) { super(props); // we do not want to follow any builds that are finished this.state = { followAlong: props && props.result && props.result.state !== 'FINISHED' }; this.listener = {}; this._handleKeys = this._handleKeys.bind(this); this.onScrollHandler = this.onScrollHandler.bind(this); } componentWillMount() { const { fetchNodes, fetchLog, result } = this.props; this.mergedConfig = this.generateConfig(this.props); if (!result.isQueued()) { // It should really be using capability using /rest/classes API const supportsNode = result && result._class === 'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl'; if (supportsNode) { fetchNodes(this.mergedConfig); } else { // console.log('fetch the log directly') const logGeneral = calculateRunLogURLObject(this.mergedConfig); // fetchAll indicates whether we want all logs const fetchAll = this.mergedConfig.fetchAll; fetchLog({ ...logGeneral, fetchAll }); } } this.listener.sse = sse.subscribe('pipeline', this._onSseEvent); } componentDidMount() { const { result } = this.props; if (!result.isQueued()) { // determine scroll area const domNode = ReactDOM.findDOMNode(this.refs.scrollArea); // add both listemer, one to the scroll area and another to the whole document domNode.addEventListener('wheel', this.onScrollHandler, false); document.addEventListener('keydown', this._handleKeys, false); } } componentWillReceiveProps(nextProps) { if (this.props.result.isQueued()) { return; } const followAlong = this.state.followAlong; this.mergedConfig = this.generateConfig({ ...nextProps, followAlong }); // we do not want any timeouts if we are not doing karaoke if (!this.state.followAlong && this.timeout) { clearTimeout(this.timeout); } // calculate if we need to trigger any actions to get into the right state (is plain js for testing reasons) const nodeAction = calculateNode(this.props, nextProps, this.mergedConfig); if (nodeAction && nodeAction.action) { // use updated config this.mergedConfig = nodeAction.config; // we may need to stop following if (this.state.followAlong !== nodeAction.state.followAlong) { this.setState({ followAlong: nodeAction.state.followAlong }); } // 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 || 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 const newStart = log.newStart; if (Number(newStart) > 0) { // in case we doing karaoke we want to see more logs if (this.state.followAlong) { // kill current timeout if any clearTimeout(this.timeout); // we need to get mpre input from the log stream 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() { if (this.listener.sse) { sse.unsubscribe(this.listener.sse); delete this.listener.sse; } if (this.props.result.isQueued()) { return; } const domNode = ReactDOM.findDOMNode(this.refs.scrollArea); 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 // we bail out on scroll up onScrollHandler(elem) { if (elem.deltaY < 0 && this.state.followAlong) { this.setState({ followAlong: false }); } } // Listen for pipeline flow node events. // We filter them only for steps and the end event all other we let pass _onSseEvent(event) { const { fetchNodes, fetchSteps } = this.props; const jenkinsEvent = event.jenkins_event; // we are using try/catch to throw an early out error try { if (event.pipeline_run_id !== this.props.result.id) { // console.log('early out'); throw new Error('exit'); } // we turn on refetch so we always fetch a new Node result const refetch = true; switch (jenkinsEvent) { case 'pipeline_step': { // we are not using an early out for the events since we want to refresh the node if we finished if (this.state.followAlong) { // if we do it means we want karaoke // if the step_stage_id has changed we need to change the focus if (event.pipeline_step_stage_id !== this.mergedConfig.node) { // console.log('nodes fetching via sse triggered'); delete this.mergedConfig.node; fetchNodes({ ...this.mergedConfig, refetch }); } else { // console.log('only steps fetching via sse triggered'); fetchSteps({ ...this.mergedConfig, refetch }); } } break; } case 'pipeline_end': { // we always want to refresh if the run has finished fetchNodes({ ...this.mergedConfig, refetch }); break; } default: { // //console.log(event); } } } catch (e) { // we only ignore the exit error if (e.message !== 'exit') { throw e; } } } // we bail out on arrow_up key _handleKeys(event) { if (event.keyCode === 38 && this.state.followAlong) { this.setState({ followAlong: false }); } } generateConfig(props) { const { config = {} } = this.context; const followAlong = this.state.followAlong; const { isMultiBranch, params } = 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) { nodeReducer = { id: null, displayName: 'Steps' }; } // if we have a node param we do not want the calculation of the focused node const node = params.node || nodeReducer.id; // Merge config return { ...config, name: params.pipeline, branch: params.branch, runId: params.runId, isMultiBranch, node, nodeReducer, followAlong, fetchAll, }; } render() { const { location, router } = this.context; const { isMultiBranch, steps, nodes, logs, result: run, params } = this.props; if (run.isQueued()) { return queuedState(); } const resultRun = run.isCompleted() ? run.state : run.result; const followAlong = this.state.followAlong; // in certain cases we want that the log component will scroll to the end of a log const scrollToBottom = resultRun.toLowerCase() === 'failure' || (resultRun.toLowerCase() === 'running' && followAlong) ; const nodeKey = calculateNodeBaseUrl(this.mergedConfig); const key = calculateStepsBaseUrl(this.mergedConfig); const logGeneral = calculateRunLogURLObject(this.mergedConfig); const log = logs ? logs[logGeneral.url] : null; let title = this.mergedConfig.nodeReducer.displayName; if (log) { title = 'Logs'; } else if (this.mergedConfig.nodeReducer.id !== null && title) { title = `Steps - ${title}`; } const currentSteps = steps ? steps[key] : null; // here we decide what to do next if somebody clicks on a flowNode const afterClick = (id) => { // get some information about the node the user clicked const nodeInfo = nodes[nodeKey].model.filter((item) => item.id === id)[0]; const pathname = location.pathname; let newPath; // if path ends with pipeline we simply use it if (pathname.endsWith('pipeline/')) { newPath = pathname; } else if (pathname.endsWith('pipeline')) { newPath = `${pathname}/`; } else { // remove last bits const pathArray = pathname.split('/'); pathArray.pop(); if (pathname.endsWith('/')) { pathArray.pop(); } pathArray.shift(); newPath = `${pathArray.join('/')}/`; } // we only want to redirect to the node if the node is finished if (nodeInfo.state === 'FINISHED') { newPath = `${newPath}${id}`; } // see whether we need to update the state if (nodeInfo.state === 'FINISHED' && followAlong) { this.setState({ followAlong: false }); } if (nodeInfo.state !== 'FINISHED' && !followAlong) { this.setState({ followAlong: true }); } router.push(newPath); }; 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; } const stepScrollAreaClass = `step-scroll-area ${followAlong ? 'follow-along-on' : 'follow-along-off'}`; return (
{ nodes && nodes[nodeKey] && } { shouldShowLogHeader && } { currentSteps && } { noSteps &&

There are no steps.

} { log && }
); } } RunDetailsPipeline.propTypes = { pipeline: object, isMultiBranch: any, params: object, result: object, fileName: string, url: string, fetchLog: func, fetchNodes: func, setNode: func, fetchSteps: func, cleanNodePointer: func, logs: object, steps: object, nodes: object, nodeReducer: object, }; RunDetailsPipeline.contextTypes = { config: object.isRequired, params: object, pipeline: object, router: object.isRequired, // From react-router location: object.isRequired, // From react-router }; const selectors = createSelector( [stepsSelector, logSelector, nodeSelector, nodesSelector], (steps, logs, nodeReducer, nodes) => ({ steps, logs, nodeReducer, nodes })); export default connect(selectors, actions)(RunDetailsPipeline);