diff --git a/releasenotes/notes/web-page-config-errors-f2f00d6d1eed9103.yaml b/releasenotes/notes/web-page-config-errors-f2f00d6d1eed9103.yaml
new file mode 100644
index 0000000000..825c401db1
--- /dev/null
+++ b/releasenotes/notes/web-page-config-errors-f2f00d6d1eed9103.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ A new Notification Drawer and a ConfigErrors page in the web interface
+ enable displaying the config-errors endpoint data.
diff --git a/web/src/App.jsx b/web/src/App.jsx
index 7ad7d8e603..5c22083ac1 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -20,23 +20,31 @@ import PropTypes from 'prop-types'
import { matchPath, withRouter } from 'react-router'
import { Link, Redirect, Route, Switch } from 'react-router-dom'
import { connect } from 'react-redux'
-import { Masthead } from 'patternfly-react'
+import {
+ Icon,
+ Masthead,
+ Notification,
+ NotificationDrawer,
+} from 'patternfly-react'
import logo from './images/logo.png'
import { routes } from './routes'
-import { setTenantAction } from './reducers'
+import { fetchConfigErrorsAction, setTenantAction } from './reducers'
class App extends React.Component {
static propTypes = {
+ configErrors: PropTypes.array,
info: PropTypes.object,
tenant: PropTypes.object,
location: PropTypes.object,
+ history: PropTypes.object,
dispatch: PropTypes.func
}
state = {
- menuCollapsed: true
+ menuCollapsed: true,
+ showErrors: false
}
onNavToggleClick = () => {
@@ -126,14 +134,75 @@ class App extends React.Component {
}
// Set tenant only if it changed to prevent DidUpdate loop
if (typeof tenant.name === 'undefined' || tenant.name !== tenantName) {
- this.props.dispatch(setTenantAction(tenantName, whiteLabel))
+ const tenantAction = setTenantAction(tenantName, whiteLabel)
+ this.props.dispatch(tenantAction)
+ if (tenantName) {
+ this.props.dispatch(fetchConfigErrorsAction(tenantAction.tenant))
+ }
}
}
}
+ renderConfigErrors = (configErrors) => {
+ const { history } = this.props
+ const errors = []
+ configErrors.forEach((item, idx) => {
+ let error = item.error
+ let cookie = error.indexOf('The error was:')
+ if (cookie !== -1) {
+ error = error.slice(cookie + 18).split('\n')[0]
+ }
+ let ctxPath = item.source_context.path
+ if (item.source_context.branch !== 'master') {
+ ctxPath += ' (' + item.source_context.branch + ')'
+ }
+ errors.push(
+ {
+ history.push(this.props.tenant.linkPrefix + '/config-errors')
+ this.setState({showErrors: false})
+ }}
+ >
+
+
+
+ {error}
+
+
+
+
+ )
+ })
+ return (
+
+
+
+
+ Config Errors
+
+
+
+
+
+ {errors.map(item => (item))}
+
+
+
+
+
+ )
+ }
+
render() {
- const { menuCollapsed } = this.state
- const { tenant } = this.props
+ const { menuCollapsed, showErrors } = this.state
+ const { tenant, configErrors } = this.props
+
if (typeof tenant.name === 'undefined') {
return (
@@ -181,6 +261,7 @@ class App extends React.Component {
// This connect the info state from the store to the info property of the App.
export default withRouter(connect(
state => ({
+ configErrors: state.configErrors,
info: state.info,
tenant: state.tenant
})
diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx
index a5dc77df6a..494e1596ee 100644
--- a/web/src/App.test.jsx
+++ b/web/src/App.test.jsx
@@ -28,6 +28,8 @@ import * as api from './api'
api.fetchInfo = jest.fn()
api.fetchTenants = jest.fn()
api.fetchStatus = jest.fn()
+api.fetchConfigErrors = jest.fn()
+api.fetchConfigErrors.mockImplementation(() => Promise.resolve({data: []}))
it('renders without crashing', () => {
diff --git a/web/src/api.js b/web/src/api.js
index d59ee37ee6..28c1b39ab9 100644
--- a/web/src/api.js
+++ b/web/src/api.js
@@ -108,6 +108,9 @@ function fetchInfo () {
function fetchTenants () {
return Axios.get(apiUrl + 'tenants')
}
+function fetchConfigErrors (apiPrefix) {
+ return Axios.get(apiUrl + apiPrefix + 'config-errors')
+}
function fetchStatus (apiPrefix) {
return Axios.get(apiUrl + apiPrefix + 'status')
}
@@ -131,6 +134,7 @@ function fetchJobs (apiPrefix) {
export {
getHomepageUrl,
getStreamUrl,
+ fetchConfigErrors,
fetchStatus,
fetchBuild,
fetchBuilds,
diff --git a/web/src/index.css b/web/src/index.css
index d2d82dd379..a83b7f60bc 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -9,6 +9,11 @@ a.refresh {
text-decoration: none;
}
+/* Notification bell color */
+.fa-bell {
+ color: orange;
+}
+
/* Status page */
.zuul-change {
margin-bottom: 10px;
diff --git a/web/src/pages/ConfigErrors.jsx b/web/src/pages/ConfigErrors.jsx
new file mode 100644
index 0000000000..04470c0716
--- /dev/null
+++ b/web/src/pages/ConfigErrors.jsx
@@ -0,0 +1,70 @@
+// 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 {
+ Icon
+} from 'patternfly-react'
+
+import { fetchConfigErrorsAction } from '../reducers'
+
+class ConfigErrorsPage extends React.Component {
+ static propTypes = {
+ configErrors: PropTypes.object,
+ tenant: PropTypes.object,
+ dispatch: PropTypes.func
+ }
+
+ updateData = () => {
+ this.props.dispatch(fetchConfigErrorsAction(this.props.tenant))
+ }
+
+ render () {
+ const { configErrors } = this.props
+ return (
+
+
+
+
+ {configErrors.map((item, idx) => {
+ let ctxPath = item.source_context.path
+ if (item.source_context.branch !== 'master') {
+ ctxPath += ' (' + item.source_context.branch + ')'
+ }
+ return (
+ -
+
{item.source_context.project} - {ctxPath}
+
+ {item.error}
+
+
+ )
+ })}
+
+
+
+ )
+ }
+}
+
+export default connect(state => ({
+ tenant: state.tenant,
+ configErrors: state.configErrors
+}))(ConfigErrorsPage)
diff --git a/web/src/reducers.js b/web/src/reducers.js
index 0ce01c3b2d..b02c9f6378 100644
--- a/web/src/reducers.js
+++ b/web/src/reducers.js
@@ -23,7 +23,7 @@
import { applyMiddleware, createStore, combineReducers } from 'redux'
import thunk from 'redux-thunk'
-import { fetchInfo } from './api'
+import { fetchConfigErrors, fetchInfo } from './api'
const infoReducer = (state = {}, action) => {
switch (action.type) {
@@ -34,6 +34,15 @@ const infoReducer = (state = {}, action) => {
}
}
+const configErrorsReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'FETCH_CONFIGERRORS_SUCCESS':
+ return action.errors
+ default:
+ return state
+ }
+}
+
const tenantReducer = (state = {}, action) => {
switch (action.type) {
case 'SET_TENANT':
@@ -46,7 +55,8 @@ const tenantReducer = (state = {}, action) => {
function createZuulStore() {
return createStore(combineReducers({
info: infoReducer,
- tenant: tenantReducer
+ tenant: tenantReducer,
+ configErrors: configErrorsReducer,
}), applyMiddleware(thunk))
}
@@ -62,6 +72,18 @@ function fetchInfoAction () {
})
}
}
+function fetchConfigErrorsAction (tenant) {
+ return (dispatch) => {
+ return fetchConfigErrors(tenant.apiPrefix)
+ .then(response => {
+ dispatch({type: 'FETCH_CONFIGERRORS_SUCCESS',
+ errors: response.data})
+ })
+ .catch(error => {
+ throw (error)
+ })
+ }
+}
function setTenantAction (name, whiteLabel) {
let apiPrefix = ''
@@ -90,5 +112,6 @@ function setTenantAction (name, whiteLabel) {
export {
createZuulStore,
setTenantAction,
+ fetchConfigErrorsAction,
fetchInfoAction
}
diff --git a/web/src/routes.js b/web/src/routes.js
index fef79debeb..34da870efa 100644
--- a/web/src/routes.js
+++ b/web/src/routes.js
@@ -17,6 +17,7 @@ import JobPage from './pages/Job'
import JobsPage from './pages/Jobs'
import BuildPage from './pages/Build'
import BuildsPage from './pages/Builds'
+import ConfigErrorsPage from './pages/ConfigErrors'
import TenantsPage from './pages/Tenants'
import StreamPage from './pages/Stream'
@@ -52,6 +53,10 @@ const routes = () => [
to: '/build/:buildId',
component: BuildPage
},
+ {
+ to: '/config-errors',
+ component: ConfigErrorsPage,
+ },
{
to: '/tenants',
component: TenantsPage,