community content: update realtime location tracking application

GITHUB_PR_NUMBER: 8195
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/8195

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3692
Co-authored-by: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
GitOrigin-RevId: dfc19df7d007cf140d9b20a9c60b67e262f552cf
This commit is contained in:
hasura-bot 2022-02-24 16:07:39 +05:30
parent faf9716c28
commit 0fc135d279
17 changed files with 375 additions and 361 deletions

View File

@ -1,4 +1,4 @@
FROM node:carbon as builder
FROM node:16 as builder
ENV NODE_ENV=PRODUCTION
WORKDIR /app
COPY package.json ./
@ -6,7 +6,7 @@ RUN npm install --production
COPY . .
RUN npm run build
FROM node:8-alpine
FROM node:16-alpine
RUN npm -g install serve
WORKDIR /app

View File

@ -12,9 +12,9 @@ The application makes use of Hasura GraphQL Engine's real-time capabilities
using `subscription`. There is no backend code involved. The application is
hosted on GitHub pages and the Postgres+GraphQL Engine is running on Postgres.
- Checkout the [live app](https://realtime-location-tracking.demo.hasura.app/).
- Checkout the [live app](https://realtime-location-tracking.demo.hasura.io).
- Explore the backend using [Hasura
Console](https://realtime-location-tracking.hasura.app/console).
Console](https://cloud.hasura.io/public/graphiql?endpoint=https%3A%2F%2Frealtime-location.hasura.app%2Fv1%2Fgraphql).
# Running the app yourself

View File

@ -3,7 +3,7 @@
configuration:
connection_info:
database_url:
from_env: SAMPLE_APPS_DATABASE_URL
from_env: PG_DATABASE_URL
pool_settings:
idle_timeout: 180
max_connections: 50

View File

@ -3,24 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"apollo-boost": "^0.1.14",
"apollo-cache-inmemory": "^1.2.8",
"apollo-client": "^2.4.0",
"apollo-link": "^1.2.2",
"apollo-link-http": "^1.5.4",
"apollo-link-ws": "^1.0.8",
"apollo-utilities": "^1.0.19",
"@apollo/client": "^3.5.8",
"bootstrap": "^4.1.3",
"google-map-react": "^1.0.6",
"google-maps-react": "^2.0.2",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"react": "^16.4.2",
"react-apollo": "^2.1.11",
"react-dom": "^16.4.2",
"react-scripts": "1.1.4",
"google-map-react": "^2.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"serve": "^10.0.0",
"subscriptions-transport-ws": "^0.9.14",
"subscriptions-transport-ws": "^0.9.19",
"uuid": "^3.3.2"
},
"scripts": {
@ -34,6 +24,19 @@
},
"devDependencies": {
"gh-pages": "^1.2.0",
"react-router-dom": "^4.3.1"
"react-router-dom": "^4.3.1",
"eslint-plugin-graphql": "^4.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -4,7 +4,6 @@
.App-header {
background-color: #222;
// height: 75px;
padding: 20px;
color: white;
margin-bottom: 25px;

View File

@ -1,94 +1,87 @@
import React, {Component} from 'react';
import React, { useEffect, useState } from 'react';
import { ApolloProvider, ApolloConsumer, gql } from '@apollo/client';
import { Subscription } from '@apollo/client/react/components';
import PropTypes from 'prop-types';
import GoogleApiWrapper from "./MapContainer";
import client from '../apollo'
import './App.css';
import GoogleApiWrapper from "./MapContainer";
import { ApolloConsumer, Subscription } from 'react-apollo';
import gql from 'graphql-tag';
import client from '../apollo'
import { ApolloProvider } from 'react-apollo';
class App extends Component {
constructor(props) {
super(props);
this.state = {
vehicleId: props.vehicleId,
}
}
componentWillReceiveProps(nextProps) {
if ( nextProps.vehicleId !== this.props.vehicleId ) {
this.setState({ vehicleId: nextProps.vehicleId });
}
}
render() {
const LOCATION_SUBSCRIPTION = gql`
subscription getLocation($vehicleId: String!) {
vehicle(where: {id: {_eq: $vehicleId}}) {
locations(order_by: {timestamp:desc}, limit: 1) {
location
timestamp
}
const LOCATION_SUBSCRIPTION = gql`
subscription getLocation($vehicleId: String!) {
vehicle(where: {id: {_eq: $vehicleId}}) {
locations(order_by: {timestamp:desc}, limit: 1) {
location
timestamp
}
}
`;
}
`;
const queryImg = require('../assets/carbon.png');
function App(props) {
const [ vehicleId, setVehicleId ] = useState(props.vehicleId)
return (
<ApolloConsumer>
{client => (
<Subscription subscription={LOCATION_SUBSCRIPTION} variables={{vehicleId: this.props.vehicleId}}>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error! </p>;
useEffect(() => {
setVehicleId(props.vehicleId)
}, [props.vehicleId]);
let latestLocation = null;
const vehicle = data.vehicle[0];
const latestLocationObject = vehicle.locations[0];
if (latestLocationObject) {
latestLocation = latestLocationObject.location;
}
const vehicleLocation = {
'width': '100%',
'marginBottom': '20px',
};
const queryImgStyle = {
'width': '100%',
};
return (
<div style={ vehicleLocation }>
<div className="row ">
<div className="col-md-6 col-xs-12 request_block">
<div className="subscription_wrapper">
<h4>Live query</h4>
<div className="subscription_query">
The GraphQL subscription required to fetch the realtime location data.
</div>
<div>
<img style={ queryImgStyle } src={ queryImg } alt="Subscription query"/>
</div>
const queryImg = require('../assets/carbon.png');
return (
<ApolloConsumer>
{ client => (
<Subscription subscription={ LOCATION_SUBSCRIPTION } variables={{ vehicleId }}>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error! </p>;
let latestLocation = null;
const vehicle = data.vehicle[0];
const latestLocationObject = vehicle.locations[0];
if (latestLocationObject) {
latestLocation = latestLocationObject.location;
}
const vehicleLocation = {
'width': '100%',
'marginBottom': '20px',
};
const queryImgStyle = {
'width': '100%',
};
return (
<div style={ vehicleLocation }>
<div className="row ">
<div className="col-md-6 col-xs-12 request_block">
<div className="subscription_wrapper">
<h4>Live query</h4>
<div className="subscription_query">
The GraphQL subscription required to fetch the realtime location data.
</div>
</div>
<div className="col-md-6 col-xs-12">
<h4>Live tracking</h4>
<div className="tracking_info">
Location is updated every 3 secs to simulate live tracking
</div>
<div className="map_wrapper">
<GoogleApiWrapper marker_location={latestLocation}/>
<div>
<img style={ queryImgStyle } src={ queryImg } alt="Subscription query"/>
</div>
</div>
</div>
<div className="col-md-6 col-xs-12">
<h4>Live tracking</h4>
<div className="tracking_info">
Location is updated every 3 secs to simulate live tracking
</div>
<div className="map_wrapper">
<GoogleApiWrapper marker_location={ latestLocation }/>
</div>
</div>
</div>
);
}}
</Subscription>
)}
</ApolloConsumer>
);
}
</div>
);
}}
</Subscription>
)}
</ApolloConsumer>
);
}
App.propTypes = {
@ -97,7 +90,7 @@ App.propTypes = {
const ApolloWrappedComponent = (props) => {
return (
<ApolloProvider client={client}>
<ApolloProvider client={ client }>
<App { ...props }/>
</ApolloProvider>
);

View File

@ -1,38 +1,38 @@
import React, { Component } from 'react';
import React, { useState, useEffect } from 'react';
import GoogleMapReact from 'google-map-react';
import { fitBounds } from 'google-map-react/utils';
import { fitBounds } from 'google-map-react';
import {
GOOGLE_API_KEY,
bounds,
HASURA_LOCATION
} from '../constants'
import drivingLoc from '../mapInfo/drivingJson';
import { usePrevious } from '../hooks/usePrevious';
class Marker extends Component {
render() {
function Marker() {
const greatPlaceStyle = {
position: 'absolute',
top: "100%",
left: "50%",
transform: 'translate(-50%, -50%)'
};
const divStyle = {
'background':'#ffff00',
'borderRadius':'50%',
'border': '3px solid black',
'padding': '2px',
'height':'30px',
'width':'30px',
'padding': '4px',
'height':'35px',
'width':'35px',
'position':'relative',
};
const imgStyle = {
'width': '100%',
};
const imgSrc = require('../assets/hasura.png');
return (
<div style={ greatPlaceStyle }>
<div style={ divStyle }>
@ -40,30 +40,29 @@ class Marker extends Component {
</div>
</div>
);
}
}
export class MapContainer extends Component {
constructor(props) {
super(props);
this.state = {};
this.state.mapLoaded = false;
this.state.latlng = {};
if ( this.props.marker_location ) {
this.state.latlng = this.getLatLng(this.props.marker_location);
}
}
handleGoogleMapApi = (google) => {
this.setState({ ...google, mapLoaded: true});
function MapContainer(props) {
const markerLocation_initialState = props.marker_location ? getLatLng(props.marker_location) : {};
const [ mapLoaded, setMapLoaded ] = useState(false);
const [ latlng, setLatLng ] = useState(markerLocation_initialState);
const prevMarker_location = usePrevious(props.marker_location);
const handleGoogleMapApi = (google) => {
setMapLoaded(true);
const getPolyline = (routeJson) => {
var polyline = new google.maps.Polyline({
path: [],
strokeColor: '#0000FF',
strokeWeight: 8
});
var bounds = new google.maps.LatLngBounds();
var legs = routeJson.routes[0].legs;
for (var i = 0; i < legs.length; i++) {
var steps = legs[i].steps;
for (var j = 0; j < steps.length; j++) {
@ -78,40 +77,44 @@ export class MapContainer extends Component {
polyline.setMap(google.map);
}
getPolyline(drivingLoc);
}
componentWillReceiveProps(nextProps) {
if ( nextProps.marker_location !== this.props.marker_location ){
const nextPos = this.getLatLng(nextProps.marker_location);
const currPos = this.getLatLng(this.props.marker_location);
const newLatLng = {...currPos};
const currThis = this;
var numDeltas = 100;
var delay = 10; //milliseconds
var i = 0;
var deltaLat;
var deltaLng;
transition(nextPos);
function transition(result){
i = 0;
deltaLat = (nextPos.lat - currPos.lat)/numDeltas;
deltaLng = (nextPos.lng - currPos.lng)/numDeltas;
moveMarker();
}
useEffect(() => {
const currPos = getLatLng(prevMarker_location);
const nextPos = getLatLng(props.marker_location);
const newLatLng = {...currPos};
var numDeltas = 100;
var delay = 10;
var i = 0;
var deltaLat;
var deltaLng
function moveMarker(){
newLatLng.lat += deltaLat;
newLatLng.lng += deltaLng;
currThis.setState({ ...currThis.state, latlng: { ...newLatLng }});
if(i!==numDeltas){
i++;
setTimeout(moveMarker, delay);
}
}
transition(nextPos);
function transition(result) {
i = 0;
deltaLat = (nextPos.lat - currPos.lat) / numDeltas;
deltaLng = (nextPos.lng - currPos.lng) / numDeltas;
moveMarker();
}
}
getLatLng(pos) {
function moveMarker() {
newLatLng.lat += deltaLat;
newLatLng.lng += deltaLng;
setLatLng({ ...newLatLng });
if(i!==numDeltas){
i++;
setTimeout(moveMarker, delay);
}
}
}, [props.marker_location]);
function getLatLng(pos) {
if (pos) {
const markerLocationSplit = pos.replace(/[()]/g, "").split(",").map(x => x.trim());
@ -119,34 +122,37 @@ export class MapContainer extends Component {
lat: parseFloat(markerLocationSplit[0]),
lng: parseFloat(markerLocationSplit[1])
};
return markerLocation;
}
return HASURA_LOCATION;
}
render() {
const size = {
width: 320, // map width in pixels
height: 400, // map height in pixels
};
const {center, zoom} = fitBounds(bounds, size);
return (
<div style={{height: '100%'}}>
<GoogleMapReact
bootstrapURLKeys={
{
key: GOOGLE_API_KEY
}
const size = {
width: 320, // map width in pixels
height: 400, // map height in pixels
};
const { center, zoom } = fitBounds(bounds, size);
return (
<div style={{ height: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={
{
key: GOOGLE_API_KEY
}
center={center}
zoom={zoom}
yesIWantToUseGoogleMapApiInternals
onGoogleApiLoaded={this.handleGoogleMapApi}
>
<Marker lat={this.state.latlng.lat ? this.state.latlng.lat : HASURA_LOCATION.lat} lng={this.state.latlng.lng ? this.state.latlng.lng : HASURA_LOCATION.lng } />
</GoogleMapReact>
</div>
);
}
}
center={ center }
zoom={ zoom }
yesIWantToUseGoogleMapApiInternals
onGoogleApiLoaded={ handleGoogleMapApi }
>
<Marker lat={ latlng.lat ? latlng.lat : HASURA_LOCATION.lat} lng={latlng.lng ? latlng.lng : HASURA_LOCATION.lng } />
</GoogleMapReact>
</div>
);
}
export default MapContainer;

View File

@ -1,25 +1,23 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import './UserInfo.css';
class UserInfo extends Component {
render() {
return (
<div className="user_info">
<div className="detail">
<div className="onboarding">
We've created a sample vehicle for this demo. Click on the following button to start tracking this vehicle's realtime location.
</div>
<div className="btn_wrapper">
<button disabled={ this.props.isLoading ? true: false } onClick={ !this.props.isLoading ? this.props.handleTrackLocationClick : () => {}}>
TRACK LOCATION
</button>
</div>
function UserInfo(props) {
return (
<div className="user_info">
<div className="detail">
<div className="onboarding">
We've created a sample vehicle for this demo. Click on the following button to start tracking this vehicle's realtime location.
</div>
<div className="btn_wrapper">
<button disabled={ props.isLoading ? true: false } onClick={ !props.isLoading ? props.handleTrackLocationClick : () => {}}>
TRACK LOCATION
</button>
</div>
</div>
)
}
</div>
)
}
UserInfo.propTypes = {

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,6 @@
// Remove the apollo-boost import and change to this:
import ApolloClient from "apollo-client";
// Setup the network "links"
import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from 'apollo-link-http';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient, InMemoryCache, split, HttpLink } from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
import { WebSocketLink } from "@apollo/client/link/ws";
import { wsurl, httpurl } from './constants';
const wsLink = new WebSocketLink({
@ -14,6 +9,7 @@ const wsLink = new WebSocketLink({
reconnect: true
}
});
const httpLink = new HttpLink({
uri: httpurl,
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

View File

@ -8,8 +8,8 @@ const scheme = (proto) => {
return window.location.protocol === 'https:' ? `${proto}s` : proto;
}
const wsurl = `${scheme('ws')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
const httpurl = `${scheme('http')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
const wsurl = `${scheme('wss')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
const httpurl = `${scheme('https')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
const HASURA_LOCATION = {
lat: 12.93958,

View File

@ -0,0 +1,22 @@
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
import { useEffect, useRef } from 'react';
export function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

View File

@ -0,0 +1,13 @@
// Example from https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
import { useEffect, useRef } from 'react';
export function usePrevious(value) {
const reference = useRef();
useEffect(() => {
reference.current = value;
}, [value]);
return reference.current;
}