diff options
author | Tristan Cacqueray <tdecacqu@redhat.com> | 2018-08-28 12:41:19 +0000 |
---|---|---|
committer | Tristan Cacqueray <tdecacqu@redhat.com> | 2018-10-11 03:02:30 +0000 |
commit | 35734e4d3ce0525a3d04d672720aee0946813467 (patch) | |
tree | 3b1b6097018c9e4dbf99eb19b2d40e29bf5a2621 | |
parent | 6cb6b736150f0f65a29733e3055fa098953f901c (diff) | |
download | zuul-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.yaml | 5 | ||||
-rw-r--r-- | web/src/App.jsx | 93 | ||||
-rw-r--r-- | web/src/App.test.jsx | 2 | ||||
-rw-r--r-- | web/src/api.js | 4 | ||||
-rw-r--r-- | web/src/index.css | 5 | ||||
-rw-r--r-- | web/src/pages/ConfigErrors.jsx | 70 | ||||
-rw-r--r-- | web/src/reducers.js | 27 | ||||
-rw-r--r-- | web/src/routes.js | 5 |
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 + </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 |