68270164 |
/*
Copyright 2018 Joseph Weston
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react' ;
|
68ba1347 |
import AnimateHeight from 'react-animate-height'
|
68270164 |
import Spinner from 'react-spinkit'
import {
ComposableMap,
ZoomableGroup,
Geographies,
Geography,
} from "react-simple-maps"
|
68ba1347 |
import 'normalize.css/normalize.css'
import './static/styles.css'
|
68270164 |
// Import licence for GEOJSON so as to include it into the webpack output.
// This is necessary to avoid violating the license terms.
import './static/LICENSE'
import World from './static/world.geo.json'
const VPN = Object.freeze({
disconnected:1,
connecting:2,
connected:3,
disconnecting: 4,
|
68ba1347 |
error: 5,
|
68270164 |
}) ;
class Nord extends React.Component {
constructor(props) {
super(props) ;
this.state = {
|
f2e51a96 |
enabled: false,
loading: true,
|
68270164 |
status: VPN.disconnected,
country: null,
host: null,
|
68ba1347 |
viewportWidth: null,
|
68270164 |
} ;
|
f2e51a96 |
this.connection = null
|
68270164 |
|
68ba1347 |
|
68270164 |
this.connect = this.connect.bind(this) ;
|
68ba1347 |
this.updateDimensions = this.updateDimensions.bind(this) ;
|
68270164 |
this.disconnect = this.disconnect.bind(this) ;
|
f2e51a96 |
this.handleData = this.handleData.bind(this) ;
}
|
68ba1347 |
updateDimensions() {
this.setState({
viewportWidth: Math.max(document.documentElement.clientWidth,
window.innerWidth || 0)
}) ;
}
componentWillMount() {
this.updateDimensions() ;
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateDimensions) ;
}
|
f2e51a96 |
componentDidMount() {
|
68ba1347 |
const upstream = location.hostname+(location.port ? ':'+location.port : '')
window.addEventListener("resize", this.updateDimensions) ;
|
f2e51a96 |
this.setState({enabled: false, loading: true})
|
68ba1347 |
this.connection = new WebSocket('ws://'+upstream+'/api') ;
|
f2e51a96 |
this.connection.onopen = () =>
this.setState({enabled: true, loading: false}) ;
this.connection.onerror = () =>
this.setState({enabled: false, loading: false}) ;
this.connection.onclose = () =>
this.setState({enabled: false, loading: false}) ;
this.connection.onmessage = this.handleData ;
}
handleData(msg) {
msg = JSON.parse(msg.data)
switch(msg.state) {
case "connected":
|
68ba1347 |
this.setState({
status: VPN.connected,
host: msg.host
}) ;
|
f2e51a96 |
break ;
case "disconnected":
|
68ba1347 |
this.setState({
status: VPN.disconnected
}) ;
break ;
case "connecting":
this.setState({
status: VPN.connecting,
country: msg.country,
host: null,
}) ;
break ;
case "disconnecting":
this.setState({
status: VPN.disconnecting,
}) ;
break ;
case "error":
this.setState({
status: VPN.error,
error_message: msg.message
}) ;
|
f2e51a96 |
break ;
}
|
68270164 |
}
connect(geography) {
const country_info = geography.properties ;
|
68ba1347 |
// this.setState({
// status: VPN.connecting,
// country: country_info.NAME,
// host: null,
// }) ;
|
f2e51a96 |
this.connection.send(JSON.stringify({
method: 'connect',
country: country_info.ISO_A2
})) ;
|
68270164 |
}
disconnect() {
|
68ba1347 |
// this.setState((prev) => ({ ...prev, status: VPN.disconnecting })) ;
|
f2e51a96 |
this.connection.send(JSON.stringify({
method: 'disconnect'
})) ;
|
68270164 |
}
render() {
return (
|
68ba1347 |
<div id="grid">
<Title className="full-width"/>
<section className="clip">
<VPNStatus connection={ this.state }
onDisconnect={ this.disconnect }
className="full-width overlap"/>
<WorldMap onClick={ this.connect }
viewportWidth={ this.state.viewportWidth }/>
</section>
|
68270164 |
</div>
) ;
}
}
|
68ba1347 |
const Title = (props) => {
return (
<section id="title" className={props.className}>
<h1 className="no-margin">Nord</h1>
</section>
) ;
} ;
|
68270164 |
|
68ba1347 |
const ConnectionSpinner = () => {
return (
<div>
<div className="connection-spinner">
<Spinner name="double-bounce" fadeIn="none" />
</div>
|
68270164 |
</div>
) ;
|
68ba1347 |
} ;
|
68270164 |
|
68ba1347 |
const DisconnectButton = (props) => {
const onClick = props.connected ? props.onDisconnect : null ;
const content = props.connected ? "Disconnect" : <ConnectionSpinner/> ;
return (
<div className="disconnect-button-wrapper">
<button className="disconnect-button red" onClick={ onClick }>
{ content }
|
68270164 |
</button>
|
68ba1347 |
</div>
) ;
} ;
const VPNStatus = (props) => {
const connection = props.connection ;
|
68270164 |
var color = "" ;
|
68ba1347 |
var content = null
|
68270164 |
|
68ba1347 |
switch(connection.status) {
|
68270164 |
case VPN.connecting:
|
68ba1347 |
color = "yellow" ;
content = <div>
Connecting to servers in <b>{ connection.country }</b>
<ConnectionSpinner/>
</div>;
|
68270164 |
break ;
case VPN.connected:
case VPN.disconnecting:
|
68ba1347 |
case VPN.disconnected:
color = "green" ;
content = <div>
Connected to <b>{ connection.host }</b>
<DisconnectButton
connected={ connection.status == VPN.connected }
onDisconnect={ props.onDisconnect }/>
</div> ;
|
68270164 |
break ;
|
68ba1347 |
case VPN.error:
color = "red" ;
content = <div>Error on backend: {connection.error_message}</div> ;
|
68270164 |
}
|
f2e51a96 |
if (!connection.enabled && !connection.loading) {
|
68ba1347 |
color = "red" ;
content = <div>Lost connection to backend</div> ;
|
f2e51a96 |
}
|
68270164 |
return (
|
68ba1347 |
<AnimateHeight
className={props.className}
duration={ 500 }
height={ connection.status == VPN.disconnected ? '0' : 'auto'}>
<div className={"padded " + color}>
{content}
|
68270164 |
</div>
|
68ba1347 |
</AnimateHeight>
|
68270164 |
) ;
} ;
const WorldMap = (props) => {
|
68ba1347 |
var zoom = 7 ;
var center = [ 5, 65 ] ;
if (props.viewportWidth > 500) {
zoom = 4 ;
center = [5, 50]
}
if (props.viewportWidth > 1000) {
zoom = 2 ;
}
|
68270164 |
const default_map_style = {
fill: "#ECEFF1",
stroke: "#607D8B",
strokeWidth: 0.75,
outline: "none",
} ;
const map_container_style = {
width: "100%",
|
68ba1347 |
height: "100%",
|
68270164 |
} ;
const map_style = {
default: default_map_style,
hover: { ...default_map_style, fill: "#CFD8DC" },
pressed: { ...default_map_style, fill: "#CFD8DC" },
} ;
return (
<ComposableMap style={ map_container_style }>
<ZoomableGroup zoom={ zoom } center={ center }>
<Geographies geography={ World }>
{(geographies, projection) => geographies.map(geography => (
<Geography
key={ geography.id }
geography={ geography }
projection={ projection }
style={ map_style }
onClick={ props.onClick }
/>
))}
</Geographies>
</ZoomableGroup>
</ComposableMap>
) ;
} ;
export default Nord ;
|