diff --git a/web/src/containers/build/Artifact.jsx b/web/src/containers/build/Artifact.jsx index 23d4c032a6..e9d6243a4b 100644 --- a/web/src/containers/build/Artifact.jsx +++ b/web/src/containers/build/Artifact.jsx @@ -43,13 +43,13 @@ class Artifact extends React.Component { class ArtifactList extends React.Component { static propTypes = { - build: PropTypes.object.isRequired + artifacts: PropTypes.array.isRequired } render() { - const { build } = this.props + const { artifacts } = this.props - const nodes = build.artifacts.map((artifact, index) => { + const nodes = artifacts.map((artifact, index) => { const node = {text: {artifact.name}, icon: null} if (artifact.metadata) { @@ -60,11 +60,14 @@ class ArtifactList extends React.Component { }) return ( -
- -
+ <> +
+
+ +
+ ) } } diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx index 32829f3def..be09272ce7 100644 --- a/web/src/containers/build/Build.jsx +++ b/web/src/containers/build/Build.jsx @@ -16,68 +16,238 @@ import * as React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { Link } from 'react-router-dom' +import { + Flex, + FlexItem, + List, + ListItem, + Title, +} from '@patternfly/react-core' +import { + BookIcon, + BuildIcon, + CodeBranchIcon, + CodeIcon, + CubeIcon, + FileCodeIcon, + FingerprintIcon, + HistoryIcon, + OutlinedCalendarAltIcon, + OutlinedClockIcon, + StreamIcon, +} from '@patternfly/react-icons' +import * as moment from 'moment' +import 'moment-duration-format' -import Summary from './Summary' -import Manifest from './Manifest' -import Console from './Console' +import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc' +import { ExternalLink } from '../../Misc' -class Build extends React.Component { - static propTypes = { - build: PropTypes.object, - tenant: PropTypes.object, - active: PropTypes.string, - hash: PropTypes.array, - } - - render () { - const { build, active, hash } = this.props - - return ( -
-

Build result {build.uuid}

-
- -
- {/* NOTE (felix): Since I'm already working on a PF4 change for - this file, I don't want to change too much here for now and - just make it compatible to the improved routing solution. - */} - {active === 'summary' && } - {active === 'logs' && build && build.manifest && ( - - )} - {active === 'console' && build && build.output && ( - 0?hash:undefined} - /> - )} -
-
-
- ) - } + } + /> + } + value={ + + View buildset result + + } + /> + )} + } + value={ + build.log_url ? ( + View log + ) : ( + + No log available + + ) + } + /> + + + + + + ) } +Build.propTypes = { + build: PropTypes.object, + tenant: PropTypes.object, + hash: PropTypes.array, + timezone: PropTypes.string, + fetchable: PropTypes.node, +} -export default connect(state => ({tenant: state.tenant}))(Build) +export default connect((state) => ({ + tenant: state.tenant, + timezone: state.timezone, +}))(Build) diff --git a/web/src/containers/build/BuildOutput.jsx b/web/src/containers/build/BuildOutput.jsx index cdd7489315..4e5add8265 100644 --- a/web/src/containers/build/BuildOutput.jsx +++ b/web/src/containers/build/BuildOutput.jsx @@ -108,6 +108,7 @@ class BuildOutput extends React.Component { const { output } = this.props return ( +
{Object.entries(output) .filter(([, values]) => values.failed.length > 0) diff --git a/web/src/containers/build/Summary.jsx b/web/src/containers/build/Summary.jsx deleted file mode 100644 index dbccdbc598..0000000000 --- a/web/src/containers/build/Summary.jsx +++ /dev/null @@ -1,131 +0,0 @@ -// 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 PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { Link } from 'react-router-dom' - -import ArtifactList from './Artifact' -import BuildOutput from './BuildOutput' - -import * as moment from 'moment' -import 'moment-duration-format' - - -class Summary extends React.Component { - static propTypes = { - build: PropTypes.object, - tenant: PropTypes.object, - timezone: PropTypes.string, - } - - render () { - const { build } = this.props - const rows = [] - const myColumns = [ - 'job_name', 'result', 'buildset', 'voting', - 'pipeline', 'start_time', 'end_time', 'duration', - 'project', 'branch', 'change', 'patchset', 'oldrev', 'newrev', - 'ref', 'new_rev', 'ref_url', 'log_url', 'event_id'] - - if (!build.buildset) { - // Safely handle missing buildset information - myColumns.splice(myColumns.indexOf('buildset'), 1) - } - - myColumns.forEach(column => { - let label = column - let value = build[column] - if (column === 'job_name') { - label = 'job' - value = ( - - - {value} - - - - {value.uuid} - - ) - } - if (column === 'voting') { - if (value) { - value = 'true' - } else { - value = 'false' - } - } - if (column === 'start_time' || column === 'end_time') { - value = moment.utc(value).tz(this.props.timezone).format('YYYY-MM-DD HH:mm:ss') - } - if (column === 'duration') { - value = moment.duration(value, 'seconds') - .format('h [hr] m [min] s [sec]') - } - if (value && (column === 'log_url' || column === 'ref_url')) { - value = {value} - } - if (column === 'log_url') { - label = 'log url' - if (build.manifest && build.manifest.index_links) { - value = {value} - } else { - value = {value} - } - } - if (column === 'ref_url') { - label = 'ref url' - value = {value} - } - if (column === 'event_id') { - label = 'event id' - } - if (value) { - rows.push({key: label, value: value}) - } - }) - return ( - -
- - - {rows.map(item => ( - - - - - ))} - -
{item.key}{item.value}
-

Artifacts

- -

Results

- {build.hosts && } -
- ) - } -} - - -export default connect(state => ({tenant: state.tenant, timezone: state.timezone}))(Summary) diff --git a/web/src/pages/Build.jsx b/web/src/pages/Build.jsx index d4975d9725..edf34e4326 100644 --- a/web/src/pages/Build.jsx +++ b/web/src/pages/Build.jsx @@ -14,13 +14,36 @@ import * as React from 'react' import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' import PropTypes from 'prop-types' -import { PageSection, PageSectionVariants } from '@patternfly/react-core' +import { + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + PageSection, + PageSectionVariants, + Tab, + Tabs, + TabTitleIcon, + TabTitleText, + Title, +} from '@patternfly/react-core' +import { + BuildIcon, + FileArchiveIcon, + FileCodeIcon, + TerminalIcon, + PollIcon, +} from '@patternfly/react-icons' import { fetchBuildIfNeeded } from '../actions/build' -import { Fetchable } from '../containers/Fetching' +import { EmptyPage } from '../containers/Errors' +import { Fetchable, 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' class BuildPage extends React.Component { static propTypes = { @@ -30,45 +53,220 @@ class BuildPage extends React.Component { dispatch: PropTypes.func, activeTab: PropTypes.string.isRequired, location: PropTypes.object, + history: PropTypes.object, } updateData = (force) => { - this.props.dispatch(fetchBuildIfNeeded( - this.props.tenant, this.props.match.params.buildId, null, force)) + this.props.dispatch( + fetchBuildIfNeeded( + this.props.tenant, + this.props.match.params.buildId, + null, + force + ) + ) } - componentDidMount () { + componentDidMount() { document.title = 'Zuul Build' if (this.props.tenant.name) { this.updateData() } } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { if (this.props.tenant.name !== prevProps.tenant.name) { this.updateData() } } - render () { - const { remoteData, activeTab, location } = this.props + 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: + // results + history.push(`${tenant.linkPrefix}/build/${build.uuid}`) + } + } + + render() { + const { remoteData, activeTab, location, tenant } = this.props const build = remoteData.builds[this.props.match.params.buildId] const hash = location.hash.substring(1).split('/') + + if (!build && remoteData.isFetching) { + return + } + + if (!build) { + return ( + + ) + } + + const fetchable = ( + + ) + + const resultsTabContent = + !build.hosts && remoteData.isFetchingOutput ? ( + + ) : build.hosts ? ( + + ) : ( + + + + This build does not provide any results + + + ) + + const artifactsTabContent = build.artifacts.length ? ( + + ) : ( + + + + This build does not provide any artifacts + + + ) + + const logsTabContent = + !build.manifest && remoteData.isFetchingManifest ? ( + + ) : build.manifest ? ( + + ) : ( + + + + This build does not provide any logs + + + ) + + const consoleTabContent = + !build.output && remoteData.isFetchingOutput ? ( + + ) : build.output ? ( + 0 ? hash : undefined} + /> + ) : ( + + + + This build does not provide any console information + + + ) + return ( - - - + + - {build && } - + + this.handleTabClick(tabIndex, build)} + > + + + + + Results + + } + > + {resultsTabContent} + + + + + + Artifacts + + } + > + {artifactsTabContent} + + + + + + Logs + + } + > + {logsTabContent} + + + + + + Console + + } + > + {consoleTabContent} + + + + ) } } -export default connect(state => ({ +export default connect((state) => ({ tenant: state.tenant, remoteData: state.build, -}))(BuildPage) +}))(withRouter(BuildPage)) diff --git a/web/src/routes.js b/web/src/routes.js index 1a0f2aaf59..07569dc22a 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -89,7 +89,12 @@ const routes = () => [ { to: '/build/:buildId', component: BuildPage, - props: {'activeTab': 'summary'}, + props: {'activeTab': 'results'}, + }, + { + to: '/build/:buildId/artifacts', + component: BuildPage, + props: {'activeTab': 'artifacts'}, }, { to: '/build/:buildId/logs',