From 35734e4d3ce0525a3d04d672720aee0946813467 Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Tue, 28 Aug 2018 12:41:19 +0000 Subject: [PATCH] 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 --- ...b-page-config-errors-f2f00d6d1eed9103.yaml | 5 + web/src/App.jsx | 93 +++++++++++++++++-- web/src/App.test.jsx | 2 + web/src/api.js | 4 + web/src/index.css | 5 + web/src/pages/ConfigErrors.jsx | 70 ++++++++++++++ web/src/reducers.js | 27 +++++- web/src/routes.js | 5 + 8 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/web-page-config-errors-f2f00d6d1eed9103.yaml create mode 100644 web/src/pages/ConfigErrors.jsx 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 (

Loading...

) } @@ -149,6 +218,16 @@ class App extends React.Component {
{tenant.name && this.renderMenu()} + {showErrors && this.renderConfigErrors(configErrors)}
{!menuCollapsed && (
@@ -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 4c030195ba..a595dfda19 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 ( + +
+ {this.updateData()}}> + refresh   + +
+
+
    + {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,