Better handle loading states

Now if BuildBot does not have build data, it will be indicated
with question mark.
Moved pagination to a separate component, got rid of pageReducer
This commit is contained in:
Victor Perevertkin 2020-03-16 05:36:21 +03:00
parent 9d00ab2f3f
commit 55258739d2
No known key found for this signature in database
GPG Key ID: C750B7222E9C7830
15 changed files with 232 additions and 225 deletions

View File

@ -6,18 +6,29 @@ import {
Route,
Redirect
} from "react-router-dom";
import { connect } from 'react-redux'
import Header from './components/Header';
import Commits from './components/Commits';
import Pulls from './components/Pulls';
import Loading from './components/Loading'
import configureStore from './redux/store';
import { loadBuilders } from './redux/actions'
import { LOAD_STATE } from './redux/constants'
const store = configureStore();
export default function App() {
return (
<Provider store={store}>
<Router>
<div>
<Header/>
class App extends React.PureComponent {
// things which are needed regardles the current view
componentDidMount() {
this.props.dispatch(loadBuilders())
}
render() {
return (
<React.Fragment>
<Header/>
{this.props.canRender ?
<Switch>
<Route path="/pulls/:pull_state"><Pulls/></Route>
<Route path="/commits/:branch"><Commits/></Route>
@ -26,7 +37,22 @@ export default function App() {
<Route path="/commits"><Redirect to="/commits/master"/></Route>
<Route path="/"><Redirect to="/commits/master"/></Route>
</Switch>
</div>
: <Loading text="Loading basic data..." />}
</React.Fragment>)
}
}
function mapStateToProps(state) {
return ({canRender: state.isLoading.buildersDataState === LOAD_STATE.LOADED})
}
const WrappedApp = connect(mapStateToProps)(App)
export default function outerApp() {
return (
<Provider store={store}>
<Router>
<WrappedApp />
</Router>
</Provider>
);

View File

@ -7,11 +7,7 @@ import {
DropdownMenu,
DropdownItem
} from 'reactstrap';
import {
loadBranches,
loadCommits,
loadBuilders
} from '../redux/actions';
import { loadBranches, loadCommits } from '../redux/actions';
class Branches extends React.Component {
constructor(props) {
@ -21,7 +17,6 @@ class Branches extends React.Component {
};
}
componentDidMount() {
this.props.loadBuilders();
this.props.loadBranches();
}
@ -76,8 +71,7 @@ const mapStateToProps = ({ branches }) => ({
const mapDispatchToProps = dispatch => ({
loadCommits: (branch, next) => dispatch(loadCommits(branch, next)),
loadBranches: () => dispatch(loadBranches()),
loadBuilders: () => dispatch(loadBuilders())
loadBranches: () => dispatch(loadBranches())
});
export default connect(

View File

@ -6,17 +6,15 @@ import Branches from './Branches';
import './styles/Commit.css';
import CommitsCard from './CommitsCard';
import Loading from './Loading';
import Pagination from './Pagination'
import { LOAD_STATE } from '../redux/constants'
class Commits extends React.PureComponent {
componentDidMount() {
this.props.loadCommits(this.props.branch);
this.props.loadBuilds();
}
componentDidUpdate(prevProps) {
if (this.props.branch !== prevProps.branch) {
this.props.loadCommits(this.props.branch)
this.props.loadBuilds()
if (this.props.firstLoad) {
this.props.loadCommits(this.props.branch, 1); // TODO: remove the hack
this.props.loadBuilds();
}
}
@ -35,7 +33,7 @@ class Commits extends React.PureComponent {
};
render() {
const {branch, page} = this.props;
const { branch, currentPage } = this.props
return (
<div className='container mt-2'>
@ -46,7 +44,7 @@ class Commits extends React.PureComponent {
</div>
</div>
{this.props.isLoading.load ? (
{this.props.isLoading ? (
<Loading
text={`Fetching latest Commits of ${branch} for you...`}
/>
@ -60,38 +58,10 @@ class Commits extends React.PureComponent {
Err:{this.props.error}
</div>
) : (
<div>
<button
type='button'
onClick={() => {
this.props.loadCommits(branch, page.prev);
}}
className='btn btn-primary '
disabled={
page.prev === null || this.props.error !== null
}
>
<i className='fa fa-caret-left' aria-hidden='true' />
Previous Page{' '}
</button>{' '}
<button
type='button'
onClick={() => {
this.props.loadCommits(branch, page.next);
}}
className='btn btn-primary'
disabled={
page.next === null || this.props.error !== null
}
>
Next Page{' '}
<i className='fa fa-caret-right' aria-hidden='true' />
</button>
<footer className='blockquote-footer'>
Page {page.next - 1}
</footer>
<div className='footer-blockquote' />
</div>
<Pagination
currentPage={currentPage}
nextPage={() => this.props.loadCommits(branch, currentPage + 1)}
prevPage={() => this.props.loadCommits(branch, currentPage - 1)} />
)}
</div>
)}
@ -110,15 +80,15 @@ const mapStateToProps = ({
commits,
builders,
error,
page,
builds,
tests
}) => ({
isLoading,
isLoading: isLoading.commitsLoadInfo.lastState !== LOAD_STATE.LOADED,
firstLoad: !isLoading.commitsLoadInfo.loadedPages.includes(1), //if we need to load first page
currentPage: isLoading.commitsLoadInfo.currentPage,
commits,
builders,
error,
page,
builds,
tests
});

View File

@ -1,8 +1,9 @@
import React from 'react';
import { connect } from 'react-redux'
import { UncontrolledCollapse, CardBody, Card, CardHeader } from 'reactstrap';
import BuildDetails from './BuildDetails';
import TestDetails from './TestDetails';
import { JOB_STATUS } from '../redux/constants'
import { JOB_STATUS, LOAD_STATE } from '../redux/constants'
import { statusElement } from './utils'
function firstLineTrimmed(str) {
@ -16,7 +17,7 @@ function firstLineTrimmed(str) {
}
function getTotalStatus(jobs) {
if (!jobs.length) return null
if (!jobs || !jobs.length) return JOB_STATUS.NO_DATA
let ret = JOB_STATUS.SUCCESS
@ -30,31 +31,31 @@ function getTotalStatus(jobs) {
return ret
}
function CommitsCard({sha, ...props}) {
function CommitsCard({sha, author, commit, loadStatus, ...props}) {
let tog = 'toggler' + sha;
let committerDate = new Date(props.commit.committer.date);
let authorDate = new Date(props.commit.author.date);
let author = encodeURIComponent(props.commit.author.name);
let committer = encodeURIComponent(props.commit.committer.name);
let committerDate = new Date(commit.committer.date);
let authorDate = new Date(commit.author.date);
const author_login = author ? author.login : commit.author.name
const builds = props.builds ? props.builds : []
const tests = props.tests ? props.tests : []
return (
<Card className="mb-1">
<CardHeader className='new' type='button' id={tog}>
<div className='row'>
<div className='col-sm-9'>
<a className="text-monospace" href={`https://github.com/reactos/reactos/commit/${sha}`}>{sha.substring(0, 7)}</a>
{" "}{firstLineTrimmed(props.commit.message)}
{" "}{firstLineTrimmed(commit.message)}
</div>
<div className='col-sm-2'>{props.author.login}</div>
<div className='col-sm-2'>{author_login}</div>
<div className="col-sm-1">
{props.builds &&
props.builds.length > 0
? statusElement(getTotalStatus(props.builds), "Build status")
: <span title="Loading results"><i className="fa fa-refresh fa-spin" /></span> }
{loadStatus.buildBot === LOAD_STATE.LOADING
? <span title="Loading results"><i className="fa fa-refresh fa-spin" /></span>
: statusElement(getTotalStatus(props.builds), "Build status") }
{" "}
{props.tests &&
props.tests.length > 0
? statusElement(getTotalStatus(props.tests), "Test status")
: <span title="Loading results"><i className="fa fa-refresh fa-spin" /></span> }
{loadStatus.buildBot === LOAD_STATE.LOADING
? <span title="Loading results"><i className="fa fa-refresh fa-spin" /></span>
: statusElement(getTotalStatus(props.tests), "Test status") }
</div>
</div>
</CardHeader>
@ -66,13 +67,13 @@ function CommitsCard({sha, ...props}) {
<a
target='_blank'
rel='noreferrer noopener'
href={props.commit.html_url}
href={commit.html_url}
>
{sha}
</a>
</p>
<p>
<strong>Commit Msg:</strong> {props.commit.message}
<strong>Commit Msg:</strong> {commit.message}
</p>
</div>
<div className='row'>
@ -81,11 +82,11 @@ function CommitsCard({sha, ...props}) {
<a
target='_blank'
rel='noreferrer noopener'
href={`https://git.reactos.org/?p=reactos.git;a=search;s=${author};st=author`}
href={`https://git.reactos.org/?p=reactos.git;a=search;s=${encodeURIComponent(commit.author.name)};st=author`}
>
{props.commit.author.name}
{commit.author.name}
</a>
{` <${props.commit.author.email}>`}
{` <${commit.author.email}>`}
</div>
<div className='col-sm'>
<strong>Author Date: </strong>
@ -98,11 +99,11 @@ function CommitsCard({sha, ...props}) {
<a
target='_blank'
rel='noreferrer noopener'
href={`https://git.reactos.org/?p=reactos.git;a=search;s=${committer};st=committer`}
href={`https://git.reactos.org/?p=reactos.git;a=search;s=${encodeURIComponent(commit.committer.name)};st=committer`}
>
{props.commit.committer.name}
{commit.committer.name}
</a>
{` <${props.commit.committer.email}>`}
{` <${commit.committer.email}>`}
</div>
<div className='col-sm'>
<strong>Committer Date: </strong>
@ -113,23 +114,15 @@ function CommitsCard({sha, ...props}) {
<div className="row">
<div className="col-md-5">
<h5>Build Details:</h5>
{props.builds ? (
<BuildDetails builds={props.builds} />
) : (
<div>
<strong>Loading Builds...</strong>
</div>
)}
{loadStatus.buildBot === LOAD_STATE.LOADING
? <strong>Loading builds data...</strong>
: <BuildDetails builds={builds} /> }
</div>
<div className="col-md-7">
<h5>Test Details:</h5>
{props.tests ? (
<TestDetails tests={props.tests} previousTests={props.previousTests} />
) : (
<div>
<strong>No data Exists</strong>
</div>
)}
{loadStatus.buildBot === LOAD_STATE.LOADING
? <strong>Loading tests data...</strong>
: <TestDetails tests={tests} previousTests={props.previousTests} /> }
</div>
</div>
</CardBody>
@ -137,4 +130,9 @@ function CommitsCard({sha, ...props}) {
</Card>
);
}
export default CommitsCard;
function mapStateToProps(state, ownProps) {
return {...ownProps, loadStatus: state.isLoading.byCommit[ownProps.sha]}
}
export default connect(mapStateToProps)(CommitsCard);

View File

@ -0,0 +1,30 @@
import React from 'react'
export default function Pagination({currentPage, nextPage, prevPage}) {
return (
<div>
<button
type='button'
onClick={prevPage}
className='btn btn-primary '
disabled={currentPage === 1}
>
<i className='fa fa-caret-left' aria-hidden='true' />
Previous Page{' '}
</button>{' '}
<button
type='button'
onClick={nextPage}
className='btn btn-primary'
>
Next Page{' '}
<i className='fa fa-caret-right' aria-hidden='true' />
</button>
<footer className='blockquote-footer'>
Page {currentPage}
</footer>
<div className='footer-blockquote' />
</div>
)
}

View File

@ -4,16 +4,14 @@ import { NavLink, useParams } from 'react-router-dom';
import { loadPulls } from '../redux/actions';
import './styles/Pulls.css';
import Loading from './Loading';
import Pagination from './Pagination'
import PullsCard from './PullsCard';
import { LOAD_STATE } from '../redux/constants'
class Pulls extends React.PureComponent {
componentDidMount() {
this.props.loadPulls(this.props.pullState);
}
componentDidUpdate(prevProps) {
if (this.props.pullState !== prevProps.pullState) {
this.props.loadPulls(this.props.pullState)
if (this.props.firstLoad) {
this.props.loadPulls(this.props.pullState, 1)
}
}
@ -30,7 +28,7 @@ class Pulls extends React.PureComponent {
);
};
render() {
const {pullState, page} = this.props;
const { pullState, currentPage } = this.props;
return (
<div className='container mt-2'>
@ -51,7 +49,7 @@ class Pulls extends React.PureComponent {
</div>
</div>
{this.props.isLoading.load ? (
{this.props.isLoading ? (
<Loading text='Fetching latest PRs for you...' />
) : (
<div>
@ -63,38 +61,10 @@ class Pulls extends React.PureComponent {
Err:{this.props.error}
</div>
) : (
<div>
<button
type='button'
onClick={() => {
this.props.loadPulls(pullState, page.prev);
}}
className='btn btn-primary '
disabled={
page.prev === null || this.props.error !== null
}
>
<i className='fa fa-caret-left' aria-hidden='true' />
Previous Page{' '}
</button>{' '}
<button
type='button'
onClick={() => {
this.props.loadPulls(pullState, page.next);
}}
className='btn btn-primary'
disabled={
page.next === null || this.props.error !== null
}
>
Next Page{' '}
<i className='fa fa-caret-right' aria-hidden='true' />
</button>
<footer className='blockquote-footer'>
Page {page.next - 1}
</footer>
<div className='footer-blockquote' />
</div>
<Pagination
currentPage={currentPage}
nextPage={() => this.props.loadPulls(pullState, currentPage + 1)}
prevPage={() => this.props.loadPulls(pullState, currentPage - 1)} />
)}
</div>
)}
@ -111,16 +81,16 @@ function PullsWrapper(props) {
const mapStateToProps = ({
pulls,
builders,
page,
isLoading,
error,
builds,
tests
}) => ({
isLoading: isLoading.pullsLoadInfo.lastState !== LOAD_STATE.LOADED,
firstLoad: !isLoading.pullsLoadInfo.loadedPages.includes(1), //if we need to load first page
currentPage: isLoading.pullsLoadInfo.currentPage,
pulls,
builders,
page,
isLoading,
error,
builds,
tests

View File

@ -10,6 +10,8 @@ export function statusElement(status, statusText) {
return <span className="text-warning" title={statusText}><i className="fa fa-hourglass" /></span>
case JOB_STATUS.FAILURE:
return <span className="text-danger" title={statusText}><i className="fa fa-times" /></span>
case JOB_STATUS.NO_DATA:
return <span title={statusText}><i className="fa fa-question" /></span>
default:
return null
}

View File

@ -71,11 +71,8 @@ export const setTestmanError = error => ({
error
});
export const setPages = (next, prev) => ({
type: 'PAGE_LOAD_SUCCESS',
next,
prev
});
// branches
export const loadBranches = () => ({
type: BRANCHES.LOAD
});

View File

@ -7,7 +7,15 @@ export const PULL_STATE = {
export const JOB_STATUS = {
SUCCESS: 0,
FAILURE: 1,
ONGOING: 2
ONGOING: 2,
NO_DATA: 3
}
export const LOAD_STATE = {
NOT_LOADED: 0,
LOADING: 1,
LOADED: 2,
ERROR: 3
}
export const BUILDER_TYPE = {

View File

@ -6,7 +6,6 @@ import errorReducer from './errorReducer';
import branchReducer from './branchReducer';
import builderReducer from './builderReducer';
import pullsReducer from './pullsReducer';
import pageReducer from './pageReducer';
import builds from './builds';
import tests from './tests'
@ -17,7 +16,6 @@ const rootReducer = combineReducers({
branches: branchReducer,
builders: builderReducer,
pulls: pullsReducer,
page: pageReducer,
builds,
tests
});

View File

@ -1,45 +1,89 @@
import { COMMITS, BRANCHES, PULLS, BUILD_DATA, BUILDERS } from '../constants';
import { COMMITS, BRANCHES, PULLS, BUILD_DATA, TEST_DATA, BUILDERS, LOAD_STATE } from '../constants';
const loadingReducer = (state = { newPage: 1, load: false }, action) => {
const defaultState = {
byCommit: {},
buildersDataState: LOAD_STATE.NOT_LOADED,
commitsLoadInfo: {lastState: LOAD_STATE.NOT_LOADED, currentPage: 1, loadedPages: []},
pullsLoadInfo: {lastState: LOAD_STATE.NOT_LOADED, currentPage: 1, loadedPages: []},
buildBotLoadInfo: {lastState: LOAD_STATE.NOT_LOADED},
testManLoadInfo: {lastState: LOAD_STATE.NOT_LOADED}
}
function loadingReducer(state = defaultState, action) {
switch (action.type) {
case COMMITS.LOAD:
return { newPage: action.newPage, load: true };
case BUILDERS.LOAD:
return { ...state, load: true };
case PULLS.LOAD:
return { newPage: action.newPage, load: true };
case BRANCHES.LOAD:
return { newPage: action.newPage, load: true };
case BUILD_DATA.LOAD:
return { newPage: action.newPage, load: true };
case COMMITS.LOAD_SUCCESS:
return { ...state, load: false };
return { ...state, buildersDataState: LOAD_STATE.LOADING }
case BUILDERS.LOAD_SUCCESS:
return { ...state, load: true };
return { ...state, buildersDataState: LOAD_STATE.LOADED }
case PULLS.LOAD_SUCCESS:
return { ...state, load: false };
case COMMITS.LOAD: {
const commitsLoadInfo = {
...state.commitsLoadInfo,
lastState: LOAD_STATE.LOADING,
lastPage: action.newPage
}
case BRANCHES.LOAD_SUCCESS:
return { newPage: action.newPage, load: true };
return { ...state, commitsLoadInfo}
}
case COMMITS.LOAD_SUCCESS: {
const byCommit = {...state.byCommit}
case BUILD_DATA.LOAD_SUCCESS:
return { load: false };
const commitsLoadInfo = {
...state.commitsLoadInfo,
lastState: LOAD_STATE.LOADED,
currentPage: state.commitsLoadInfo.lastPage,
loadedPages: state.commitsLoadInfo.loadedPages.concat([state.commitsLoadInfo.lastPage])
}
case COMMITS.LOAD_FAIL:
return { ...state, load: false };
for(let commit of Object.values(action.commits))
{
byCommit[commit.sha] = {buildBot: LOAD_STATE.LOADING, tests: LOAD_STATE.LOADING}
}
case PULLS.LOAD_FAIL:
return { ...state, load: false };
return { ...state, byCommit, commitsLoadInfo}
}
case BUILD_DATA.LOAD: {
const byCommit = {...state.byCommit}
for(let sha of Object.keys(byCommit))
{
byCommit[sha] = {buildBot: LOAD_STATE.LOADING, tests: byCommit[sha].tests}
}
return {...state, byCommit };
}
case PULLS.LOAD: {
const pullsLoadInfo = {
...state.pullsLoadInfo,
lastState: LOAD_STATE.LOADING,
lastPage: action.newPage
}
return { ...state, pullsLoadInfo}
}
case PULLS.LOAD_SUCCESS: {
const pullsLoadInfo = {
...state.pullsLoadInfo,
lastState: LOAD_STATE.LOADED,
currentPage: state.pullsLoadInfo.lastPage,
loadedPages: state.pullsLoadInfo.loadedPages.concat([state.pullsLoadInfo.lastPage])
}
return { ...state, pullsLoadInfo }
}
case BUILD_DATA.LOAD_SUCCESS: {
const byCommit = {...state.byCommit}
for(let [sha, build] of Object.entries(action.builds))
{
if (byCommit[sha]) {
byCommit[sha] = {buildBot: LOAD_STATE.LOADED, tests: byCommit[sha].tests}
}
}
return { ...state, byCommit };
}
default:
return { ...state, load: false };
return state
}
};

View File

@ -1,19 +0,0 @@
import { BRANCHES } from '../constants';
const pageReducer = (state = { next: 1, prev: 0 }, action) => {
switch (action.type) {
case BRANCHES.CURRENT:
return {
next: 1,
prev: 0
};
case 'PAGE_LOAD_SUCCESS':
return {
next: action.next,
prev: action.prev
};
default:
return state;
}
};
export default pageReducer;

View File

@ -1,21 +1,13 @@
import { throttle, call, put, select } from 'redux-saga/effects';
import { COMMITS } from '../constants';
import { fetchCommits } from '../api';
import { setCommits, setCommitsError, setPages } from '../actions';
import { setCommits, setCommitsError } from '../actions';
export const getNewPage = state => parseInt(state.isLoading.newPage, 10);
function* handleCommitsLoad(action) {
try {
const newPage = yield select(getNewPage);
let commits = yield call(fetchCommits, action.branch, newPage);
let commits = yield call(fetchCommits, action.branch, action.newPage)
yield put(setCommits(commits.commits.body));
yield put(
setPages(
commits.page.next !== undefined ? commits.page.next.page : null,
commits.page.prev !== undefined ? commits.page.prev.page : null
)
);
} catch (error) {
//dispatch error
yield put(setCommitsError(error.toString()));

View File

@ -1,23 +1,16 @@
import { takeEvery, call, put, select } from 'redux-saga/effects';
import { PULLS } from '../constants';
import { fetchPulls } from '../api';
import { setPulls, setPullsError, setPages } from '../actions';
const getNewPage = state => parseInt(state.isLoading.newPage, 10);
import { setPulls, setPullsError } from '../actions';
function* handlePullsLoad(action) {
try {
const newPage = yield select(getNewPage);
let pulls = yield call(fetchPulls, action.state, newPage);
yield put(setPulls(pulls.pulls.body));
yield put(
setPages(
pulls.page.next !== undefined ? pulls.page.next.page : null,
pulls.page.prev !== undefined ? pulls.page.prev.page : null
)
);
} catch (error) {
yield put(setPullsError(error.toString()));
}
try {
let pulls = yield call(fetchPulls, action.state, action.newPage);
yield put(setPulls(pulls.pulls.body));
} catch (error) {
yield put(setPullsError(error.toString()));
}
}
export default function* watchPullsLoad() {

View File

@ -9,7 +9,11 @@ function* handleTestmanLoad() {
const shas = pulls.map(pull => pull.merge_commit_sha);
const testResults = yield call(fetchTests, shas[9], shas[0], 1);
const testByPulls = {};
console.log(shas)
for (let sha of shas) {
// some pulls don't have merge_commit_sha
if (!sha) continue
testByPulls[sha] = testResults.filter(test =>
sha.startsWith(test.revision._text)
);