Build the Multi-player Game Leader UI
Often a leader is associated with more than one active run of a game.
It will be helpful to show Div
leaders the state of all their active runs as soon as they log in.
We will do this by presenting a table of the leader’s active runs on the home page. If a run is in Play
phase, an Advance to Debrief
button will be available.
If a run is in Debrief
phase, the run’s name will be linked to the run’s debrief page.
Start by adding these actions to js/actions/Actions.js
:
// actions used to bracket processing a request (e.g. advancing the phase)
export const startProcessing = createAction('START_PROCESSING');
export const completeProcessing = createAction('COMPLETE_PROCESSING');
// advance run to next phase
export const advanceRunPhase = createAction('ADVANCE_RUN_PHASE', (run, ...args) => (
AutobahnReact.call(`model:model.run.${run.id}.advance_phase`)
));
The advanceRunPhase
action calls an RPC provided by the simpl-modelservice
Run
scope that advances the run to its next phase.
The startProcessing
and completeProcessing
actions are used to prevent the user from resubmitting this request before it has completed.
Create a reducer js/reducers/ProcessingReducer.js
to handle the startProcessing
and completeProcessing
actions:
import {createReducer} from 'redux-create-reducer';
import recycleState from 'redux-recycle';
import {recyleStateAction} from 'simpl-react/lib/actions/state';
import {
startProcessing,
completeProcessing
} from '../actions/Actions';
const initial = {inProcess: false};
const processing = recycleState(createReducer(initial, {
[startProcessing](state, action) {
return Object.assign({}, state, {
inProcess: true
});
},
[completeProcessing](state, action) {
// console.log("setting inProcess: false");
return Object.assign({}, state, {
inProcess: false
});
},
}), `${recyleStateAction}`);
export default processing
Then add processing
to reducers/combined/appReducers.js
:
import {simplReducers} from 'simpl-react/lib/reducers/combined';
import {reducer as form} from 'redux-form';
import processing from '../ProcessingReducer';
import status from '../StatusReducer';
const reducers = simplReducers({
form,
// Add your customer reducers here, if any.
processing,
status
});
export default reducers;
Create a presentational component js/components/AdvancePhase.js
to display the Advance to Debrief
button:
import React from 'react';
import {Button} from 'react-bootstrap';
import PropTypes from 'prop-types';
class AdvancePhase extends React.Component {
render() {
const phases = this.props.phases || [];
const currentPhase = phases.find(
(phase) => phase.id == this.props.run.phase
);
const nextPhase = phases.find(
(phase) => phase.order == currentPhase.order + 1
)
if (nextPhase === undefined) {
return (<span>{currentPhase.name}</span>);
} else {
if (this.props.advancingPhase) {
return (
<Button type='button'
className="btn btn-sm btn-labeled btn-primary disabled">
Advance to {nextPhase.name}
</Button>
);
} else {
return (
<Button type='button'
className="btn btn-sm btn-labeled btn-primary"
onClick={() => this.props.advanceRunPhase(this.props.run)}>
Advance to {nextPhase.name}
</Button>
);
}
}
}
}
AdvancePhase.propTypes = {
run: PropTypes.object.isRequired,
phases: PropTypes.array,
advanceRunPhase: PropTypes.func.isRequired,
advancingPhase: PropTypes.bool.isRequired,
};
export default AdvancePhase;
and wrap it in a container component js/containers/AdvancePhaseContainer.js
:
import {connect} from 'react-redux';
import AdvancePhase from '../components/AdvancePhase';
import {
advanceRunPhase,
startProcessing,
completeProcessing
} from '../actions/Actions';
function mapStateToProps(state) {
return {
phases: state.simpl.phase,
advancingPhase: state.processing.inProcess
};
}
function mapDispatchToProps(dispatch) {
return {
advanceRunPhase: function (run) {
dispatch(startProcessing()); // disable all advance phase buttons
dispatch(advanceRunPhase(run))
.then((result) => {
dispatch(completeProcessing()); // enable all advance phase buttons
});
}
};
}
const AdvancePhaseContainer = connect(
mapStateToProps,
mapDispatchToProps
)(AdvancePhase);
export default AdvancePhaseContainer;
Add a presentational component js/components/RunRow.js
for displaying a run in a table row:
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import AdvancePhaseContainer from '../containers/AdvancePhaseContainer';
class RunRow extends React.Component {
constructor(props) {
super(props);
this.getPhase = this.getPhase.bind(this);
}
getPhase() {
return this.props.phases.find(
(p) => p.id == this.props.run.phase
);
}
render() {
const phase = this.getPhase();
const linkDestination = `/run/${this.props.run.id}/debrief`;
let name = this.props.run.name;
if ((phase.name === 'Debrief')) {
name = (
<Link to={linkDestination} id={`Link_${this.props.run.name}`}>{this.props.run.name}</Link>
);
}
return (
<tr>
<td>{name}</td>
<td>
<AdvancePhaseContainer run={this.props.run}/>
</td>
</tr>
);
}
}
RunRow.propTypes = {
run: PropTypes.object.isRequired,
phases: PropTypes.array.isRequired,
};
export default RunRow;
and wrap it in a container component js/containers/RunRowContainer.js
:
import {connect} from 'react-redux';
import RunRow from '../components/RunRow';
function mapStateToProps(state, ownProps) {
const run = ownProps.run;
return {
phases: state.simpl.phase,
};
}
const RunRowContainer = connect(
mapStateToProps,
null
)(RunRow);
export default RunRowContainer;
Finally, replace the contents of js/modules/LeaderHome.js
with:
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import RunRowContainer from '../containers/RunRowContainer'
class LeaderHome extends React.Component {
render() {
const name = this.props.runuser.first_name + ' ' + this.props.runuser.last_name;
const runRows = this.props.runs.map(
(r) => <RunRowContainer key={r.id} run={r}/>
);
return (
<div>
<div>
<h1>Hello {name}</h1>
</div>
<div>
<table>
<thead>
<tr>
<th width="60%">Run</th>
<th width="30%"> </th>
</tr>
</thead>
<tbody>
{runRows}
</tbody>
</table>
</div>
<br/>
<a href="/logout/" className="btn btn-success btn-lg">Logout</a>
</div>
);
}
}
LeaderHome.propTypes = {
runuser: PropTypes.object.isRequired,
runs: PropTypes.array,
};
function mapStateToProps(state) {
return {
runuser: state.simpl.current_runuser,
runs: state.simpl.run
};
}
const module = connect(
mapStateToProps,
null
)(LeaderHome);
export default module;
Now when you login as [email protected]
with password div
, you will see information about your active run:
Let’s add some styling to make it easier to read the table of results.
In frontend/templates/frontend/home.html
, replace:
<head>
</head>
with
<head>
<style>
table, th, td { border: 1px solid black; }
</style>
</head>
Let’s test the Advance to Debrief
button.
First, open an incognito window and log in to ‘http://localhost:8000’ as ‘[email protected]’ with password ‘s3’ and submit a decision.
Next, click the Advance to Debrief
button. You should see something similar to:
Both pages have updated to reflect the run is now in Debrief
phase. On the leader’s page, the run name is now a link.
However, the application does not yet know how to route the the link’s URL.
We will now create a debrief page that displays a table of the run’s worlds’ most recent decisions and results.
You will start by adding a presentational component to display the world information js/component/WorldRow.js
:
import React from 'react';
import PropTypes from 'prop-types';
class WorldRow extends React.Component {
render() {
console.log("WorldRow: props=", this.props);
return (
<tr>
<td>{this.props.world.name}</td>
<td>{this.props.dividend}</td>
<td>{this.props.divisor}</td>
<td>{this.props.quotient}</td>
</tr>
);
}
}
WorldRow.propTypes = {
world: PropTypes.object.isRequired,
dividend: PropTypes.string.isRequired,
divisor: PropTypes.string.isRequired,
quotient: PropTypes.string.isRequired,
};
export default WorldRow;
and wrapping it in a container component js/containers/WorldRowContainer.js
:
import {connect} from 'react-redux';
import WorldRow from '../components/WorldRow';
function mapStateToProps(state, ownProps) {
const world = ownProps.world;
const scenario = state.simpl.scenario.find(
(s) => world.id === s.world
);
const period = state.simpl.period.find(
(p) => scenario.id === p.scenario
);
const decisions = state.simpl.decision.filter(
(d) => period.id === d.period
);
const dividendRole = state.simpl.role.find(
(r) => r.name === 'Dividend'
);
let dividend = '';
let divisor = '';
decisions.forEach((d) => {
if (d.role === dividendRole.id) {
dividend = d.data.operand.toString();
} else {
divisor = d.data.operand.toString();
}
});
const result = state.simpl.result.find(
(r) => period.id === r.period
);
let quotient = '';
if (result != undefined) {
quotient = result.data.quotient.toString();
}
console.log('WorldRowContainer: dividend=', dividend, ', divisor=', divisor, ', quotient=', quotient);
return {
world,
dividend,
divisor,
quotient
};
}
const WorldRowContainer = connect(
mapStateToProps,
null
)(WorldRow);
export default WorldRowContainer;
Next, you will add page that displays the run’s world information js/modules/LeaderRunDebrief.js
:
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {withRouter} from 'react-router';
import {Alert} from 'react-bootstrap';
import WorldRowContainer from '../containers/WorldRowContainer';
class LeaderRunDebrief extends React.Component {
render() {
const name = this.props.run.name;
const worldRows = this.props.worlds.map(
(w) => <WorldRowContainer key={w.id} world={w}/>
);
return (
<div>
<div>
<h1>Run: {name}</h1>
</div>
<div>
<table>
<thead>
<tr>
<th>World</th>
<th>Dividend</th>
<th>Divisor</th>
<th>Quotient</th>
</tr>
</thead>
<tbody>
{worldRows}
</tbody>
</table>
</div>
<br/>
<a href="/" className="btn btn-success btn-lg">Home</a>
<br/>
<a href="/logout/" className="btn btn-success btn-lg">Logout</a>
</div>
);
}
}
LeaderRunDebrief.propTypes = {
run: PropTypes.object.isRequired,
worlds: PropTypes.array.isRequired,
};
function mapStateToProps(state, ownProps) {
const run = state.simpl.run.find(
(r) => r.id == ownProps.params['id']
);
const unsortedWorlds = state.simpl.world.filter(
(w) => run.id === w.run
);
const worlds = _.sortBy(unsortedWorlds, (s) => s.id); // worlds are created in order
return {
run,
worlds,
};
}
const module = connect(
mapStateToProps,
null
)(LeaderRunDebrief);
export default withRouter(module);
Finally, you will add your new page to the routes in js/modules/Root.js
:
...
import LeaderRunDebrief from './LeaderRunDebrief';
import LeaderHome from './LeaderHome';
import PlayerHome from './PlayerHome';
const Home = (LEADER) ? LeaderHome : PlayerHome;
class Root extends React.Component {
componentWillMount() {
const userId = parseInt(AUTHID, 10);
}
render() {
return (
<Router history={browserHistory}>
{/* player or leader home page route */}
<Route path="/" component={Home}/>
<Route path="/run/:id/debrief" component={LeaderRunDebrief}/>
</Router>
);
}
}
...
After you refresh your browser, you should be taken to the LeaderRunDebrief
page when you click the linked run name:
Congratulations! You have completed this tutorial’s leader UI.