summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTristan Cacqueray <tdecacqu@redhat.com>2019-02-13 04:14:02 +0000
committerTristan Cacqueray <tdecacqu@redhat.com>2019-08-08 22:20:39 +0000
commit5049023d41dfdb9115df50794d927c1b8841d582 (patch)
treeff0f5c4a944e025db87000298ffd64a62ec4a203
parent459ee2f1d3c18ef264b473d75850ffdec6fd163e (diff)
downloadzuul-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.js50
-rw-r--r--web/src/api.js5
-rw-r--r--web/src/containers/build/Buildset.jsx123
-rw-r--r--web/src/pages/Buildset.jsx58
-rw-r--r--web/src/pages/Buildsets.jsx12
-rw-r--r--web/src/reducers/build.js13
-rw-r--r--web/src/routes.js5
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,
},