summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTristan Cacqueray <tdecacqu@redhat.com>2018-08-28 12:41:19 +0000
committerTristan Cacqueray <tdecacqu@redhat.com>2018-10-11 03:02:30 +0000
commit35734e4d3ce0525a3d04d672720aee0946813467 (patch)
tree3b1b6097018c9e4dbf99eb19b2d40e29bf5a2621
parent6cb6b736150f0f65a29733e3055fa098953f901c (diff)
downloadzuul-35734e4d3ce0525a3d04d672720aee0946813467.tar.gz
web: add config-errors notifications drawer
This change adds a Notification drawer to display the config errors and a dedicated config-errors web interface. Change-Id: I5cfc608219e26848a20f14e6c99bdb166ac67121
-rw-r--r--releasenotes/notes/web-page-config-errors-f2f00d6d1eed9103.yaml5
-rw-r--r--web/src/App.jsx93
-rw-r--r--web/src/App.test.jsx2
-rw-r--r--web/src/api.js4
-rw-r--r--web/src/index.css5
-rw-r--r--web/src/pages/ConfigErrors.jsx70
-rw-r--r--web/src/reducers.js27
-rw-r--r--web/src/routes.js5
8 files changed, 203 insertions, 8 deletions
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 000000000..825c401db
--- /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 7ad7d8e60..5c22083ac 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(
+ <Notification
+ key={idx}
+ seen={false}
+ onClick={() => {
+ history.push(this.props.tenant.linkPrefix + '/config-errors')
+ this.setState({showErrors: false})
+ }}
+ >
+ <Icon className='pull-left' type='pf' name='error-circle-o' />
+ <Notification.Content>
+ <Notification.Message>
+ {error}
+ </Notification.Message>
+ <Notification.Info
+ leftText={item.source_context.project}
+ rightText={ctxPath}
+ />
+ </Notification.Content>
+ </Notification>
+ )
+ })
+ return (
+ <NotificationDrawer style={{minWidth: '500px'}}>
+ <NotificationDrawer.Panel>
+ <NotificationDrawer.PanelHeading>
+ <NotificationDrawer.PanelTitle>
+ Config Errors
+ </NotificationDrawer.PanelTitle>
+ <NotificationDrawer.PanelCounter
+ text={errors.length + ' error(s)'} />
+ </NotificationDrawer.PanelHeading>
+ <NotificationDrawer.PanelCollapse id={1} collapseIn>
+ <NotificationDrawer.PanelBody key='containsNotifications'>
+ {errors.map(item => (item))}
+ </NotificationDrawer.PanelBody>
+
+ </NotificationDrawer.PanelCollapse>
+ </NotificationDrawer.Panel>
+ </NotificationDrawer>
+ )
+ }
+
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 (<h2>Loading...</h2>)
}
@@ -149,6 +218,16 @@ class App extends React.Component {
<div className='collapse navbar-collapse'>
{tenant.name && this.renderMenu()}
<ul className='nav navbar-nav navbar-utility'>
+ { configErrors.length > 0 &&
+ <NotificationDrawer.Toggle
+ className="zuul-config-errors"
+ hasUnreadMessages
+ style={{color: 'orange'}}
+ onClick={(e) => {
+ e.preventDefault()
+ this.setState({showErrors: !this.state.showErrors})}}
+ />
+ }
<li>
<a href='https://zuul-ci.org/docs'
rel='noopener noreferrer' target='_blank'>
@@ -163,6 +242,7 @@ class App extends React.Component {
</li>
)}
</ul>
+ {showErrors && this.renderConfigErrors(configErrors)}
</div>
{!menuCollapsed && (
<div className='collapse navbar-collapse navbar-collapse-1 in'>
@@ -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 a5dc77df6..494e1596e 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 d59ee37ee..28c1b39ab 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 4c030195b..a595dfda1 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 000000000..04470c071
--- /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 (
+ <React.Fragment>
+ <div className="pull-right">
+ <a className="refresh" onClick={() => {this.updateData()}}>
+ <Icon type="fa" name="refresh" /> refresh&nbsp;&nbsp;
+ </a>
+ </div>
+ <div className="pull-left">
+ <ul className="list-group">
+ {configErrors.map((item, idx) => {
+ let ctxPath = item.source_context.path
+ if (item.source_context.branch !== 'master') {
+ ctxPath += ' (' + item.source_context.branch + ')'
+ }
+ return (
+ <li className="list-group-item" key={idx}>
+ <h3>{item.source_context.project} - {ctxPath}</h3>
+ <p style={{whiteSpace: 'pre'}}>
+ {item.error}
+ </p>
+ </li>
+ )
+ })}
+ </ul>
+ </div>
+ </React.Fragment>
+ )
+ }
+}
+
+export default connect(state => ({
+ tenant: state.tenant,
+ configErrors: state.configErrors
+}))(ConfigErrorsPage)
diff --git a/web/src/reducers.js b/web/src/reducers.js
index 0ce01c3b2..b02c9f637 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 fef79debe..34da870ef 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'
@@ -53,6 +54,10 @@ const routes = () => [
component: BuildPage
},
{
+ to: '/config-errors',
+ component: ConfigErrorsPage,
+ },
+ {
to: '/tenants',
component: TenantsPage,
globalRoute: true