A few months ago I bought an Apple Cinema display to use with my Macbook Pro. My plan was to use both screens, my Cinema display as my primary screen for work and important apps and my MBP as a secondary screen for less important background apps like Spotify. It quickly became clear that I almost never used the secondary screen on my MBP as I had more than enough space on my Cinema display. At about the same time I decided that I’d quite like to dip my toes in the wonderful world of React and learn what all the hype was about.
So I came up with the idea of building a small status board web app that I could leave in my secondary display that would give me a quick overview of everything I needed for my work. As I wanted to learn React, it made sense to build a single page web app powered by React components and I decided that it would be simple enough to power the app using Lumen as a small backend API.
At Delicious Brains there are three apps we use that pretty much power everything we do: Slack for team communication, Help Scout for giving our customers support and GitHub for building our software. I didn’t want the dashboard to be crammed full of graphs and useless information, I just wanted to be able to see helpful information at a glance. So I decided the three elements of the board would be:
- A Slack “graph” showing who was online and what timezone they were in
- A list of our Help Scout mailboxes and their statuses
- A list of recent GitHub notifications
Before we get into the details I’ll just mention that this whole project is available on GitHub so feel free to browse through, borrow, and steal code as you wish.
Building the backend API with Lumen
The backend API has two important jobs to do. It has to authenticate our web app with the external services (using OAuth) and it has to query the services for the relevant data and cache the responses. One of the great things about working with open source projects is that, chances are someone has already done most of the leg work for you, and as this was a quick side project I relied heavily on third party libraries in the API.
Slack and GitHub both have OAuth authentication endpoints. That means you need to create an “app” on both of their services (Slack and GitHub) and then use OAuth to authenticate with your new apps. I decided to use The League of Extraordinary Packages OAuth 2 Client to handle all of the OAuth autentication code (see AuthController.php). Help Scout doesn’t have OAuth endpoints so I just had to use an API key.
It’s worth noting at this point that the place to put all of the OAuth app details and API keys is in the Lumen .env
file and not in the actual code repository. This makes the code more secure, reusable and far easier to maintain.
Next I had to create some API endpoints that React could use to load the data into the components. These endpoints would query their respective services and cache the response as required. You can see how I did this in the ApiController.php. I decided to abstract out the process of actually querying the respective APIs by creating some Util classes that handle the API queries and also return a single OauthTokenError if there was an authentication issue when querying the service API. Again these Util classes made use of third party libraries: Frlnc/php-slack for Slack and KnpLabs/php-github-api for GitHub. For Help Scout I just used cURL to query their API.
That’s it for the backend API. If you’re wondering at this point how to add a service that I’ve not included, the process would look something like this:
- Add a new route in routes.php
- Add the corresponding method to ApiController.php
- If the service uses OAuth for authentication you’ll need to add a method for it in AuthController.php (remebering to add the corresponding
/auth
route inroutes.php
) - Create a Util class to fetch the required data that can be used in your
ApiController.php
method.
Building the frontend with React
On to the “fun stuff”. If you haven’t heard of React yet it is a framework built by Facebook for building modern user interfaces. The idea is that while libraries like Backbone, Ember and Angular provide a framework for all aspects of a frontend Model-View-Controller application, React only provides functionality for the “View” part of a MVC application and it doesn’t care what you use to power the rest of your application. React has gained huge popularity and momentum recently as the main player in the “building UI’s in JS” space and I decided to use it to build the UI for our app.
There are another couple of libraries I needed to make use of to make our frontend work. I decided to use good old jQuery to query our API using AJAX, Moment.js to parse timestamps and output relative time strings, Normalize.css to… well… normalize our CSS and Font Awesome to provide our icons.
As this was going to be a single page application, first I needed to create the single page that would be output by Lumen. This meant creating a single view whose sole purpose is to load all of the JS and CSS dependancies. As this was going to be a small web app that I ran on my local machine, I wasn’t worried about optimisation, but in production a lot of the JS and CSS files would typically be concatenated and minified using a build tool.
The structure of the JS is as follows:
- bootstrap.js – Creates the global
App
variable that we will use to hold our application objects, functions and React components. It also holds any “utility” functions we might need. - react/error.js – A global error component that handles our
OauthTokenError
that we spoke about above. - react/[slack|helpscout|github].js – The individual service components.
- react/app.js – This is where the
ReactDOM.render()
method goes that ties our app together.
The composition of the React service components are as follows (using github.js as an example):
loadNotificationsFromServer: function() {
this.setState({loading: true});
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
if (typeof data.error !== 'undefined') {
this.setState({error: data.error, loading: false});
} else {
this.setState({data: data, loading: false});
setTimeout(this.loadNotificationsFromServer, this.props.pollInterval);
}
}.bind(this),
error: function(xhr, status, err) {
this.setState({loading: false});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
The load{Data}FromServer
function uses jQuery’s AJAX functionality to ping our backend API for the data it requires. We store any data and errors in the component’s state so we can use it later. Note that we also use setTimeout
here to poll our backend API to refresh the data at a regular interval. The pollInterval
is a component property that we define in app.js.
getInitialState: function() {
return {
loading: false,
error: null,
data: []
};
},
componentDidMount: function() {
this.loadNotificationsFromServer();
},
We make use of React’s getInitialState
function to setup the initial state of the component and the componentDidMount
function to kick off our initial call to the API.
render: function() {
var loading;
if (this.state.loading) {
loading = <div className="box-loading"><i className="fa fa-spinner fa-spin"></i></div>
}
if (this.state.error) {
return (
<div className="github-status box box-has-error">
{loading}
<App.Views.Error type={this.state.error.type} data={this.state.error.data} />
</div>
);
}
var notifications = this.state.data.map(function(notification) {
var typeIcon = <i className="fa fa-exclamation-circle"></i>
if(notification.subject.type == 'PullRequest') {
typeIcon = <i className="fa fa-code-fork"></i>
}
return (
<tr key={notification.id}>
<td className={'type type-' + notification.subject.type.toLowerCase()}>{typeIcon}</td>
<td className="name"><a href={notification.repository.html_url} target="_blank">{notification.repository.full_name}</a></td>
<td className="title">{notification.subject.title}</td>
<td className="time">{moment(notification.updated_at).fromNow()}</td>
</tr>
);
});
return (
<div className="github-status box">
{loading}
<table className="github-notifications">
{notifications}
</table>
</div>
);
}
Finally we use the render
function to actually create the output for our component. If there is an error we simply return the App.Views.Error
component which is a good example of how to make use of modular components. You’ll have noticed by now that components can contain multiple components that are either defined elsewhere, or are created at runtime, such as the {notifications}
list above. As you can see I make use of the map
function to create a list of table rows, in this case. The table row could easily be split out into it’s own component if necessary. Finally the render function returns the structure of the JSX that we will use for our component.
I’m not going to go into the intricacies of React here, suffice to say you should check out the documentation and learn it for yourself. I’ll just point out that one “gotcha” to look out for is that JSX uses className
and not class
.
Putting it all together
Now that we have both the backend and frontend in place, it’s time to set it up and run it. I personally use the status board on my secondary screen but there is nothing stopping you displaying a similar dashboard on a dedicated monitor on the wall of your office, but that’s entirely up to you.
As MAMP is already running on my machine it made sense to use it to run my status board. I simply set up a new virtual host running PHP >= 5.5.9 called statusboard.dev
. If you are setting up the repo yourself you should follow the instructions in the readme. I recommend using Fluid to make it feel more like an “app” and add it to your dock. Point fluid to your web address (statusboard.dev
) and add an icon if you want.
If you have any comments or suggestions I’d love to hear them in the comments. I’d especially love to hear if you make use of the status board anywhere and what customizations you make to it.