Last Time
Last time, we looked at creating a reactive Kafka publisher that would push out a Rating from a REST call (which will eventually come from the completion of a job). The last post also used some akka streams, and how we can use a back off supervisor. In a nutshell, this post will build on the Rating stream/Kafka KTable storage we have set up to date where we will actually create the React/Play framework endpoint to wire up the “View Rating” page with the Rating data that has been pushed through the stream processing so far.
Preamble
Just as a reminder, this is part of my ongoing set of posts which I talk about here, where we will be building up to a point where we have a full app using lots of different stuff, such as these:
- WebPack
- React.js
- React Router
- TypeScript
- Babel.js
- Akka
- Scala
- Play (Scala Http Stack)
- MySql
- SBT
- Kafka
- Kafka Streams
Ok, so now that we have the introductions out of the way, let's crack on with what we want to cover in this post.
Where is the Code?
As usual, the code is on GitHub here.
What Is This Post All About?
As stated above, this post largely boils down to the following things:
- Create an endpoint (Façade over existing Kafka stream Akka Http endpoint) to expose the previously submitted Rating data
- Make the “View Rating” page work with the retrieved data
Play Back End Changes
This section shows the changes to the play backend API code.
Rating Endpoint Façade
As we stated 2 posts ago, we created an Akka Http REST endpoint to serve up the combined Rating(s) that have been pushed through the Kafka stream processing rating topic. However, we have this Play framework API which we use for all other REST endpoints. So I have chosen to create a façade endpoint in the Play backend that will simply call out to the existing Akka Http endpoint. Keeping all the traffic in one place is a nice thing if you ask me. So let's look at this play code to do this.
New Route
We obviously need a new route, which is as follows:
GET /rating/byemail controllers.RatingController.ratingByEmail()
Controller Action
To serve this new route, we need a new Action
in the RatingController
. This is shown below:
package controllers
import javax.inject.Inject
import Actors.Rating.RatingProducerActor
import Entities.RatingJsonFormatters._
import Entities._
import akka.actor.{ActorSystem, OneForOneStrategy, Props, SupervisorStrategy}
import akka.pattern.{Backoff, BackoffSupervisor}
import akka.stream.{ActorMaterializer, ActorMaterializerSettings, Supervision}
import play.api.libs.json._
import play.api.libs.json.Json
import play.api.libs.json.Format
import play.api.libs.json.JsSuccess
import play.api.libs.json.Writes
import play.api.libs.ws._
import play.api.mvc.{Action, Controller}
import utils.{Errors, Settings}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Random
import scala.concurrent.duration._
class RatingController @Inject()
(
implicit actorSystem: ActorSystem,
ec: ExecutionContext,
ws: WSClient
) extends Controller
{
def ratingByEmail = Action.async { request =>
val email = request.getQueryString("email")
email match {
case Some(emailAddress) => {
val url = s"http://${Settings.ratingRestApiHostName}:
${Settings.ratingRestApiPort}/ratingByEmail?email=${emailAddress}"
ws.url(url).get().map {
response => (response.json).validate[List[Rating]]
}.map(x => Ok(Json.toJson(x.get)))
}
case None => {
Future.successful(BadRequest(
"ratingByEmail endpoint MUST be supplied with a non empty 'email' query string value"))
}
}
}
}
The main thing to note here is:
- We use the play ws (web services) library to issues a
GET
request against the existing Akka Http endpoint thus creating our façade. - We are still using Future to make it nice an async
React Front End Changes
This section shows the changes to the React frontend code.
React “View Rating” Page
This is the final result for the “View Rating” react page. I think it's all fairly self explanatory. I guess the only bit that really of any note is that we use lodash _.sumBy(..)
to do the summing up of the Ratings for this user to create an overall rating. The rest is standard jQuery/react stuff.
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as _ from "lodash";
import { OkDialog } from "./components/OkDialog";
import 'bootstrap/dist/css/bootstrap.css';
import
{
Well,
Grid,
Row,
Col,
Label,
ButtonInput
} from "react-bootstrap";
import { AuthService } from "./services/AuthService";
import { hashHistory } from 'react-router';
class Rating {
fromEmail: string
toEmail: string
score: number
constructor(fromEmail, toEmail, score) {
this.fromEmail = fromEmail;
this.toEmail = toEmail;
this.score = score;
}
}
export interface ViewRatingState {
ratings: Array<Rating>;
overallRating: number;
okDialogOpen: boolean;
okDialogKey: number;
okDialogHeaderText: string;
okDialogBodyText: string;
wasSuccessful: boolean;
}
export class ViewRating extends React.Component<undefined, ViewRatingState> {
private _authService: AuthService;
constructor(props: any) {
super(props);
this._authService = props.route.authService;
if (!this._authService.isAuthenticated()) {
hashHistory.push('/');
}
this.state = {
overallRating: 0,
ratings: Array(),
okDialogHeaderText: '',
okDialogBodyText: '',
okDialogOpen: false,
okDialogKey: 0,
wasSuccessful: false
};
}
loadRatingsFromServer = () => {
var self = this;
var currentUserEmail = this._authService.userEmail();
$.ajax({
type: 'GET',
url: 'rating/byemail?email=' + currentUserEmail,
contentType: "application/json; charset=utf-8",
dataType: 'json'
})
.done(function (jdata, textStatus, jqXHR) {
console.log("result of GET rating/byemail");
console.log(jqXHR.responseText);
let ratingsObtained = JSON.parse(jqXHR.responseText);
self.setState(
{
overallRating: _.sumBy(ratingsObtained, 'score'),
ratings: ratingsObtained
});
})
.fail(function (jqXHR, textStatus, errorThrown) {
self.setState(
{
okDialogHeaderText: 'Error',
okDialogBodyText: 'Could not load Ratings',
okDialogOpen: true,
okDialogKey: Math.random()
});
});
}
componentDidMount() {
this.loadRatingsFromServer();
}
render() {
var rowComponents = this.generateRows();
return (
<Well className="outer-well">
<Grid>
<Row className="show-grid">
<Col xs={6} md={6}>
<div>
<h4>YOUR OVERALL RATING <Label>{this.state.overallRating}</Label></h4>
</div>
</Col>
</Row>
<Row className="show-grid">
<Col xs={10} md={6}>
<h6>The finer details of your ratings are shown below</h6>
</Col>
</Row>
<Row className="show-grid">
<Col xs={10} md={6}>
<div className="table-responsive">
<table className=
"table table-striped table-bordered table-condensed factTable">
<thead>
<tr>
<th>Rated By</th>
<th>Rating Given</th>
</tr>
</thead>
<tbody>
{rowComponents}
</tbody>
</table>
</div>
</Col>
</Row>
<Row className="show-grid">
<span>
<OkDialog
open= {this.state.okDialogOpen}
okCallBack= {this._okDialogCallBack}
headerText={this.state.okDialogHeaderText}
bodyText={this.state.okDialogBodyText}
key={this.state.okDialogKey}/>
</span>
</Row>
</Grid>
</Well>
)
}
_okDialogCallBack = () => {
this.setState(
{
okDialogOpen: false
});
}
generateRows = () => {
return this.state.ratings.map(function (item) {
return <tr key={item.fromEmail}>
<td>{item.fromEmail}</td>
<td>{item.score}</td>
</tr>;
});
}
}
And with that, the “View Rating” page is actually done. This was a short post for a change, which is nice.
Conclusion
The previous set of posts have made this one very easy to do. It's just standard React/REST/Play stuff. So this one has been fairly easy to do.
Next Time
The more interesting stuff is still to come where we push out new jobs onto a new Kafka stream topic.