diff options
author | elijah <elijah@riseup.net> | 2017-04-24 00:53:41 -0700 |
---|---|---|
committer | Kali Kaneko (leap communications) <kali@leap.se> | 2017-04-24 11:12:26 +0200 |
commit | 9d301349ab434dc744546918fe026d50485a0797 (patch) | |
tree | 802bec24bb446c9ed3af11f7ccfcd9793250b070 /ui/app/components | |
parent | 2976cf11e451f1086d98eae20bdfb0fffa87abb0 (diff) |
[feat] usable vpn ui
Diffstat (limited to 'ui/app/components')
-rw-r--r-- | ui/app/components/main_panel/email_section.js | 6 | ||||
-rw-r--r-- | ui/app/components/main_panel/index.js | 3 | ||||
-rw-r--r-- | ui/app/components/main_panel/main_panel.less | 34 | ||||
-rw-r--r-- | ui/app/components/main_panel/section_layout.js | 10 | ||||
-rw-r--r-- | ui/app/components/main_panel/user_section.js | 4 | ||||
-rw-r--r-- | ui/app/components/main_panel/vpn_section.js | 312 | ||||
-rw-r--r-- | ui/app/components/wizard/wizard.less | 1 |
7 files changed, 291 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/> <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} + + <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} /> ) } diff --git a/ui/app/components/wizard/wizard.less b/ui/app/components/wizard/wizard.less index 29efc20..1606e89 100644 --- a/ui/app/components/wizard/wizard.less +++ b/ui/app/components/wizard/wizard.less @@ -33,6 +33,7 @@ } .wizard .stage .body { + background-color: white; -webkit-flex: 1 1 auto; flex: 1 1 auto; padding: 20px; |