web: refactor status page to use a reducer

This change updates the status page component to use a fetchStatusAction
instead of direct axios call. This enables using the generic error reducers.
This change also refactors the refresh button into a parent component to
enable re-use on the other pages.

Change-Id: Iac8a317263f84f14f28d2ea015f918268b903407
This commit is contained in:
Tristan Cacqueray 2018-12-02 04:23:37 +00:00
parent f312f68ec6
commit 833dbd257b
5 changed files with 187 additions and 52 deletions

67
web/src/actions/status.js Normal file
View File

@ -0,0 +1,67 @@
/* global Promise */
// 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 API from '../api'
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'
export const requestStatus = () => ({
type: STATUS_FETCH_REQUEST
})
export const receiveStatus = json => ({
type: STATUS_FETCH_SUCCESS,
status: json,
receivedAt: Date.now()
})
const failedStatus = error => ({
type: STATUS_FETCH_FAIL,
error
})
// Create fake delay
//function sleeper(ms) {
// return function(x) {
// return new Promise(resolve => setTimeout(() => resolve(x), ms));
// };
//}
const fetchStatus = (tenant) => dispatch => {
dispatch(requestStatus())
return API.fetchStatus(tenant.apiPrefix)
.then(response => dispatch(receiveStatus(response.data)))
.catch(error => dispatch(failedStatus(error)))
}
const shouldFetchStatus = state => {
const status = state.status
if (!status) {
return true
}
if (status.isFetching) {
return false
}
return true
}
export const fetchStatusIfNeeded = (tenant) => (dispatch, getState) => {
if (shouldFetchStatus(getState())) {
return dispatch(fetchStatus(tenant))
}
return Promise.resolve()
}

View File

@ -0,0 +1,55 @@
// 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.
// Boiler plate code to manage refresh button
import * as React from 'react'
import PropTypes from 'prop-types'
import {
Icon,
Spinner
} from 'patternfly-react'
class Refreshable extends React.Component {
static propTypes = {
tenant: PropTypes.object,
remoteData: PropTypes.object,
}
componentDidMount () {
if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
}
renderSpinner () {
const { remoteData } = this.props
return (
<Spinner loading={ remoteData.isFetching }>
<a className="refresh" onClick={() => {this.updateData(true)}}>
<Icon type="fa" name="refresh" /> refresh&nbsp;&nbsp;
</a>
</Spinner>
)
}
}
export default Refreshable

View File

@ -17,31 +17,29 @@ import * as React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
Alert,
Checkbox,
Icon,
Form,
FormGroup,
FormControl,
Spinner
} from 'patternfly-react'
import { fetchStatus } from '../api'
import { fetchStatusIfNeeded } from '../actions/status'
import Pipeline from '../containers/status/Pipeline'
import Refreshable from '../containers/Refreshable'
class StatusPage extends React.Component {
class StatusPage extends Refreshable {
static propTypes = {
location: PropTypes.object,
tenant: PropTypes.object
tenant: PropTypes.object,
remoteData: PropTypes.object,
dispatch: PropTypes.func
}
state = {
status: null,
filter: null,
expanded: false,
error: null,
loading: false,
autoReload: true
}
@ -84,29 +82,11 @@ class StatusPage extends React.Component {
}
updateData = (force) => {
/* // Create fake delay
function sleeper(ms) {
return function(x) {
return new Promise(resolve => setTimeout(() => resolve(x), ms));
};
}
*/
if (force || (this.visible && this.state.autoReload)) {
this.setState({error: null, loading: true})
fetchStatus(this.props.tenant.apiPrefix)
// .then(sleeper(2000))
.then(response => {
this.setState({status: response.data, loading: false})
if (this.state.autoReload) {
this.timer = setTimeout(this.updateData, 5000)
}
}).catch(error => {
this.setState({error: error.message, status: null})
if (this.state.autoReload) {
this.timer = setTimeout(this.updateData, 5000)
}
})
this.props.dispatch(fetchStatusIfNeeded(this.props.tenant))
.then(() => {if (this.state.autoReload) {
this.timer = setTimeout(this.updateData, 5000)
}})
}
// Clear any running timer
if (this.timer) {
@ -118,18 +98,7 @@ class StatusPage extends React.Component {
componentDidMount () {
document.title = 'Zuul Status'
this.loadState()
if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps, prevState) {
// When autoReload is set, also call updateData to retrigger the setTimeout
if (this.props.tenant.name !== prevProps.tenant.name || (
this.state.autoReload &&
this.state.autoReload !== prevState.autoReload)) {
this.updateData()
}
super.componentDidMount()
}
componentWillUnmount () {
@ -221,10 +190,9 @@ class StatusPage extends React.Component {
}
render () {
const { autoReload, error, status, filter, expanded, loading } = this.state
if (error) {
return (<Alert>{this.state.error}</Alert>)
}
const { remoteData } = this.props
const { autoReload, filter, expanded } = this.state
const status = remoteData.status
if (this.filter && !this.filterLoaded && filter) {
this.filterLoaded = true
this.filter.value = filter
@ -261,11 +229,7 @@ class StatusPage extends React.Component {
return (
<React.Fragment>
<div className="pull-right" style={{display: 'flex'}}>
<Spinner loading={loading}>
<a className="refresh" onClick={() => {this.updateData(true)}}>
<Icon type="fa" name="refresh" /> refresh&nbsp;&nbsp;
</a>
</Spinner>
{this.renderSpinner()}
<Checkbox
defaultChecked={autoReload}
onChange={(e) => {this.setState({autoReload: e.target.checked})}}
@ -291,4 +255,7 @@ class StatusPage extends React.Component {
}
}
export default connect(state => ({tenant: state.tenant}))(StatusPage)
export default connect(state => ({
tenant: state.tenant,
remoteData: state.status,
}))(StatusPage)

View File

@ -17,12 +17,14 @@ import { combineReducers } from 'redux'
import configErrors from './configErrors'
import errors from './errors'
import info from './info'
import status from './status'
import tenant from './tenant'
const reducers = {
info,
configErrors,
errors,
status,
tenant,
}

View File

@ -0,0 +1,44 @@
// 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 {
STATUS_FETCH_FAIL,
STATUS_FETCH_REQUEST,
STATUS_FETCH_SUCCESS
} from '../actions/status'
export default (state = {
isFetching: false,
status: null
}, action) => {
switch (action.type) {
case STATUS_FETCH_REQUEST:
return {
isFetching: true,
status: state.status
}
case STATUS_FETCH_SUCCESS:
return {
isFetching: false,
status: action.status,
}
case STATUS_FETCH_FAIL:
return {
isFetching: false,
status: state.status,
}
default:
return state
}
}