Last Time
Last time we looked at creating an 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 :
https://sachabarbs.wordpress.com/2017/05/01/madcap-idea/, 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, lets crack on with what we want to cover in this post.
Where is the code?
As usual the code is on GitHub here : https://github.com/sachabarber/MadCapIdea
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 chose 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 lets 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 results for the “View Rating” react page. I think its 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. Its 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.