summaryrefslogtreecommitdiff
path: root/ui/app/components/main_panel
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2017-04-24 00:53:41 -0700
committerKali Kaneko (leap communications) <kali@leap.se>2017-04-24 11:12:26 +0200
commit9d301349ab434dc744546918fe026d50485a0797 (patch)
tree802bec24bb446c9ed3af11f7ccfcd9793250b070 /ui/app/components/main_panel
parent2976cf11e451f1086d98eae20bdfb0fffa87abb0 (diff)
[feat] usable vpn ui
Diffstat (limited to 'ui/app/components/main_panel')
-rw-r--r--ui/app/components/main_panel/email_section.js6
-rw-r--r--ui/app/components/main_panel/index.js3
-rw-r--r--ui/app/components/main_panel/main_panel.less34
-rw-r--r--ui/app/components/main_panel/section_layout.js10
-rw-r--r--ui/app/components/main_panel/user_section.js4
-rw-r--r--ui/app/components/main_panel/vpn_section.js312
6 files changed, 290 insertions, 79 deletions
diff --git a/ui/app/components/main_panel/email_section.js b/ui/app/components/main_panel/email_section.js
index cab637b..fcfd36c 100644
--- a/ui/app/components/main_panel/email_section.js
+++ b/ui/app/components/main_panel/email_section.js
@@ -30,11 +30,11 @@ export default class EmailSection extends React.Component {
componentWillMount() {
this.updateStatus(this.props.account.address)
- bitmask.events.register("MAIL_STATUS_CHANGED", this.updateStatus)
+ bitmask.events.register("MAIL_STATUS_CHANGED", 'email section update', this.updateStatus)
}
componentWillUnmount() {
- bitmask.events.unregister("MAIL_STATUS_CHANGED")
+ bitmask.events.unregister("MAIL_STATUS_CHANGED", 'email section update', this.updateStatus)
}
updateStatus(address) {
@@ -46,7 +46,7 @@ export default class EmailSection extends React.Component {
status: status.status,
error: status.error
})
- },
+ },
error => {
this.setState({
error: error,
diff --git a/ui/app/components/main_panel/index.js b/ui/app/components/main_panel/index.js
index 83c14cf..b70af7c 100644
--- a/ui/app/components/main_panel/index.js
+++ b/ui/app/components/main_panel/index.js
@@ -68,7 +68,8 @@ export default class MainPanel extends React.Component {
if (this.state.account && this.state.provider) {
return this.renderPanel()
} else {
- return <Spinner />
+ return <div className="main-panel">
+ </div>
}
}
diff --git a/ui/app/components/main_panel/main_panel.less b/ui/app/components/main_panel/main_panel.less
index f15eced..2ec9d05 100644
--- a/ui/app/components/main_panel/main_panel.less
+++ b/ui/app/components/main_panel/main_panel.less
@@ -44,6 +44,7 @@
.main-panel > .body {
padding: 20px;
+ background-color: white;
}
.main-panel .accounts {
@@ -196,7 +197,6 @@
}
}
.body-row {
-
}
}
@@ -244,8 +244,13 @@
padding: 0;
font-size: @icon-size - 10;
line-height: @icon-size;
+ display: inline;
word-break: break-all;
}
+ span.info {
+ word-break: break-all;
+ margin-left: 10px;
+ }
}
.buttons {
padding-left: 10px;
@@ -255,12 +260,33 @@
padding-left: @section-padding;
width: @section-padding + @status-size;
img {
- width: @status-size;
- height: @status-size;
+ // this fixes a bug where sometimes the status icons are not
+ // correctly rendered in the web widget
+ width: 100%;
+ height: auto;
+ }
+ &.spin img {
+ animation: icon-spin 10s linear infinite;
+ -webkit-animation: icon-spin 10s linear infinite;
}
}
.body-row {
- padding-top: @section-padding;
+ .alert {
+ margin-bottom: 0px;
+ }
+ // all immediate child divs are considered to be separate sections in
+ // in the body row of the section.
+ & > div {
+ margin-top: @section-padding;
+ }
}
}
+@-webkit-keyframes icon-spin {
+ from { -webkit-transform: rotate(0deg); }
+ to { -webkit-transform: rotate(360deg); }
+}
+@keyframes icon-spin {
+ from {transform:rotate(0deg);}
+ to {transform:rotate(360deg);}
+}
diff --git a/ui/app/components/main_panel/section_layout.js b/ui/app/components/main_panel/section_layout.js
index 3b06d68..e2f1ae1 100644
--- a/ui/app/components/main_panel/section_layout.js
+++ b/ui/app/components/main_panel/section_layout.js
@@ -16,6 +16,7 @@ export default class SectionLayout extends React.Component {
header: null, // the first line content
body: null, // expanded content
message: null, // alert content
+ error: null, // error content
onExpand: null, // callback
className: "",
style: {}
@@ -55,9 +56,13 @@ export default class SectionLayout extends React.Component {
)
}
if (status) {
+ let className = 'status'
+ if (status == 'wait') {
+ className = 'status spin'
+ }
statusIcon = (
- <div className="status">
- <img src={'img/' + status + '.svg' } />
+ <div className={className}>
+ <img width="24px" height="24px" src={'img/' + status + '.svg' } />
</div>
)
}
@@ -79,6 +84,7 @@ export default class SectionLayout extends React.Component {
body = (
<div className="body-row">
{this.props.message}
+ {this.props.error}
{this.props.body}
</div>
)
diff --git a/ui/app/components/main_panel/user_section.js b/ui/app/components/main_panel/user_section.js
index 17e492d..539d03d 100644
--- a/ui/app/components/main_panel/user_section.js
+++ b/ui/app/components/main_panel/user_section.js
@@ -69,7 +69,9 @@ export default class UserSection extends React.Component {
)
}
if (this.state.expanded) {
- body = <UserPasswordForm account={this.props.account} />
+ body = <div>
+ <UserPasswordForm account={this.props.account} />
+ </div>
}
if (this.state.loading) {
button = <Button disabled={true}><Spinner /></Button>
diff --git a/ui/app/components/main_panel/vpn_section.js b/ui/app/components/main_panel/vpn_section.js
index 400d213..4430ad9 100644
--- a/ui/app/components/main_panel/vpn_section.js
+++ b/ui/app/components/main_panel/vpn_section.js
@@ -1,11 +1,11 @@
import React from 'react'
-import { Button, Glyphicon, Alert } from 'react-bootstrap'
+import { Button, ButtonToolbar, Glyphicon, Alert } from 'react-bootstrap'
import SectionLayout from './section_layout'
import Spinner from 'components/spinner'
import bitmask from 'lib/bitmask'
-export default class VPNSection extends React.Component {
+export default class vpnSection extends React.Component {
static get defaultProps() {return{
account: null
@@ -14,32 +14,74 @@ export default class VPNSection extends React.Component {
constructor(props) {
super(props)
this.state = {
- error: null,
- expanded: false,
- vpn: "waiting",
+ interval: null, // timer callback
+ error: null, // error message
+ message: null, // info message
+ expanded: false, // show vpn section expanded or compact
+ ready: false, // true if vpn can start
+ vpn: "unknown", // current state of vpn
+ up: null, // \ throughput
+ down: null // / labels
}
this.expand = this.expand.bind(this)
this.connect = this.connect.bind(this)
this.disconnect = this.disconnect.bind(this)
- this.cancel = this.cancel.bind(this)
- this.unblock = this.unblock.bind(this)
- this.location = this.location.bind(this)
+ this.retry = this.retry.bind(this)
+ this.enable = this.enable.bind(this)
+ this.installHelper = this.installHelper.bind(this)
+
+ this.statusEvent = this.statusEvent.bind(this)
+ this.loginEvent = this.loginEvent.bind(this)
}
+ // called whenever a new account is selected
+ componentWillReceiveProps(nextProps) {
+ this.stopWatchingStatus()
+ if (this.props.account.domain != nextProps.account.domain) {
+ this.checkReadiness(nextProps.account.domain)
+ }
+ }
+
+ // called only once
componentWillMount() {
+ this.checkReadiness()
+ bitmask.events.register("VPN_STATUS_CHANGED", 'vpn section update', this.statusEvent)
+ bitmask.events.register("BONAFIDE_AUTH_DONE", 'vpn section auth', this.loginEvent)
+ }
+
+ // not sure if this is ever called
+ componentWillUnmount() {
+ bitmask.events.unregister("VPN_STATUS_CHANGED", 'vpn section update')
+ bitmask.events.unregister("BONAFIDE_AUTH_DONE", 'vpn section auth')
+ this.stopWatchingStatus()
+ }
+
+ updateStatus(domain = null) {
+ domain = domain || this.props.account.domain
bitmask.vpn.status().then(
- status => {
- console.log("status: ", status)
- if (status.VPN == "OFF") {
+ vpn => {
+ this.stopWatchingStatus()
+ if (vpn.status == "off") {
this.setState({vpn: "down"})
- } else if (status.VPN == "ON") {
- if (status.domain == this.props.account.domain) {
- this.setState({vpn: "up"})
+ } else if (vpn.status == "on") {
+ if (vpn.domain == domain) {
+ this.setState({
+ vpn: "up",
+ up: vpn.up,
+ down: vpn.down
+ })
+ this.startWatchingStatus()
} else {
this.setState({vpn: "down"})
}
+ } else if (vpn.status == "disabled") {
+ this.setState({vpn: "disabled"})
+ } else if (vpn.status == "starting") {
+ this.setState({vpn: "connecting"})
} else {
- // this.setState({vpn: "????"})
+ console.log("UNKNOWN STATUS", vpn.status)
+ // it should not get here...
+ this.setState({vpn: "waiting"})
}
},
error => {
@@ -48,44 +90,119 @@ export default class VPNSection extends React.Component {
)
}
- expand() {
- this.setState({expanded: !this.state.expanded})
- }
-
- // turn on button pressed
- connect() {
- this.setState({vpn: "connecting", error: null})
- bitmask.vpn.check(this.props.account.domain).then(
+ //
+ // Check if all the prerequisites have been met.
+ // This is called whenever the widget is shown, and also whenever a user
+ // authenticates
+ //
+ checkReadiness(domain = null) {
+ domain = domain || this.props.account.domain
+ bitmask.vpn.check(domain).then(
status => {
- console.log('check: ', status)
- if (status.vpn_ready == true) {
- this.startVPN()
- } else if (status.vpn == "disabled") {
- this.setState({vpn: "failed", error: "VPN support disabled"})
+ console.log('check()', status)
+ if (status.vpn == 'disabled') {
+ this.setState({vpn: "disabled"})
+ } else if (!status.installed) {
+ this.setState({vpn: "nohelpers"})
+ } else if (!status.vpn_ready) {
+ this.renewCert()
+ } else {
+ this.setState({
+ message: null,
+ error: null,
+ ready: true
+ })
+ this.updateStatus(domain)
+ }
+ },
+ error => {
+ console.log('check()', error)
+ if (error == "Missing VPN certificate") {
+ this.renewCert()
} else {
- bitmask.vpn.get_cert(this.props.account.id).then(
- uid => {
- this.startVPN()
- },
- error => {
- this.setState({vpn: "failed", error: error})
- }
- )
+ this.setState({vpn: "failed", error: error})
}
+ }
+ )
+ }
+
+ //
+ // install the necessary helper files
+ //
+ installHelper() {
+ bitmask.vpn.install().then(
+ ok => {
+ this.checkReadiness()
},
error => {
+ console.log('install()', error)
this.setState({vpn: "failed", error: error})
}
)
}
+
+ //
+ // event callback: something new has happened, time to re-poll
+ //
+ statusEvent() {
+ console.log('statusEvent')
+ this.updateStatus()
+ }
+
+ //
+ // event callback: the user successfully logged in
+ //
+ loginEvent(event, user) {
+ let address = user[1]
+ this.checkReadiness(address.split('@')[1])
+ }
+
+ //
+ // get new vpn cert from provider
+ //
+ renewCert() {
+ if (!this.props.account.authenticated) {
+ this.setState({
+ message: 'Please log in to renew VPN credentials.',
+ vpn: null
+ })
+ } else {
+ let message = (<div>
+ <Spinner/>&nbsp;<span>Renewing VPN credentials...</span>
+ </div>)
+ this.setState({message: message})
+ bitmask.vpn.get_cert(this.props.account.id).then(
+ ok => {
+ console.log('get_cert()', ok)
+ this.checkReadiness()
+ }, error => {
+ console.log('get_cert()', error)
+ this.setState({vpn: "failed", error: error})
+ }
+ )
+ }
+ }
+
+ // section expand/collapse button pressed
+ expand() {
+ this.setState({expanded: !this.state.expanded})
+ }
+
+ // turn on button pressed
+ connect() {
+ this.setState({vpn: "connecting", error: null})
+ bitmask.vpn.stop().then(
+ wasRunning => {this.startvpn()},
+ wasntRunning => {this.startvpn()}
+ )
+ }
+
// turn off button pressed
disconnect() {
this.setState({vpn: "disconnecting", error: null})
bitmask.vpn.stop().then(
success => {
- console.log('stop:')
- console.log(success)
this.setState({vpn: "down"})
},
error => {
@@ -94,49 +211,72 @@ export default class VPNSection extends React.Component {
)
}
- cancel() {
-
+ // retry button pressed
+ retry() {
+ this.setState({error: null, message: null, vpn: 'waiting'})
+ this.updateStatus()
+ this.checkReadiness()
}
- unblock() {
-
- }
-
- location() {
-
+ // enable button pressed
+ enable() {
+ this.setState({error: null, message: null, vpn: 'waiting'})
+ bitmask.vpn.enable().then(
+ ok => {
+ console.log('enable()', ok)
+ this.retry()
+ },
+ error => {
+ this.setState({error: error, message: null, vpn: 'failed'})
+ console.log('enable(error)', error)
+ }
+ )
}
- // call startVPN() only when everything is ready
- startVPN() {
+ //
+ // call startvpn() only when everything is ready, and no vpn is currently
+ // running.
+ //
+ startvpn() {
bitmask.vpn.start(this.props.account.domain).then(
status => {
- console.log('start: ', status)
- if (status.result == "started") {
- this.setState({vpn: "up", error: null})
- } else {
- this.setState({vpn: "failed"})
- }
- },
- error => {
+ console.log('start success', status)
+ }, error => {
+ console.log('start error', error)
this.setState({vpn: "failed", error: error})
}
)
}
+ startWatchingStatus() {
+ this.interval = setInterval(this.statusEvent, 1000)
+ }
+
+ stopWatchingStatus() {
+ clearInterval(this.interval)
+ }
+
render () {
- console.log(this.state)
let message = null
+ let error = null
let body = null
let button = null
let icon = null
+ let info = null
+ let expand = null // this.expand
- let header = <h1>VPN</h1>
+ // style may be: success, warning, danger, info
if (this.state.error) {
- // style may be: success, warning, danger, info
- message = (
+ error = (
<Alert bsStyle="danger">{this.state.error}</Alert>
)
}
+ if (this.state.message) {
+ message = (
+ <Alert bsStyle="info">{this.state.message}</Alert>
+ )
+ }
+
if (this.state.expanded) {
body = <div>traffic details go here</div>
}
@@ -149,35 +289,71 @@ export default class VPNSection extends React.Component {
case "up":
button = <Button onClick={this.disconnect}>Turn OFF</Button>
icon = "on"
+ info = "Connected"
+ if (this.state.up) {
+ info = <span>
+ <Glyphicon glyph="chevron-down" />
+ {this.state.down}
+ &nbsp;&nbsp;
+ <Glyphicon glyph="chevron-up" />
+ {this.state.up}
+ </span>
+ }
break
case "connecting":
- button = <Button onClick={this.cancel}>Cancel</Button>
+ button = <Button onClick={this.disconnect}>Cancel</Button>
icon = "wait"
+ info = "Connecting..."
break
case "disconnecting":
- button = <Button onClick={this.cancel}>Cancel</Button>
+ button = <Button onClick={this.disconnect}>Cancel</Button>
icon = "wait"
break
case "failed":
- button = <div>
- <Button onClick={this.connect}>Turn ON</Button>
- </div>
- // <Button onClick={this.unblock}>Unblock</Button>
+ info = "Failed"
+ if (this.state.ready) {
+ button = <ButtonToolbar>
+ <Button onClick={this.connect}>Turn ON</Button>
+ <Button onClick={this.disconnect}>Unblock</Button>
+ </ButtonToolbar>
+ } else {
+ button = <Button onClick={this.retry}>Retry</Button>
+ }
icon = "off"
break
case "disabled":
- button = <div>Disabled</div>
+ button = <Button onClick={this.enable}>Enable</Button>
icon = "disabled"
break
case "waiting":
button = <Spinner />
icon = "wait"
break
+ case "nohelpers":
+ body = (
+ <div>
+ <p>The VPN requires that certain helpers are installed on your system.</p>
+ <Button onClick={this.installHelper}>Install Helper Files</Button>
+ </div>
+ )
+ break
+ }
+
+ let header = (
+ <div>
+ <h1>VPN</h1>
+ <span className="info">{info}</span>
+ </div>
+ )
+
+ if (button == null) {
+ expand = null
}
return (
<SectionLayout icon="planet" buttons={button} status={icon}
- onExpand={this.expand} header={header} body={body} message={message} />
+ onExpand={expand} header={header} body={body}
+ message={message} error={error} />
)
}