Last Time
Last time we looked at finishing the “Create Job” page, which creates the initial Job and sends it out via Kafka through the play backend and back to other users browsers via comet/websock and finally some RX.js. This post will see us finish the entire system by implementing the final page “ViewJob
”.
PreAmble
Just as a reminder, this is part of my ongoing set of posts which I talk about in this post, 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 deals with the “View Job” functions.
This is kind of a hard post to write, as what I will have to do really is just present a wall of text, as it's all very isolated to one single typescript file with a couple of helper classes. However, what might help is to talk about some of the actions that this wall of text that follows will do.
Is This The End?
Whilst this does indeed wrap up the blog side of things, I intend to do a single http://www.codeproject.com article which will be a highly compressed version of these 13 posts. As such, it may be easier to get the gist of what I have done here than reading these 13 posts.
So What Should the “View Job” Page Do?
This page should do the following things:
- If a passenger sends out a job, it should be seen by ANY driver that is logged in (providing the job is not already assigned to a driver)
- Positions updates from passenger to drivers (that know about the passenger) should show the new passenger position
- When a driver pushes out (single laptop requires that users click on map to make their own position known to others) their new position that the client sees that and updates the driver marker accordingly
- That a passenger can accept a driver for a job
- That a driver cannot accept a job from a passenger
- That once a job is paired between passenger/driver, only those 2 markers will be shown if you are either of these users
- That once a job is paired between passenger/driver AND YOU ARE NOT ONE OF THESE USERS that you ONLY see your own markers
- That a job may be completed by passenger OR driver independently and that they are able to “Rate” each other
I think those are the main points. So how about that wall of text.
Wall of Text that Does the Stuffz
Here is the Wall of Text
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as _ from "lodash";
import Measure from 'react-measure'
import { RatingDialog } from "./components/RatingDialog";
import { YesNoDialog } from "./components/YesNoDialog";
import { OkDialog } from "./components/OkDialog";
import { AcceptList } from "./components/AcceptList";
import 'bootstrap/dist/css/bootstrap.css';
import {
Well,
Grid,
Row,
Col,
ButtonInput,
ButtonGroup,
Button,
Modal,
Popover,
Tooltip,
OverlayTrigger
} from "react-bootstrap";
import { AuthService } from "./services/AuthService";
import { JobService } from "./services/JobService";
import { JobStreamService } from "./services/JobStreamService";
import { PositionService } from "./services/PositionService";
import { Position } from "./domain/Position";
import { PositionMarker } from "./domain/PositionMarker";
import { hashHistory } from 'react-router';
import { withGoogleMap, GoogleMap, Marker, OverlayView } from "react-google-maps";
const STYLES = {
overlayView: {
background: `white`,
border: `1px solid #ccc`,
padding: 15,
}
}
const GetPixelPositionOffset = (width, height) => {
return { x: -(width / 2), y: -(height / 2) };
}
const ViewJobGoogleMap = withGoogleMap(props => (
<GoogleMap
ref={props.onMapLoad}
defaultZoom={16}
defaultCenter={{ lat: 50.8202949, lng: -0.1406958 }}
onClick={props.onMapClick}>
{props.markers.map((marker, index) => (
<OverlayView
key={marker.key}
mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
position={marker.position}
getPixelPositionOffset={GetPixelPositionOffset}>
<div style={STYLES.overlayView}>
<img src={marker.icon} />
<strong>{marker.key}</strong>
</div>
</OverlayView>
))}
</GoogleMap>
));
export interface ViewJobState {
markers: Array<PositionMarker>;
okDialogOpen: boolean;
okDialogKey: number;
okDialogHeaderText: string;
okDialogBodyText: string;
dimensions: {
width: number,
height: number
},
currentPosition: Position;
isJobAccepted: boolean;
finalActionHasBeenClicked: boolean;
}
type DoneCallback = (jdata: any, textStatus: any, jqXHR: any) => void
export class ViewJob extends React.Component<undefined, ViewJobState> {
private _authService: AuthService;
private _jobService: JobService;
private _jobStreamService: JobStreamService;
private _positionService: PositionService;
private _subscription: any;
private _currentJobUUID: any;
constructor(props: any) {
super(props);
this._authService = props.route.authService;
this._jobStreamService = props.route.jobStreamService;
this._jobService = props.route.jobService;
this._positionService = props.route.positionService;
if (!this._authService.isAuthenticated()) {
hashHistory.push('/');
}
let savedMarkers: Array<PositionMarker> = new Array<PositionMarker>();
if (this._positionService.hasJobPositions()) {
savedMarkers = this._positionService.userJobPositions();
}
this.state = {
markers: savedMarkers,
okDialogHeaderText: '',
okDialogBodyText: '',
okDialogOpen: false,
okDialogKey: 0,
dimensions: { width: -1, height: -1 },
currentPosition: this._authService.isDriver() ? null :
this._positionService.currentPosition(),
isJobAccepted: false,
finalActionHasBeenClicked: false
};
}
componentWillMount() {
var self = this;
this._subscription =
this._jobStreamService.getJobStream()
.retry()
.where(function (x, idx, obs) {
return self.shouldShowMarkerForJob(x.detail);
})
.subscribe(
jobArgs => {
console.log('RX saw onJobChanged');
console.log('RX x = ', jobArgs.detail);
this._jobService.clearUserIssuedJob();
this._jobService.storeUserIssuedJob(jobArgs.detail);
this.addMarkerForJob(jobArgs.detail);
},
error => {
console.log('RX saw ERROR');
console.log('RX error = ', error);
},
() => {
console.log('RX saw COMPLETE');
}
);
}
componentWillUnmount() {
this._subscription.dispose();
this._positionService.storeUserJobPositions(this.state.markers);
}
render() {
const adjustedwidth = this.state.dimensions.width;
return (
<Well className="outer-well">
<Grid>
<Row className="show-grid">
<Col xs={10} md={6}>
<h4>CURRENT JOB</h4>
</Col>
</Row>
<Row className="show-grid">
<Col xs={10} md={6}>
<AcceptList
markers={_.filter(this.state.markers, { isDriverIcon: true })}
currentUserIsDriver={this._authService.isDriver()}
clickCallback={this.handleMarkerClick}
/>
</Col>
</Row>
<Row className="show-grid">
<Col xs={10} md={6}>
<Measure
bounds
onResize={(contentRect) => {
this.setState({ dimensions: contentRect.bounds })
}}>
{({ measureRef }) =>
<div ref={measureRef}>
<ViewJobGoogleMap
containerElement={
<div style={{
position: 'relative',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: { adjustedwidth },
height: 600,
justifyContent: 'flex-end',
alignItems: 'center',
marginTop: 20,
marginLeft: 0,
marginRight: 0,
marginBottom: 20
}} />
}
mapElement={
<div style={{
position: 'relative',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: { adjustedwidth },
height: 600,
marginTop: 20,
marginLeft: 0,
marginRight: 0,
marginBottom: 20
}} />
}
markers={this.state.markers}
onMapClick={this.handleMapClick}
/>
</div>
}
</Measure>
</Col>
</Row>
{this.state.isJobAccepted === true ?
<Row className="show-grid">
<span>
<RatingDialog
theId="viewJobCompleteBtn"
headerText="Rate your driver/passenger"
okCallBack={this.ratingsDialogOkCallBack}
actionPerformed={this.state.finalActionHasBeenClicked} />
{!(this._authService.isDriver() === true) ?
<YesNoDialog
theId="viewJobCancelBtn"
launchButtonText="Cancel"
actionPerformed={this.state.finalActionHasBeenClicked}
yesCallBack={this.jobCancelledCallBack}
noCallBack={this.jobNotCancelledCallBack}
headerText="Cancel the job" />
:
null
}
<OkDialog
open={this.state.okDialogOpen}
okCallBack={this.okDialogCallBack}
headerText={this.state.okDialogHeaderText}
bodyText={this.state.okDialogBodyText}
key={this.state.okDialogKey} />
</span>
</Row> :
null
}
</Grid>
</Well>
);
}
handleMapClick = (event) => {
let currentUser = this._authService.user();
let isDriver = this._authService.isDriver();
let matchedMarker = _.find(this.state.markers, { 'email': currentUser.email });
let newPosition = new Position(event.latLng.lat(), event.latLng.lng());
let currentJob = this._jobService.currentJob();
this._positionService.clearUserPosition();
this._positionService.storeUserPosition(newPosition);
if (matchedMarker != undefined) {
let newMarkersList = this.state.markers;
_.remove(newMarkersList, function (n) {
return n.email === matchedMarker.email;
});
matchedMarker.position = newPosition;
newMarkersList.push(matchedMarker);
const newState = Object.assign({}, this.state, {
currentPosition: newPosition,
markers: newMarkersList
})
this.setState(newState);
currentJob = matchedMarker.jobForMarker;
}
else {
if (isDriver) {
let newDriverMarker =
this.createDriverMarker(currentUser, event);
let newMarkersList = this.state.markers;
newMarkersList.push(newDriverMarker);
const newState = Object.assign({}, this.state, {
currentPosition: newPosition,
markers: newMarkersList
})
this.setState(newState);
}
}
this._positionService.clearUserJobPositions();
this._positionService.storeUserJobPositions(this.state.markers);
this.pushOutJob(newPosition, currentJob);
}
handleMarkerClick = (targetMarker) => {
console.log('button on AcceptList clicked:' + targetMarker.key);
console.log(targetMarker);
let currentJob = this._jobService.currentJob();
let jobForMarker = targetMarker.jobForMarker;
let clientMarker = _.find(this.state.markers, { 'isDriverIcon': false });
if (clientMarker != undefined && clientMarker != null) {
let clientJob = clientMarker.jobForMarker;
clientJob.driverFullName = jobForMarker.driverFullName;
clientJob.driverEmail = jobForMarker.driverEmail;
clientJob.driverPosition = jobForMarker.driverPosition;
clientJob.vehicleDescription = jobForMarker.vehicleDescription;
clientJob.vehicleRegistrationNumber = jobForMarker.vehicleRegistrationNumber;
clientJob.isAssigned = true;
let self = this;
console.log("handleMarkerClick job");
console.log(clientJob);
this.makePOSTRequest('job/submit', clientJob, this,
function (jdata, textStatus, jqXHR) {
console.log("After is accepted");
const newState = Object.assign({}, self.state, {
isJobAccepted: true
})
self.setState(newState);
});
}
}
addMarkerForJob = (jobArgs: any): void => {
console.log("addMarkerForJob");
console.log(this.state);
if (this.state.isJobAccepted || jobArgs.isAssigned) {
this.processAcceptedMarkers(jobArgs);
}
else {
this.processNotAcceptedMarkers(jobArgs);
}
}
processAcceptedMarkers = (jobArgs: any): void => {
if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
this._currentJobUUID = jobArgs.jobUUID;
let isDriver = this._authService.isDriver();
let jobClientEmail = jobArgs.clientEmail;
let jobDriverEmail = jobArgs.driverEmail;
let newMarkersList = this.state.markers;
let newPositionForUser = null;
let newPositionForDriver = null;
console.log("JOB ACCEPTED WE NEED TO ONLY SHOW THE RELEVANT MARKERS + CURRENT USER");
let allowedNamed = [this._authService.userEmail()];
if (this._authService.userEmail() == jobArgs.clientEmail ||
this._authService.userEmail() == jobArgs.driverEmail) {
allowedNamed = [jobArgs.clientEmail, jobArgs.driverEmail];
}
let finalList: Array<PositionMarker> = new Array<PositionMarker>();
for (var i = 0; i < this.state.markers.length; i++) {
if (allowedNamed.indexOf(this.state.markers[i].email) >= 0) {
let theMarker = this.state.markers[i];
theMarker.jobForMarker.isAssigned = true;
finalList.push(theMarker);
}
}
newMarkersList = finalList;
if (this._authService.userEmail() == jobArgs.clientEmail ||
this._authService.userEmail() == jobArgs.driverEmail) {
let clientMarker = _.find(newMarkersList, { 'email': jobArgs.clientEmail });
if (clientMarker != undefined && clientMarker != null) {
newPositionForUser = jobArgs.clientPosition;
clientMarker.position = jobArgs.clientPosition;
}
let driverMarker = _.find(newMarkersList, { 'email': jobArgs.driverEmail });
if (driverMarker != undefined && driverMarker != null) {
newPositionForUser = jobArgs.driverPosition;
driverMarker.position = jobArgs.driverPosition;
}
}
else {
let matchedMarker = _.find(newMarkersList, { 'email': this._authService.userEmail() });
newPositionForUser = matchedMarker.position;
}
this.addClientDetailsToDrivers(newMarkersList);
var newState = this.updateStateForAcceptedMarker(newMarkersList, newPositionForUser);
this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
}
processNotAcceptedMarkers = (jobArgs: any): void => {
if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
this._currentJobUUID = jobArgs.jobUUID;
let isDriver = this._authService.isDriver();
let jobClientEmail = jobArgs.clientEmail;
let jobDriverEmail = jobArgs.driverEmail;
let newMarkersList = this.state.markers;
let newPositionForUser = null;
let newPositionForDriver = null;
console.log("JOB NOT ACCEPTED WE NEED TO ONLY ALL");
if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
newPositionForUser = new Position
(jobArgs.clientPosition.latitude, jobArgs.clientPosition.longitude);
}
if (jobClientEmail != undefined && jobClientEmail != null &&
newPositionForUser != undefined && newPositionForUser != null) {
let matchedMarker = _.find(this.state.markers, { 'email': jobClientEmail });
if (matchedMarker == null) {
newMarkersList.push(new PositionMarker(
jobArgs.clientFullName,
newPositionForUser,
jobArgs.clientFullName,
jobArgs.clientEmail,
false,
isDriver,
jobArgs)
);
}
else {
if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
this.updateMatchedUserMarker(
jobClientEmail,
newMarkersList,
newPositionForUser,
jobArgs);
}
}
}
if (jobArgs.driverPosition != undefined && jobArgs.driverPosition != null) {
newPositionForDriver =
new Position(jobArgs.driverPosition.latitude, jobArgs.driverPosition.longitude);
}
if (jobDriverEmail != undefined && jobDriverEmail != null &&
newPositionForDriver != undefined && newPositionForDriver != null) {
let matchedMarker = _.find(this.state.markers, { 'email': jobDriverEmail });
if (matchedMarker == null) {
newMarkersList.push(new PositionMarker(
jobArgs.driverFullName,
newPositionForDriver,
jobArgs.driverFullName,
jobArgs.driverEmail,
true,
isDriver,
jobArgs));
}
else {
this.updateMatchedUserMarker(
jobDriverEmail,
newMarkersList,
newPositionForDriver,
jobArgs);
}
}
if (isDriver) {
newPositionForUser = newPositionForDriver;
}
this.addClientDetailsToDrivers(newMarkersList);
var newState = this.updateStateForNewMarker(newMarkersList, newPositionForUser);
this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
}
addClientDetailsToDrivers = (newMarkersList: PositionMarker[]): void => {
let clientMarker = _.find(newMarkersList, { 'isDriverIcon': false });
if (clientMarker != undefined && clientMarker != null) {
let driverMarkers = _.filter(newMarkersList, { 'isDriverIcon': true });
for (var i = 0; i < driverMarkers.length; i++) {
let driversJob = driverMarkers[i].jobForMarker;
driversJob.jobUUID = clientMarker.jobForMarker.jobUUID;
driversJob.clientFullName = clientMarker.jobForMarker.clientFullName;
driversJob.clientEmail = clientMarker.jobForMarker.clientEmail;
driversJob.clientPosition = clientMarker.jobForMarker.clientPosition;
}
}
}
updateStateForMarkers = (newState: any, newMarkersList: PositionMarker[],
newPositionForUser: Position, jobArgs:any): void => {
this._positionService.clearUserJobPositions();
this._positionService.storeUserJobPositions(newMarkersList);
if (newPositionForUser != undefined && newPositionForUser != null) {
this._positionService.clearUserPosition();
this._positionService.storeUserPosition(newPositionForUser);
}
this._jobService.clearUserIssuedJob();
this._jobService.storeUserIssuedJob(jobArgs);
this.setState(newState);
}
updateMatchedUserMarker = (jobEmailToCheck: string, newMarkersList: PositionMarker[],
jobPosition: Position, jobForMarker:any): void => {
if (jobEmailToCheck != undefined && jobEmailToCheck != null) {
let matchedMarker = _.find(this.state.markers, { 'email': jobEmailToCheck });
if (matchedMarker != null) {
matchedMarker.position = jobPosition;
matchedMarker.jobForMarker = jobForMarker;
}
}
}
updateStateForNewMarker = (newMarkersList:PositionMarker[], position: Position): any => {
if (position != null) {
return Object.assign({}, this.state, {
currentPosition: position,
markers: newMarkersList
})
}
else {
return Object.assign({}, this.state, {
markers: newMarkersList
})
}
}
updateStateForAcceptedMarker = (newMarkersList: PositionMarker[], position: Position): any => {
if (position != null) {
return Object.assign({}, this.state, {
currentPosition: position,
markers: newMarkersList,
isJobAccepted: true
})
}
else {
return Object.assign({}, this.state, {
markers: newMarkersList,
isJobAccepted: true
})
}
}
shouldShowMarkerForJob = (jobArgs: any): boolean => {
let isDriver = this._authService.isDriver();
let currentJob = this._jobService.currentJob();
let hasJob = currentJob != undefined && currentJob != null;
if (!hasJob && isDriver)
return true;
if (hasJob && !currentJob.isAssigned)
return true;
if (hasJob && currentJob.isAssigned) {
if (currentJob.clientEmail == jobArgs.clientEmail) {
return true;
}
if (currentJob.driverEmail == jobArgs.driverEmail) {
return true;
}
}
return false;
}
pushOutJob = (newPosition: Position, jobForMarker : any): void => {
var self = this;
let currentUser = this._authService.user();
let isDriver = this._authService.isDriver();
let hasIssuedJob = this._jobService.hasIssuedJob();
let currentJob = jobForMarker;
let currentPosition = this._positionService.currentPosition();
var localClientFullName = '';
var localClientEmail = '';
var localClientPosition = null;
var localDriverFullName = '';
var localDriverEmail = '';
var localDriverPosition = null;
var localIsAssigned = false;
if (hasIssuedJob) {
if (currentJob.isAssigned != undefined && currentJob.isAssigned != null) {
localIsAssigned = currentJob.isAssigned;
}
else {
localIsAssigned = false;
}
}
if (hasIssuedJob) {
if (currentJob.clientFullName != undefined && currentJob.clientFullName != "") {
localClientFullName = currentJob.clientFullName;
}
else {
localClientFullName = !isDriver ? currentUser.fullName : '';
}
}
if (hasIssuedJob) {
if (currentJob.clientEmail != undefined && currentJob.clientEmail != "") {
localClientEmail = currentJob.clientEmail;
}
else {
localClientEmail = !isDriver ? currentUser.email : '';
}
}
if (hasIssuedJob) {
if (!isDriver) {
localClientPosition = newPosition
}
else {
if (currentJob.clientPosition != undefined && currentJob.clientPosition != null) {
localClientPosition = currentJob.clientPosition;
}
}
}
if (hasIssuedJob) {
if (currentJob.driverFullName != undefined && currentJob.driverFullName != "") {
localDriverFullName = currentJob.driverFullName;
}
else {
localDriverFullName = isDriver ? currentUser.fullName : '';
}
if (currentJob.driverEmail != undefined && currentJob.driverEmail != "") {
localDriverEmail = currentJob.driverEmail;
}
else {
localDriverEmail = isDriver ? currentUser.email : '';
}
if (isDriver) {
localDriverPosition = newPosition
}
else {
if(currentJob.driverPosition != undefined && currentJob.driverPosition != null) {
localDriverPosition = currentJob.driverPosition;
}
}
}
else {
localDriverFullName = currentUser.fullName;
localDriverEmail = currentUser.email;
localDriverPosition = isDriver ? currentPosition : null;
}
var newJob = {
jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
this._currentJobUUID : '',
clientFullName: localClientFullName,
clientEmail: localClientEmail,
clientPosition: localClientPosition,
driverFullName: localDriverFullName,
driverEmail: localDriverEmail,
driverPosition: localDriverPosition,
vehicleDescription: isDriver ?
this._authService.user().vehicleDescription : '',
vehicleRegistrationNumber: isDriver ?
this._authService.user().vehicleRegistrationNumber : '',
isAssigned: localIsAssigned,
isCompleted: false
}
console.log("handlpushOutJob job");
console.log(newJob);
this.makePOSTRequest('job/submit', newJob, self,
function (jdata, textStatus, jqXHR) {
self._jobService.clearUserIssuedJob();
self._jobService.storeUserIssuedJob(newJob);
});
}
createDriverMarker = (
driver: any,
event: any): PositionMarker => {
let localDriverFullName = driver.fullName;
let localDriverEmail = driver.email;
let localDriverPosition = new Position(event.latLng.lat(), event.latLng.lng());
let localVehicleDescription = this._authService.user().vehicleDescription;
let localVehicleRegistrationNumber = this._authService.user().vehicleRegistrationNumber;
let currentUserIsDriver = this._authService.isDriver();
var driverJob = {
jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
this._currentJobUUID : '',
driverFullName: localDriverFullName,
driverEmail: localDriverEmail,
driverPosition: localDriverPosition,
vehicleDescription: localVehicleDescription,
vehicleRegistrationNumber: localVehicleRegistrationNumber,
isAssigned: false,
isCompleted: false
}
return new PositionMarker(
localDriverFullName,
localDriverPosition,
localDriverFullName,
localDriverEmail,
true,
currentUserIsDriver,
driverJob
);
}
ratingsDialogOkCallBack = (theRatingScore: number) => {
console.log('RATINGS OK CLICKED');
var self = this;
let currentUser = this._authService.user();
let isDriver = this._authService.isDriver();
let currentJob = this._jobService.currentJob();
var ratingJSON = null;
if (!isDriver) {
ratingJSON = {
fromEmail: this._authService.userEmail(),
toEmail: currentJob.driverEmail,
score: theRatingScore
}
}
else {
ratingJSON = {
fromEmail: this._authService.userEmail(),
toEmail: currentJob.clientEmail,
score: theRatingScore
}
}
this.makePOSTRequest('rating/submit/new', ratingJSON, self,
function (jdata, textStatus, jqXHR) {
this._jobService.clearUserIssuedJob();
this._positionService.clearUserJobPositions();
this.setState(
{
okDialogHeaderText: 'Ratings',
okDialogBodyText: 'Rating successfully recorded',
okDialogOpen: true,
okDialogKey: Math.random(),
markers: new Array<PositionMarker>(),
currentPosition: null,
isJobAccepted: false,
finalActionHasBeenClicked: true
});
});
}
makePOSTRequest = (route: string, jsonData: any, context: ViewJob,
doneCallback: DoneCallback) => {
$.ajax({
type: 'POST',
url: route,
data: JSON.stringify(jsonData),
contentType: "application/json; charset=utf-8",
dataType: 'json'
})
.done(function (jdata, textStatus, jqXHR) {
doneCallback(jdata, textStatus, jqXHR);
})
.fail(function (jqXHR, textStatus, errorThrown) {
const newState = Object.assign({}, context.state, {
okDialogHeaderText: 'Error',
okDialogBodyText: jqXHR.responseText,
okDialogOpen: true,
okDialogKey: Math.random()
})
context.setState(newState)
});
}
jobCancelledCallBack = () => {
console.log('CANCEL YES CLICKED');
this._jobService.clearUserIssuedJob();
this._positionService.clearUserJobPositions();
this.setState(
{
okDialogHeaderText: 'Job Cancellaton',
okDialogBodyText: 'Job successfully cancelled',
okDialogOpen: true,
okDialogKey: Math.random(),
markers: new Array<PositionMarker>(),
currentPosition: null,
isJobAccepted: false,
finalActionHasBeenClicked: true
});
}
jobNotCancelledCallBack = () => {
console.log('CANCEL NO CLICKED');
this.setState(
{
okDialogHeaderText: 'Job Cancellaton',
okDialogBodyText: 'Job remains open',
okDialogOpen: true,
okDialogKey: Math.random(),
finalActionHasBeenClicked: true
});
}
okDialogCallBack = () => {
console.log('OK on OkDialog CLICKED');
this.setState(
{
okDialogOpen: false
});
}
}
Some Highlights
- We use RX.Js to listen to new events straight over the Comet based forever frame, that the server side Play scala code pushes a message out on
- There was a funny thing with driver acceptance which I originally wanted to be a button on a drivers marker within the map. However, this caused an issue with the
Map
where it would get a Map
event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter
/mouseLeave
but it was just not that great, so I opted to choose to put the acceptance of driver outside of the map, thus avoiding the issue altogether)
If you are curious how to run and see what you need to install, please see the README.MD in the codebase that has full instructions.
What’s It Looks Like When It's Run?
Some scenarios of what it looks like running are shown below.
In order to run it to this point, I normally follow this set of steps afterwards:
- Open a tab, login as a passenger that I had created
- Go to the “create job” page, click the map, push the “create job” button
- Open a NEW tab, login as a new driver, go to the “view job” page
- On the 1st tab (passenger), click the map to push passenger position to driver
- On the 2nd tab (driver), click the map to push driver position to passenger
- Repeat last 4 steps for additional driver
- On client tab, pick driver to accept, click accept button
- Complete the job from client tab, give driver rating
- Complete the job from paired driver tab, give passenger rating
- Go to “view rating” page, should see ratings
One of the challenges with an app like this, is that it is a streaming app. So this means that when a client pushes out a new job, there may be no-one listening for that job. Drivers may not even be logged in at all, or may login later, so they effectively subscribe late. So for this app dealing with that was kind of out of scope. So to remedy this, you need to ensure that position updates (clicking on the map for the given user browser session (i.e., tab)) gets pushed to other user browser sessions where the marker is not currently shown.
In a real world app, we might choose to do one of the following to fix this permanently:
- Store some, and when a new user joins, grab all unassigned passengers/driver within some geographical area
- Store last N-Many passenger/driver positions and push these on new login (there is no guarantee that these are in the same geographical area as us though, could be completely unrelated/of no practical concern for the current user)
Anyway, as I say this is out of scope for this demo project, but I hope that it does give you some insight as to why you need to push position updates manually.
This gives you an example of what it all looks like when it's running (not accepted yet):
This is what it looks like for the following setup:
1 x passenger (sacha barber)
2 x driver (driver 1 / driver 2)
Passenger (sacha) sees this:
Driver 1 sees this:
Driver 2 sees this:
So now, let's see what happens when we accept one of the drivers. I have chosen “driver 1” for this example.
This gives you an example of what it all looks like when its running (after job accepted between passenger/driver1).
Here is what things look like after job has been accepted.
Passenger (sacha) sees this:
See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown:
Driver 1 sees this:
See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown:
Driver 2 sees this:
Since this driver (driver 2) was not chosen, this driver's session now only shows itself:
Conclusion / Errors Made Along The Way
As with any reasonable size project (and this definitely is in that category for me), mistakes will be made. And whilst I am happy that I got all of this to work, I have certainly made a few mistakes, such as:
- There should have been 2 streams of jobs, client and driver.
- There should have been no separation between creating a job and viewing a job.
- There was a funny thing with driver acceptance which I originally wanted to be a button on a driver's marker within the map. However, this caused an issue with the
Map
where it would get a Map
event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter
/mouseLeave
but it was just not that great, so I opted to choose to put the acceptance of driver outside of the map, thus avoiding the issue altogether). - There is currently a bug when a job becomes paired between a passenger/driver, and new position updates are not reflected. This is probably a couple of line changes, but at this stage I’m like MEH. I am kind of done, I proved what I want to try, and the main points I set out to prove all work without this, so
Math.Power(MEH, Infinity)
. - When a driver joins (possibly later after other drivers), they should instantly know about other drivers/client. Right now, the users whose position you are missing would need to push a position update to get that user reflected on the users screen who is missing that user. I could have done something where every X milliseconds ALL current user positions are pushed to all other users, but this would clearly not scale. Another alternative would have been to store the state of all uses positions in a database every time that the map was clicked, and then when a new user joins find all users/jobs in the general area providing they are not already assigned to a job. Being completely honest, this was not really what this project was about either. The main cut and thrust of this article was to mess around with Kafka Streams and see how they work, and how I could stream stuff on top of that all the way back to a browser session. To this end, the project has been a massive success.
On a personal note, I have had a great time writing this project and have learnt loads doing it. For example, I am now way more familiar with how Akka Streams work, and I now know Play and Webpack to a fairly good level. I thoroughly recommend ALL of you pick your own mad cap projects and roll with them.
For me, next, I am either going to be dipping back into Azure stuff, or more scala where we will learn cats and Shapeless.