// Copyright 2018 Red Hat, Inc // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. import * as React from 'react' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import PropTypes from 'prop-types' import { parse } from 'query-string' import { Button, EmptyState, EmptyStateVariant, EmptyStateIcon, PageSection, PageSectionVariants, Tab, Tabs, TabTitleIcon, TabTitleText, Title, } from '@patternfly/react-core' import { ArrowUpIcon, BuildIcon, FileArchiveIcon, FileCodeIcon, TerminalIcon, PollIcon, ExclamationIcon, } from '@patternfly/react-icons' import { fetchBuildAllInfo } from '../actions/build' import { fetchLogfile } from '../actions/logfile' import { EmptyPage } from '../containers/Errors' import { Fetching } from '../containers/Fetching' import ArtifactList from '../containers/build/Artifact' import Build from '../containers/build/Build' import BuildOutput from '../containers/build/BuildOutput' import Console from '../containers/build/Console' import Manifest from '../containers/build/Manifest' import LogFile from '../containers/logfile/LogFile' class BuildPage extends React.Component { static propTypes = { match: PropTypes.object.isRequired, build: PropTypes.object, logfile: PropTypes.object, isFetching: PropTypes.bool.isRequired, isFetchingManifest: PropTypes.bool.isRequired, isFetchingOutput: PropTypes.bool.isRequired, isFetchingLogfile: PropTypes.bool.isRequired, tenant: PropTypes.object.isRequired, fetchBuildAllInfo: PropTypes.func.isRequired, activeTab: PropTypes.string.isRequired, location: PropTypes.object.isRequired, history: PropTypes.object.isRequired, preferences: PropTypes.object, } state = { topOfPageVisible: true, } updateData = () => { // The related fetchBuild...() methods won't do anything if the data is // already available in the local state, so just call them. this.props.fetchBuildAllInfo( this.props.tenant, this.props.match.params.buildId, this.props.match.params.file ) } onScroll = () => { this.setState({topOfPageVisible: window.scrollY === 0}) } componentDidMount() { document.title = 'Zuul Build' if (this.props.tenant.name) { this.updateData() } window.addEventListener('scroll', this.onScroll) } componentDidUpdate(prevProps) { if (this.props.tenant.name !== prevProps.tenant.name) { this.updateData() } } handleTabClick = (tabIndex, build) => { // Usually tabs should only be used to display content in-page and not link // to other pages: // "Tabs are used to present a set on tabs for organizing content on a // .page. It must always be used together with a tab content component." // https://www.patternfly.org/v4/documentation/react/components/tabs // But as want to be able to reach every tab's content via a dedicated URL // while having the look and feel of tabs, we could hijack this onClick // handler to do the link/routing stuff. const { history, tenant } = this.props switch (tabIndex) { case 'artifacts': history.push(`${tenant.linkPrefix}/build/${build.uuid}/artifacts`) break case 'logs': history.push(`${tenant.linkPrefix}/build/${build.uuid}/logs`) break case 'console': history.push(`${tenant.linkPrefix}/build/${build.uuid}/console`) break default: // task summary history.push(`${tenant.linkPrefix}/build/${build.uuid}`) } } handleBreadcrumbItemClick = () => { // Simply link back to the logs tab without an active logfile this.handleTabClick('logs', this.props.build) } render() { const { build, logfile, isFetching, isFetchingManifest, isFetchingOutput, isFetchingLogfile, activeTab, history, location, tenant, } = this.props const hash = location.hash.substring(1).split('/') const severity = parseInt(parse(location.search).severity) // Get the logfile from react-routers URL parameters const logfileName = this.props.match.params.file // In case the build is not available yet (before the fetching started) or // is currently fetching. if (build === undefined || isFetching) { return } // The build is null, meaning it couldn't be found. if (!build) { return ( ) } const resultsTabContent = build.hosts === undefined || isFetchingOutput ? ( ) : build.hosts ? ( ) : build.error_detail ? ( <>

Error: {build.error_detail}

) : ( This build does not provide any results ) const artifactsTabContent = build.artifacts.length ? ( ) : ( This build does not provide any artifacts ) let logsTabContent = null if (build.manifest === undefined || isFetchingManifest) { logsTabContent = } else if (logfileName) { logsTabContent = ( ) } else if (build.manifest) { logsTabContent = } else { logsTabContent = ( This build does not provide any logs ) } const consoleTabContent = build.output === undefined || isFetchingOutput ? ( ) : build.output ? ( 0 ? hash : undefined} /> ) : ( This build does not provide any console information ) return ( <> this.handleTabClick(tabIndex, build)} > Task Summary } > {resultsTabContent} Artifacts } > {artifactsTabContent} Logs } > {logsTabContent} Console } > {consoleTabContent} {!this.state.topOfPageVisible && ( )} ) } } function scrollToTop() { window.scrollTo(0,0) document.activeElement.blur() } function mapStateToProps(state, ownProps) { const buildId = ownProps.match.params.buildId // JavaScript will return undefined in case the key is missing in the // dict/object. const buildFromState = state.build.builds[buildId] // Only copy the build if it's a valid object. In case it is null or undefined // directly assign the value. The cloning is necessary as we mutate the build // in the next step when adding the manifest, output, ... information. const build = buildFromState ? { ...buildFromState } : buildFromState // If the build is available, extend it with more information. All those // values will be undefined if they are not part of the dict/object. if (build) { build.manifest = state.build.manifests[buildId] build.output = state.build.outputs[buildId] build.hosts = state.build.hosts[buildId] build.errorIds = state.build.errorIds[buildId] } const logfileName = ownProps.match.params.file const logfile = buildId in state.logfile.files ? state.logfile.files[buildId][logfileName] : undefined return { build, logfile, tenant: state.tenant, isFetching: state.build.isFetching, isFetchingManifest: state.build.isFetchingManifest, isFetchingOutput: state.build.isFetchingOutput, isFetchingLogfile: state.logfile.isFetching, preferences: state.preferences, } } const mapDispatchToProps = { fetchBuildAllInfo, fetchLogfile } export default connect( mapStateToProps, mapDispatchToProps )(withRouter(BuildPage))