diff options
author | Tristan Cacqueray <tdecacqu@redhat.com> | 2019-02-13 04:14:02 +0000 |
---|---|---|
committer | Tristan Cacqueray <tdecacqu@redhat.com> | 2019-08-08 22:20:39 +0000 |
commit | 5049023d41dfdb9115df50794d927c1b8841d582 (patch) | |
tree | ff0f5c4a944e025db87000298ffd64a62ec4a203 | |
parent | 459ee2f1d3c18ef264b473d75850ffdec6fd163e (diff) | |
download | zuul-5049023d41dfdb9115df50794d927c1b8841d582.tar.gz |
web: add buildset page
This change adds a new page to display a single buildset with its
associated builds.
Change-Id: I664d0db06456c906cfabb6a2b1da613d9aebf419
-rw-r--r-- | web/src/actions/build.js | 50 | ||||
-rw-r--r-- | web/src/api.js | 5 | ||||
-rw-r--r-- | web/src/containers/build/Buildset.jsx | 123 | ||||
-rw-r--r-- | web/src/pages/Buildset.jsx | 58 | ||||
-rw-r--r-- | web/src/pages/Buildsets.jsx | 12 | ||||
-rw-r--r-- | web/src/reducers/build.js | 13 | ||||
-rw-r--r-- | web/src/routes.js | 5 |
7 files changed, 266 insertions, 0 deletions
diff --git a/web/src/actions/build.js b/web/src/actions/build.js index e29ecd624..d521ae8d1 100644 --- a/web/src/actions/build.js +++ b/web/src/actions/build.js @@ -20,6 +20,10 @@ export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST' export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS' export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL' +export const BUILDSET_FETCH_REQUEST = 'BUILDSET_FETCH_REQUEST' +export const BUILDSET_FETCH_SUCCESS = 'BUILDSET_FETCH_SUCCESS' +export const BUILDSET_FETCH_FAIL = 'BUILDSET_FETCH_FAIL' + export const BUILD_OUTPUT_REQUEST = 'BUILD_OUTPUT_FETCH_REQUEST' export const BUILD_OUTPUT_SUCCESS = 'BUILD_OUTPUT_FETCH_SUCCESS' export const BUILD_OUTPUT_FAIL = 'BUILD_OUTPUT_FETCH_FAIL' @@ -201,3 +205,49 @@ export const fetchBuildIfNeeded = (tenant, buildId, force) => (dispatch, getStat dispatch(fetchBuildManifest(buildId, getState(), force)) }) } + +export const requestBuildset = () => ({ + type: BUILDSET_FETCH_REQUEST +}) + +export const receiveBuildset = (buildsetId, buildset) => ({ + type: BUILDSET_FETCH_SUCCESS, + buildsetId: buildsetId, + buildset: buildset, + receivedAt: Date.now() +}) + +const failedBuildset = error => ({ + type: BUILDSET_FETCH_FAIL, + error +}) + +const fetchBuildset = (tenant, buildset) => dispatch => { + dispatch(requestBuildset()) + return API.fetchBuildset(tenant.apiPrefix, buildset) + .then(response => { + response.data.builds.forEach(build => { + dispatch(receiveBuild(build.uuid, build)) + }) + dispatch(receiveBuildset(buildset, response.data)) + }) + .catch(error => dispatch(failedBuildset(error))) +} + +const shouldFetchBuildset = (buildsetId, state) => { + const buildset = state.build.buildsets[buildsetId] + if (!buildset) { + return true + } + if (buildset.isFetching) { + return false + } + return false +} + +export const fetchBuildsetIfNeeded = (tenant, buildsetId, force) => ( + dispatch, getState) => { + if (force || shouldFetchBuildset(buildsetId, getState())) { + return dispatch(fetchBuildset(tenant, buildsetId)) + } +} diff --git a/web/src/api.js b/web/src/api.js index fcd3066d1..776a04e05 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -49,6 +49,7 @@ function getHomepageUrl (url) { // Remove known sub-path const subDir = [ '/build/', + '/buildset/', '/job/', '/project/', '/stream/', @@ -131,6 +132,9 @@ function fetchBuilds (apiPrefix, queryString) { } return Axios.get(apiUrl + apiPrefix + path) } +function fetchBuildset (apiPrefix, buildsetId) { + return Axios.get(apiUrl + apiPrefix + 'buildset/' + buildsetId) +} function fetchBuildsets (apiPrefix, queryString) { let path = 'buildsets' if (queryString) { @@ -166,6 +170,7 @@ export { fetchStatus, fetchBuild, fetchBuilds, + fetchBuildset, fetchBuildsets, fetchProject, fetchProjects, diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx new file mode 100644 index 000000000..5d2a2a31b --- /dev/null +++ b/web/src/containers/build/Buildset.jsx @@ -0,0 +1,123 @@ +// Copyright 2019 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 { Panel } from 'react-bootstrap' +import * as moment from 'moment' + + +class Buildset extends React.Component { + static propTypes = { + buildset: PropTypes.object, + tenant: PropTypes.object, + } + + render () { + const { buildset } = this.props + const rows = [] + const myColumns = [ + 'change', 'project', 'branch', 'pipeline', 'result', 'message' + ] + const buildRows = [] + const buildColumns = [ + 'job', 'result', 'voting', 'duration' + ] + + myColumns.forEach(column => { + let label = column + let value = buildset[column] + if (column === 'change') { + value = ( + <a href={buildset.ref_url}> + {buildset.change},{buildset.patchset} + </a> + ) + } + if (value) { + rows.push({key: label, value: value}) + } + }) + + buildset.builds.forEach(build => { + const row = [] + buildColumns.forEach(column => { + if (column === 'job') { + row.push(build.job_name) + } else if (column === 'duration') { + row.push(moment.duration(build.duration, 'seconds').humanize()) + } else if (column === 'voting') { + row.push(build.voting ? 'true' : 'false') + } else if (column === 'result') { + row.push(<Link + to={this.props.tenant.linkPrefix + '/build/' + build.uuid}> + {build.result} + </Link>) + } else { + row.push(build[column]) + } + }) + buildRows.push(row) + }) + + return ( + <React.Fragment> + <Panel> + <Panel.Heading>Buildset result {buildset.uuid}</Panel.Heading> + <Panel.Body> + <table className="table table-striped table-bordered"> + <tbody> + {rows.map(item => ( + <tr key={item.key}> + <td>{item.key}</td> + <td>{item.value}</td> + </tr> + ))} + </tbody> + </table> + </Panel.Body> + </Panel> + <Panel> + <Panel.Heading>Builds</Panel.Heading> + <Panel.Body> + <table className="table table-striped table-bordered"> + <thead> + <tr> + {buildColumns.map(item => ( + <td key={item}>{item}</td> + ))} + </tr> + </thead> + <tbody> + {buildset.builds.map((item, idx) => ( + <tr key={idx} className={item.result === 'SUCCESS' ? 'success': 'warning'}> + {buildRows[idx].map((item, idx) => ( + <td key={idx}>{item}</td> + ))} + </tr> + ))} + </tbody> + </table> + </Panel.Body> + </Panel> + + </React.Fragment> + ) + } +} + + +export default connect(state => ({tenant: state.tenant}))(Buildset) diff --git a/web/src/pages/Buildset.jsx b/web/src/pages/Buildset.jsx new file mode 100644 index 000000000..61d464040 --- /dev/null +++ b/web/src/pages/Buildset.jsx @@ -0,0 +1,58 @@ +// Copyright 2019 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 PropTypes from 'prop-types' + +import { fetchBuildsetIfNeeded } from '../actions/build' +import Refreshable from '../containers/Refreshable' +import Buildset from '../containers/build/Buildset' + + +class BuildsetPage extends Refreshable { + static propTypes = { + match: PropTypes.object.isRequired, + remoteData: PropTypes.object, + tenant: PropTypes.object + } + + updateData = (force) => { + this.props.dispatch(fetchBuildsetIfNeeded( + this.props.tenant, this.props.match.params.buildsetId, force)) + } + + componentDidMount () { + document.title = 'Zuul Buildset' + super.componentDidMount() + } + + render () { + const { remoteData } = this.props + const buildset = remoteData.buildsets[this.props.match.params.buildsetId] + return ( + <React.Fragment> + <div style={{float: 'right'}}> + {this.renderSpinner()} + </div> + {buildset && <Buildset buildset={buildset}/>} + </React.Fragment> + ) + } +} + +export default connect(state => ({ + tenant: state.tenant, + remoteData: state.build, +}))(BuildsetPage) diff --git a/web/src/pages/Buildsets.jsx b/web/src/pages/Buildsets.jsx index 13e68fb78..70434d5ba 100644 --- a/web/src/pages/Buildsets.jsx +++ b/web/src/pages/Buildsets.jsx @@ -15,6 +15,7 @@ import * as React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' +import { Link } from 'react-router-dom' import { Table } from 'patternfly-react' import { fetchBuildsets } from '../api' @@ -76,6 +77,15 @@ class BuildsetsPage extends TableFilters { </a> </Table.Cell> ) + const linkBuildsetFormat = (value, rowdata) => ( + <Table.Cell> + <Link + to={this.props.tenant.linkPrefix + + '/buildset/' + rowdata.rowData.uuid}> + {value} + </Link> + </Table.Cell> + ) this.columns = [] this.filterTypes = [] const myColumns = [ @@ -89,6 +99,8 @@ class BuildsetsPage extends TableFilters { let formatter = cellFormat if (column === 'change') { formatter = linkChangeFormat + } else if (column === 'result') { + formatter = linkBuildsetFormat } const label = column.charAt(0).toUpperCase() + column.slice(1) this.columns.push({ diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js index 084e9a36f..6ca8a9ef0 100644 --- a/web/src/reducers/build.js +++ b/web/src/reducers/build.js @@ -19,6 +19,10 @@ import { BUILD_FETCH_REQUEST, BUILD_FETCH_SUCCESS, + BUILDSET_FETCH_FAIL, + BUILDSET_FETCH_REQUEST, + BUILDSET_FETCH_SUCCESS, + BUILD_OUTPUT_FAIL, BUILD_OUTPUT_REQUEST, BUILD_OUTPUT_SUCCESS, @@ -34,15 +38,24 @@ export default (state = { isFetchingOutput: false, isFetchingManifest: false, builds: {}, + buildsets: {}, }, action) => { switch (action.type) { case BUILD_FETCH_REQUEST: + case BUILDSET_FETCH_REQUEST: return update(state, {$merge: {isFetching: true}}) case BUILD_FETCH_SUCCESS: state.builds = update( state.builds, {$merge: {[action.buildId]: action.build}}) return update(state, {$merge: {isFetching: false}}) + case BUILDSET_FETCH_SUCCESS: + return update(state, {$merge: { + isFetching: false, + buildsets: update(state.buildsets, {$merge: { + [action.buildsetId]: action.buildset}}) + }}) case BUILD_FETCH_FAIL: + case BUILDSET_FETCH_FAIL: return update(state, {$merge: {isFetching: false}}) case BUILD_OUTPUT_REQUEST: diff --git a/web/src/routes.js b/web/src/routes.js index 5eef4770c..b07572f08 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -25,6 +25,7 @@ import BuildLogsPage from './pages/BuildLogs' import BuildConsolePage from './pages/BuildConsole' import LogFilePage from './pages/LogFile' import BuildsPage from './pages/Builds' +import BuildsetPage from './pages/Buildset' import BuildsetsPage from './pages/Buildsets' import ConfigErrorsPage from './pages/ConfigErrors' import TenantsPage from './pages/Tenants' @@ -104,6 +105,10 @@ const routes = () => [ component: LogFilePage }, { + to: '/buildset/:buildsetId', + component: BuildsetPage + }, + { to: '/config-errors', component: ConfigErrorsPage, }, |