From 62f069ef0af1444089a4d477f05ac7279897fa32 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 26 Aug 2016 21:09:53 -0700 Subject: [feat] added initial bitmask_js (WIP) --- src/leap/bitmask/core/_web.py | 2 +- src/leap/bitmask_js/README.md | 90 ++++++ src/leap/bitmask_js/__init__.py | 5 + src/leap/bitmask_js/app/app.js | 33 +++ src/leap/bitmask_js/app/components/area.js | 65 +++++ src/leap/bitmask_js/app/components/center.js | 52 ++++ src/leap/bitmask_js/app/components/debug_panel.js | 40 +++ src/leap/bitmask_js/app/components/error_panel.js | 21 ++ .../bitmask_js/app/components/greeter_panel.js | 34 +++ src/leap/bitmask_js/app/components/list_edit.js | 122 +++++++++ src/leap/bitmask_js/app/components/login.js | 302 +++++++++++++++++++++ .../app/components/main_panel/account_list.js | 113 ++++++++ .../app/components/main_panel/email_section.js | 0 .../bitmask_js/app/components/main_panel/index.js | 57 ++++ .../app/components/main_panel/main_panel.less | 208 ++++++++++++++ .../app/components/main_panel/section_layout.js | 59 ++++ .../app/components/main_panel/user_section.js | 71 +++++ .../app/components/main_panel/vpn_section.js | 0 .../bitmask_js/app/components/panel_switcher.js | 56 ++++ .../bitmask_js/app/components/spinner/index.js | 15 + .../bitmask_js/app/components/spinner/spinner.css | 26 ++ src/leap/bitmask_js/app/components/splash.js | 132 +++++++++ .../app/components/wizard/add_provider_modal.js | 94 +++++++ src/leap/bitmask_js/app/components/wizard/index.js | 38 +++ .../app/components/wizard/provider_select_stage.js | 86 ++++++ .../app/components/wizard/stage_layout.js | 37 +++ .../bitmask_js/app/components/wizard/wizard.less | 36 +++ src/leap/bitmask_js/app/css/bootstrap.less | 5 + src/leap/bitmask_js/app/css/common.css | 59 ++++ src/leap/bitmask_js/app/img/arrow-down.svg | 65 +++++ src/leap/bitmask_js/app/img/arrow-up.svg | 65 +++++ src/leap/bitmask_js/app/img/cloud.svg | 65 +++++ src/leap/bitmask_js/app/img/disabled.svg | 72 +++++ src/leap/bitmask_js/app/img/envelope.svg | 64 +++++ src/leap/bitmask_js/app/img/gear.svg | 61 +++++ src/leap/bitmask_js/app/img/mask.svg | 133 +++++++++ src/leap/bitmask_js/app/img/off.svg | 88 ++++++ src/leap/bitmask_js/app/img/on.svg | 78 ++++++ src/leap/bitmask_js/app/img/planet.svg | 60 ++++ src/leap/bitmask_js/app/img/unknown.svg | 82 ++++++ src/leap/bitmask_js/app/img/user.svg | 65 +++++ src/leap/bitmask_js/app/img/wait.svg | 75 +++++ src/leap/bitmask_js/app/index.html | 13 + src/leap/bitmask_js/app/main.js | 23 ++ src/leap/bitmask_js/app/models/account.js | 88 ++++++ src/leap/bitmask_js/notes.txt | 173 ++++++++++++ src/leap/bitmask_js/package.json | 33 +++ src/leap/bitmask_js/webpack.config.js | 77 ++++++ 48 files changed, 3237 insertions(+), 1 deletion(-) create mode 100644 src/leap/bitmask_js/README.md create mode 100644 src/leap/bitmask_js/__init__.py create mode 100644 src/leap/bitmask_js/app/app.js create mode 100644 src/leap/bitmask_js/app/components/area.js create mode 100644 src/leap/bitmask_js/app/components/center.js create mode 100644 src/leap/bitmask_js/app/components/debug_panel.js create mode 100644 src/leap/bitmask_js/app/components/error_panel.js create mode 100644 src/leap/bitmask_js/app/components/greeter_panel.js create mode 100644 src/leap/bitmask_js/app/components/list_edit.js create mode 100644 src/leap/bitmask_js/app/components/login.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/account_list.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/email_section.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/index.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/main_panel.less create mode 100644 src/leap/bitmask_js/app/components/main_panel/section_layout.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/user_section.js create mode 100644 src/leap/bitmask_js/app/components/main_panel/vpn_section.js create mode 100644 src/leap/bitmask_js/app/components/panel_switcher.js create mode 100644 src/leap/bitmask_js/app/components/spinner/index.js create mode 100644 src/leap/bitmask_js/app/components/spinner/spinner.css create mode 100644 src/leap/bitmask_js/app/components/splash.js create mode 100644 src/leap/bitmask_js/app/components/wizard/add_provider_modal.js create mode 100644 src/leap/bitmask_js/app/components/wizard/index.js create mode 100644 src/leap/bitmask_js/app/components/wizard/provider_select_stage.js create mode 100644 src/leap/bitmask_js/app/components/wizard/stage_layout.js create mode 100644 src/leap/bitmask_js/app/components/wizard/wizard.less create mode 100644 src/leap/bitmask_js/app/css/bootstrap.less create mode 100644 src/leap/bitmask_js/app/css/common.css create mode 100644 src/leap/bitmask_js/app/img/arrow-down.svg create mode 100644 src/leap/bitmask_js/app/img/arrow-up.svg create mode 100644 src/leap/bitmask_js/app/img/cloud.svg create mode 100644 src/leap/bitmask_js/app/img/disabled.svg create mode 100644 src/leap/bitmask_js/app/img/envelope.svg create mode 100644 src/leap/bitmask_js/app/img/gear.svg create mode 100644 src/leap/bitmask_js/app/img/mask.svg create mode 100644 src/leap/bitmask_js/app/img/off.svg create mode 100644 src/leap/bitmask_js/app/img/on.svg create mode 100644 src/leap/bitmask_js/app/img/planet.svg create mode 100644 src/leap/bitmask_js/app/img/unknown.svg create mode 100644 src/leap/bitmask_js/app/img/user.svg create mode 100644 src/leap/bitmask_js/app/img/wait.svg create mode 100644 src/leap/bitmask_js/app/index.html create mode 100644 src/leap/bitmask_js/app/main.js create mode 100644 src/leap/bitmask_js/app/models/account.js create mode 100644 src/leap/bitmask_js/notes.txt create mode 100644 src/leap/bitmask_js/package.json create mode 100644 src/leap/bitmask_js/webpack.config.js diff --git a/src/leap/bitmask/core/_web.py b/src/leap/bitmask/core/_web.py index 5107ad13..2c9f2799 100644 --- a/src/leap/bitmask/core/_web.py +++ b/src/leap/bitmask/core/_web.py @@ -46,7 +46,7 @@ class HTTPDispatcherService(service.Service): def startService(self): webdir = os.path.abspath( - pkg_resources.resource_filename("leap.bitmask.core", "web")) + pkg_resources.resource_filename("leap.bitmask_js", "public")) root = File(webdir) api = Api(CommandDispatcher(self._core)) diff --git a/src/leap/bitmask_js/README.md b/src/leap/bitmask_js/README.md new file mode 100644 index 00000000..f1d7c34f --- /dev/null +++ b/src/leap/bitmask_js/README.md @@ -0,0 +1,90 @@ +Bitmask Javascript UI +================================================================= + +Here lies the user interface for Bitmask, written in Javascript. + +quick start: + + npm install # installs development dependencies in "node_modules" + npm run watch # continually rebuilds source .js into "public" + bitmaskd # launch backend + npm run open # opens http://localhost:7070/ in a browser + +build for deployment: + + npm install + npm run build:production + +After 'build', 'build:production', or 'watch' is run, everything needed for +Bitmask JS is contained in the 'public' directory. No additional files are +needed. Open the public/index.html file in a browser or web widget. Because of +the single origin policy of browsers, you will need to open public/index.html +through the webserver included with bitmaskd (e.g. http://localhost:7070) + +Development Dependencies +----------------------------------------------------------------- + +This application has no "runtime" dependencies: all the javascript needed is +bundled and included. However, there are many development dependencies. +Run `npm ls` for a full list. + +**npm** + +Package management, controlled by the package.json file. + +**webpack** + +Asset bundling and transformation. It takes all your javascript and CSS and +bundles it into one (or more) files, handling support for 'require' and scope +separation. + +loaders & plugins: + +* babel-loader: use Babel with Webpack. + +* style-loader/css-loader: standard css loader. + +* less-loader: allows use the less stylesheet. + +* copy-webpack-plugin: allows us to specify what files to copy using Webpack. + +* extract-text-webpack-plugin: allows you to split js and css into separate + files. + +**babel** + +Babel is used to compile javascript into javascript, transforming along the +way. We have enabled these plugins: + +* babel-presets-react: Adds support for React features, such as JSX (html + inlined in js files). + +* babel-presets-es2015: Allows the use of modern ES6 javascript. + +* babel-presets-stage-0: Allows the use of some ES7 proposals, even though + these are not standardized yet. Makes classes nicer. + +**react** + +React is an efficient way to generate HTML views with Javascript. It allows you +to create interactive UIs without ever modifying the DOM by using "one way" +data binding. This greatly simplifies the code and reduces errors. + +**bootstrap** + +The world's most popular CSS styles for UI elements. The npm package includes +both pre-compiled css and less stylesheets. Even though Semantic UI is better, +Bootstrap components for React are much more stable, I have found, and also are +easy to theme. + +To integrate Bootstrap with React: + +* react-bootstrap: React components that use Bootstrap styles. These component + include all the needed javascript and don't require JQuery, although they do + require that the bootstrap CSS is loaded independently. + +**zxcvbn** + +A password strength checker that doesn't suck, but which is big. This JS is +only loaded when we think we are about to need it. + diff --git a/src/leap/bitmask_js/__init__.py b/src/leap/bitmask_js/__init__.py new file mode 100644 index 00000000..d407605e --- /dev/null +++ b/src/leap/bitmask_js/__init__.py @@ -0,0 +1,5 @@ +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/src/leap/bitmask_js/app/app.js b/src/leap/bitmask_js/app/app.js new file mode 100644 index 00000000..57120a4e --- /dev/null +++ b/src/leap/bitmask_js/app/app.js @@ -0,0 +1,33 @@ +import bitmask from 'lib/bitmask' +import Account from 'models/account' + +class Application { + constructor() { + } + + // + // main entry point for the application + // + start() { + Account.active().then(account => { + if (account == null) { + this.show('greeter', {onLogin: this.onLogin.bind(this)}) + } else { + this.show('main', {initialAccount: account}) + } + }, error => { + this.show('error', {error: error}) + }) + } + + onLogin(account) { + this.show('main', {initialAccount: account}) + } + + show(panel, properties) { + this.switcher.show(panel, properties) + } +} + +var App = new Application +export default App \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/area.js b/src/leap/bitmask_js/app/components/area.js new file mode 100644 index 00000000..e903e5f5 --- /dev/null +++ b/src/leap/bitmask_js/app/components/area.js @@ -0,0 +1,65 @@ +// +// A bootstrap panel, but with some extra options +// + +import React from 'react' +// import {Panel} from 'react-bootstrap' + +class Area extends React.Component { + + static get defaultProps() {return{ + position: null, // top or bottom + size: 'small', // small or big + type: null, // light or dark + className: null + }} + + constructor(props) { + super(props) + } + + render() { + let style = {} + let innerstyle = {} + if (this.props.position == 'top') { + style.borderBottomRightRadius = '0px' + style.borderBottomLeftRadius = '0px' + style.marginBottom = '0px' + style.borderBottom = '0px' + if (this.props.size == 'big') { + innerstyle.padding = '25px' + } + } else if (this.props.position == 'bottom') { + style.borderTopRightRadius = '0px' + style.borderTopLeftRadius = '0px' + style.borderTop = '0px' + if (this.props.size == 'big') { + innerstyle.padding = '15px 25px' + } + } + + let type = this.props.type ? "area-" + this.props.type : "" + let className = ['panel', 'panel-default', type, this.props.className].join(' ') + return( +
+
+ {this.props.children} +
+
+ ) + } + +} + +// Area.propTypes = { +// children: React.PropTypes.oneOfType([ +// React.PropTypes.element, +// React.PropTypes.arrayOf(React.PropTypes.element) +// ]) +// } + +//Area.propTypes = { +// children: React.PropTypes.element.isRequired +//} + +export default Area diff --git a/src/leap/bitmask_js/app/components/center.js b/src/leap/bitmask_js/app/components/center.js new file mode 100644 index 00000000..a3b6409a --- /dev/null +++ b/src/leap/bitmask_js/app/components/center.js @@ -0,0 +1,52 @@ +// +// puts a block right in the center of the window +// + +import React from 'react' + +const CONTAINER_CSS = { + position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignContent: 'center', + alignItems: 'center', + top: "0px", + left: "0px", + height: "100%", + width: "100%" +} + +const ITEM_CSS = { + flex: "0 1 auto" +} + +class Center extends React.Component { + + static get defaultProps() {return{ + width: null + }} + + constructor(props) { + super(props) + } + + render() { + let style = this.props.width ? Object.assign({width: this.props.width + 'px'}, ITEM_CSS) : ITEM_CSS + return ( +
+
+ {this.props.children} +
+
+ ) + } +} + +Center.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.arrayOf(React.PropTypes.element) + ]) +} + +export default Center diff --git a/src/leap/bitmask_js/app/components/debug_panel.js b/src/leap/bitmask_js/app/components/debug_panel.js new file mode 100644 index 00000000..7515ba84 --- /dev/null +++ b/src/leap/bitmask_js/app/components/debug_panel.js @@ -0,0 +1,40 @@ +import React from 'react' +import App from '../app' + + +class DebugPanel extends React.Component { + + constructor(props) { + super(props) + this.click = this.click.bind(this) + } + + componentDidMount() { + this.click(window.location.hash.replace('#', '')) + } + + click(panel_name) { + window.location.hash = panel_name + App.show(panel_name) + } + + panel(panel_name) { + return elem( + 'a', + { onClick: () => this.click(panel_name), key: panel_name }, + panel_name + ) + } + + render() { + return elem('div', {className: 'debug-panel'}, + this.panel('splash'), + this.panel('greeter'), + this.panel('wizard'), + this.panel('main') + ) + } + +} + +export default DebugPanel \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/error_panel.js b/src/leap/bitmask_js/app/components/error_panel.js new file mode 100644 index 00000000..fc88d459 --- /dev/null +++ b/src/leap/bitmask_js/app/components/error_panel.js @@ -0,0 +1,21 @@ +import React from 'react' +import Center from './center' +import Area from './area' + +export default class ErrorPanel extends React.Component { + + constructor(props) { + super(props) + } + + render () { + return ( +
+ +

Error

+ {this.props.error} + +
+ ) + } +} diff --git a/src/leap/bitmask_js/app/components/greeter_panel.js b/src/leap/bitmask_js/app/components/greeter_panel.js new file mode 100644 index 00000000..4552db18 --- /dev/null +++ b/src/leap/bitmask_js/app/components/greeter_panel.js @@ -0,0 +1,34 @@ +import React from 'react' +import Login from './login' +import Center from './center' +import Splash from './splash' +import Area from './area' +import { Glyphicon } from 'react-bootstrap' +import App from 'app' + +export default class GreeterPanel extends React.Component { + + constructor(props) { + super(props) + } + + newAccount() { + App.show('wizard') + } + + render () { + return
+ +
+ + + + + +   + Create a new account... + +
+
+ } +} diff --git a/src/leap/bitmask_js/app/components/list_edit.js b/src/leap/bitmask_js/app/components/list_edit.js new file mode 100644 index 00000000..0d557d22 --- /dev/null +++ b/src/leap/bitmask_js/app/components/list_edit.js @@ -0,0 +1,122 @@ +// +// A simple list of items, with minus and plus buttons to add and remove +// items. +// + +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon, FormControl} from 'react-bootstrap' + +const CONTAINER_CSS = { + display: "flex", + flexDirection: "column" +} +const SELECT_CSS = { + padding: "0px", + flex: "1 1 1000px", + overflowY: "scroll" +} +const OPTION_CSS = { + padding: "10px" +} +const TOOLBAR_CSS = { + paddingTop: "10px", + flex: "0 0 auto" +} + +class ListEdit extends React.Component { + + static get defaultProps() {return{ + width: null, + items: [ + 'aaaaaaa', + 'bbbbbbb', + 'ccccccc' + ], + selected: null, + onRemove: null, + onAdd: null, + }} + + constructor(props) { + super(props) + let index = 0 + if (props.selected) { + index = props.items.indexOf(props.selected) + } + this.state = { + selected: index + } + this.click = this.click.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + } + + setSelected(index) { + this.setState({ + selected: index + }) + } + + click(e) { + let row = parseInt(e.target.value) + if (row >= 0) { + this.setState({selected: row}) + } + } + + add() { + if (this.props.onAdd) { + this.props.onAdd() + } + } + + remove() { + if (this.state.selected >= 0 && this.props.onRemove) { + if (this.props.items.length == this.state.selected + 1) { + // if we remove the last item, set the selected item + // to the one right before it. + this.setState({selected: (this.state.selected - 1)}) + } + this.props.onRemove(this.props.items[this.state.selected]) + } + } + + render() { + let options = null + if (this.props.items) { + options = this.props.items.map((item, i) => { + return + }, this) + } + return( +
+ + {options} + + + + + + + +
+ ) + } + +} + +ListEdit.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.arrayOf(React.PropTypes.element) + ]) +} + +export default ListEdit diff --git a/src/leap/bitmask_js/app/components/login.js b/src/leap/bitmask_js/app/components/login.js new file mode 100644 index 00000000..4f7b6289 --- /dev/null +++ b/src/leap/bitmask_js/app/components/login.js @@ -0,0 +1,302 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, + Checkbox, Glyphicon, Overlay, Tooltip, Alert } from 'react-bootstrap' +import Spinner from './spinner' + +import Validate from 'lib/validate' +import App from 'app' +import Account from 'models/account' + +class Login extends React.Component { + + static get defaultProps() {return{ + rememberAllowed: false, // if set, show remember password checkbox + domain: null, // if set, only allow this domain + onLogin: null + }} + + constructor(props) { + super(props) + + // validation states can be null, 'success', 'warning', or 'error' + + this.state = { + loading: false, + + authError: false, // authentication error message + + username: "etest1@riseup.net", + usernameState: null, // username validation state + usernameError: false, // username help message + + password: "whatever", + passwordState: null, // password validation state + passwordError: false, // password help message + + disabled: false, + remember: false // remember is checked? + } + + // prebind: + this.onUsernameChange = this.onUsernameChange.bind(this) + this.onUsernameBlur = this.onUsernameBlur.bind(this) + this.onPassword = this.onPassword.bind(this) + this.onSubmit = this.onSubmit.bind(this) + this.onRemember = this.onRemember.bind(this) + } + + componentDidMount() { + Validate.loadPasswdLib() + } + + render () { + let rememberCheck = "" + let submitButton = "" + let usernameHelp = null + let passwordHelp = null + let message = null + + if (this.props.rememberAllowed) { + let props = { + style: {marginTop: "0px"}, + onChange: this.onRemember + } + + if (this.state.remember) { + rememberCheck = + Remember username and password + + } else { + rememberCheck = + Remember username and password + + } + } + + if (this.state.authError) { + // style may be: success, warning, danger, info + message = ( + {this.state.authError} + ) + } + + if (this.state.usernameError) { + usernameHelp = {this.state.usernameError} + // let props = {shouldUpdatePosition: true, show:true, placement:"right", + // target:this.refs.username} + // usernameHelp = ( + // + // {this.state.usernameError} + // + // ) + } else { + //usernameHelp =   + } + + if (this.state.passwordError) { + passwordHelp = {this.state.passwordError} + // let props = {shouldUpdatePosition: true, show:true, placement:"right", + // target:this.refs.password, component: {this}} + // passwordHelp = ( + // + // {this.state.passwordError} + // + // ) + } else { + //passwordHelp =   + } + + let buttonProps = { + type: "button", + onClick: this.onSubmit, + disabled: !this.maySubmit() + } + if (this.state.loading) { + submitButton = + } else { + submitButton = + } + + let usernameref = null + if (this.props.domain) { + usernameref = function(c) { + if (c != null) { + let textarea = ReactDOM.findDOMNode(c) + let start = textarea.value.indexOf('@') + if (textarea.selectionStart > start) { + textarea.setSelectionRange(start, start) + } + } + } + } + + let form =
+ {message} + + Username + + {this.state.usernameState == 'success' ? null : } + {usernameHelp} + + + + Password + + {this.state.passwordState == 'success' ? null : } + {passwordHelp} + + + {submitButton} + {rememberCheck} +
+ + return form + } + + // + // Here we do a partial validation, because the user has not stopped typing. + // + onUsernameChange(e) { + let username = e.target.value.toLowerCase().replace("\n", "") + if (this.props.domain) { + let [userpart, domainpart] = username.split( + new RegExp('@|' + this.props.domain.replace(".", "\\.") + '$') + ) + username = [userpart, this.props.domain].join('@') + } + let error = Validate.usernameInteractive(username, this.props.domain) + let state = null + if (error) { + state = 'error' + } else { + if (username && username.length > 0) { + let finalError = Validate.username(username) + state = finalError ? null : 'success' + } + } + this.setState({ + username: username, + usernameState: state, + usernameError: error ? error : null + }) + } + + // + // Here we do a more complete validation, since the user have left the field. + // + onUsernameBlur(e) { + let username = e.target.value.toLowerCase() + this.setState({ + username: username + }) + if (username.length > 0) { + this.validateUsername(username) + } else { + this.setState({ + usernameState: null, + usernameError: null + }) + } + } + + onPassword(e) { + let password = e.target.value + this.setState({password: password}) + if (password.length > 0) { + this.validatePassword(password) + } else { + this.setState({ + passwordState: null, + passwordError: null + }) + } + } + + onRemember(e) { + let currentValue = e.target.value == 'on' ? true : false + let value = !currentValue + this.setState({remember: value}) + } + + validateUsername(username) { + let error = Validate.username(username, this.props.domain) + this.setState({ + usernameState: error ? 'error' : 'success', + usernameError: error ? error : null + }) + } + + validatePassword(password) { + let state = null + let message = null + let result = Validate.passwordStrength(password) + if (result) { + message = "Time to crack: " + result.crack_times_display.offline_slow_hashing_1e4_per_second + if (result.score == 0) { + state = 'error' + } else if (result.score == 1 || result.score == 2) { + state = 'warning' + } else { + state = 'success' + } + } + this.setState({ + passwordState: state, + passwordError: message + }) + } + + maySubmit() { + return( + !this.stateLoading && + !this.state.usernameError && + this.state.username != "" && + this.state.password != "" + ) + } + + onSubmit(e) { + e.preventDefault() // don't reload the page please! + if (!this.maySubmit()) { return } + this.setState({loading: true}) + + let account = new Account(this.state.username) + account.login(this.state.password).then( + account => { + this.setState({loading: false}) + if (this.props.onLogin) { + this.props.onLogin(account) + } + }, + error => { + console.log(error) + if (error == "") { + error = 'Something failed, but we did not get a message' + } + this.setState({ + loading: false, + usernameState: 'error', + passwordState: 'error', + authError: error + }) + } + ) + } + +} + +export default Login \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/main_panel/account_list.js b/src/leap/bitmask_js/app/components/main_panel/account_list.js new file mode 100644 index 00000000..d0ef092f --- /dev/null +++ b/src/leap/bitmask_js/app/components/main_panel/account_list.js @@ -0,0 +1,113 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' + +import App from 'app' +import Account from 'models/account' + +export default class AccountList extends React.Component { + + static get defaultProps() {return{ + account: null, + accounts: [], + onAdd: null, + onRemove: null, + onSelect: null + }} + + constructor(props) { + super(props) + + this.state = { + mode: 'expanded' + } + + // prebind: + this.select = this.select.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.expand = this.expand.bind(this) + this.collapse = this.collapse.bind(this) + } + + select(e) { + let account = this.props.accounts.find( + account => account.id == e.currentTarget.dataset.id + ) + if (this.props.onSelect) { + this.props.onSelect(account) + } + } + + add() { + App.show('wizard') + } + + remove() { + } + + expand() { + this.setState({mode: 'expanded'}) + } + + collapse() { + this.setState({mode: 'collapsed'}) + } + + render() { + let style = {} + let expandButton = null + let plusminusButtons = null + + if (this.state.mode == 'expanded') { + expandButton = ( + + ) + plusminusButtons = ( + + + + + ) + } else { + style.width = '60px' + expandButton = ( + + ) + } + + let items = this.props.accounts.map((account, i) => { + let className = account == this.props.account ? 'active' : 'inactive' + return ( +
  • + {account.userpart} + {account.domain} + + +
  • + ) + }) + + + return ( +
    + + + {plusminusButtons} + {expandButton} + +
    + ) + } + + +} diff --git a/src/leap/bitmask_js/app/components/main_panel/email_section.js b/src/leap/bitmask_js/app/components/main_panel/email_section.js new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask_js/app/components/main_panel/index.js b/src/leap/bitmask_js/app/components/main_panel/index.js new file mode 100644 index 00000000..910d58b6 --- /dev/null +++ b/src/leap/bitmask_js/app/components/main_panel/index.js @@ -0,0 +1,57 @@ +// +// The main panel manages the current account and the list of available accounts +// +// It displays multiple sections, one for each service. +// + +import React from 'react' +import App from 'app' +import Login from 'components/login' + +import './main_panel.less' +import AccountList from './account_list' +import UserSection from './user_section' + +export default class MainPanel extends React.Component { + + static get defaultProps() {return{ + initialAccount: null + }} + + constructor(props) { + super(props) + this.state = { + account: null, + accounts: [] + } + this.activateAccount = this.activateAccount.bind(this) + } + + componentWillMount() { + if (this.props.initialAccount) { + this.setState({ + account: this.props.initialAccount, + accounts: [this.props.initialAccount] + }) + } + } + + activateAccount(account) { + this.setState({ + account: account, + accounts: [account] + }) + } + + render() { + return ( +
    + +
    + +
    +
    + ) + } + +} diff --git a/src/leap/bitmask_js/app/components/main_panel/main_panel.less b/src/leap/bitmask_js/app/components/main_panel/main_panel.less new file mode 100644 index 00000000..3172bba5 --- /dev/null +++ b/src/leap/bitmask_js/app/components/main_panel/main_panel.less @@ -0,0 +1,208 @@ +// The space around account entries: +@accounts-padding: 8px; +@accounts-width: 200px; + +// +// LAYOUT +// + +.main-panel { + position: absolute; + height: 100%; + width: 100%; + display: flex; + flex-direction: row; + + > .body { + flex: 1 1 auto; + overflow: auto; + } + + .accounts { + flex: 0 0 auto; + overflow-y: auto; + overflow-x: hidden; + + display: flex; + flex-direction: column; + ul { + flex: 1 1 1000px; + } + .btn-toolbar { + flex: 0 0 auto; + } + + } + +} + +// +// Style +// + +.main-panel > .body { + padding: 20px; +} + +.main-panel .accounts { + background-color: #333; + width: @accounts-width; + padding: @accounts-padding; + padding-right: 0px; +} + +.main-panel .accounts ul { + list-style: none; + margin: 0; + padding: 0; +} + +.main-panel .accounts li { + position: relative; + cursor: pointer; + color: white; + padding: 15px; + background-color: #444; + margin-bottom: @accounts-padding; + border-top-left-radius: @accounts-padding; + border-bottom-left-radius: @accounts-padding; + z-index: 100; +} + +.main-panel .accounts li span.domain { + display: block; + font-weight: bold; + //margin-left: 40px; +} + +.main-panel .accounts li span.username { + display: block; + //margin-left: 40px; + //line-height: 7px; + //margin-bottom: 4px; +} + +/*.main-panel .accounts li span.icon { + display: block; + height: 32px; + width: 32px; + background-color: #999; + float: left; +} +*/ + +.main-panel .accounts li.active { + background-color: white; + color: #333; +} + +.main-panel .accounts li.active span.arc { + display: block; + height: @accounts-padding; + width: @accounts-padding; + background-color: white; + position: absolute; + right: 0; +} + +.main-panel .accounts li.active span.arc.top { + top: 0; + margin-top: -@accounts-padding; +} +.main-panel .accounts li.active span.arc.bottom { + bottom: 0; + margin-bottom: -@accounts-padding; +} +.main-panel .accounts li.active span.arc:after { + display: block; + content: ""; + border-radius: 100%; + height: 0px; + width: 0px; + margin-left: -@accounts-padding; +} +.main-panel .accounts li.active span.arc.top:after { + border: @accounts-padding solid transparent; + border-right: @accounts-padding solid #333; + margin-top: -@accounts-padding; + transform: rotate(45deg); +} +.main-panel .accounts li.active span.arc.bottom:after { + border: @accounts-padding solid #333; +} + +.main-panel .accounts .btn.expander { + margin-right: @accounts-padding; +} + + +// +// SECTIONS +// + +@icon-size: 32px; +@status-size: 24px; +@section-padding: 10px; + +// service sections layout + +.main-panel .service-section { + display: flex; + flex-direction: row; + > .icon { + flex: 0 0 auto; + } + > .body { + flex: 1 1 auto; + } + > .buttons { + flex: 0 0 auto; + } + > .status { + flex: 0 0 auto; + display: flex; + align-items: center; + } +} + +.main-panel .service-section div { + //outline: 1px solid rgba(0,0,0,0.1); +} + +// service sections style + +.main-panel .service-section { + background: #f6f6f6; + border-radius: 4px; + padding: 10px; + &.wide-margin { + padding: 20px 20px 20px 10px; // arbitrary, looks nice + } + > .icon { + padding-right: @section-padding; + img { + width: @icon-size; + height: @icon-size; + } + } + > .body { + h1 { + margin: 0; + padding: 0; + font-size: @icon-size - 10; + line-height: @icon-size; + } + } + > .buttons { + padding-left: 10px; + } + > .status { + padding-left: @section-padding; + width: @section-padding + @status-size; + img { + width: @status-size; + height: @status-size; + } + } +} + diff --git a/src/leap/bitmask_js/app/components/main_panel/section_layout.js b/src/leap/bitmask_js/app/components/main_panel/section_layout.js new file mode 100644 index 00000000..e7c6f2ab --- /dev/null +++ b/src/leap/bitmask_js/app/components/main_panel/section_layout.js @@ -0,0 +1,59 @@ +// +// This is the layout for a service section in the main window. +// It does not do anything except for arrange items using css and html. +// + +import React from 'react' + +export default class SectionLayout extends React.Component { + + static get defaultProps() {return{ + icon: null, + buttons: null, + status: null, + className: "", + style: {} + }} + + constructor(props) { + super(props) + } + + render() { + let className = ["service-section", this.props.className].join(' ') + let status = null + let icon = null + let buttons = null + + if (this.props.status) { + status = ( +
    + +
    + ) + } + if (this.props.icon) { + icon = ( +
    + +
    + ) + } + if (this.props.buttons) + buttons = ( +
    + {this.props.buttons} +
    + ) + return( +
    + {icon} +
    + {this.props.children} +
    + {buttons} + {status} +
    + ) + } +} diff --git a/src/leap/bitmask_js/app/components/main_panel/user_section.js b/src/leap/bitmask_js/app/components/main_panel/user_section.js new file mode 100644 index 00000000..0b4ba136 --- /dev/null +++ b/src/leap/bitmask_js/app/components/main_panel/user_section.js @@ -0,0 +1,71 @@ +import React from 'react' +import { Button, Glyphicon, Alert } from 'react-bootstrap' +import SectionLayout from './section_layout' +import Login from 'components/login' +import Spinner from 'components/spinner' +import Account from 'models/account' + +import bitmask from 'lib/bitmask' + +export default class UserSection extends React.Component { + + static get defaultProps() {return{ + account: null, + onLogout: null, + onLogin: null + }} + + constructor(props) { + super(props) + this.state = { + error: null, + loading: false + } + this.logout = this.logout.bind(this) + } + + logout() { + this.setState({loading: true}) + this.props.account.logout().then( + account => { + this.setState({error: null, loading: false}) + if (this.props.onLogout) { + this.props.onLogout(account) + } + }, error => { + this.setState({error: error, loading: false}) + } + ) + } + + render () { + let message = null + if (this.state.error) { + // style may be: success, warning, danger, info + message = ( + {this.state.error} + ) + } + + if (this.props.account.authenticated) { + let button = null + if (this.state.loading) { + button = + } else { + button = + } + return ( + +

    {this.props.account.address}

    + {message} +
    + ) + } else { + return ( + + + + ) + } + } +} diff --git a/src/leap/bitmask_js/app/components/main_panel/vpn_section.js b/src/leap/bitmask_js/app/components/main_panel/vpn_section.js new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask_js/app/components/panel_switcher.js b/src/leap/bitmask_js/app/components/panel_switcher.js new file mode 100644 index 00000000..f4f32cf6 --- /dev/null +++ b/src/leap/bitmask_js/app/components/panel_switcher.js @@ -0,0 +1,56 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import DebugPanel from './debug_panel' +import Splash from './splash' +import GreeterPanel from './greeter_panel' +import MainPanel from './main_panel' +import Wizard from './wizard' + +import App from 'app' +import 'lib/common' + +export default class PanelSwitcher extends React.Component { + + constructor(props) { + super(props) + this.state = { + panel: null, + panel_properties: null, + debug: false + } + App.switcher = this + } + + show(component_name, properties={}) { + this.setState({panel: component_name, panel_properties: properties}) + } + + render() { + let elems = [] + if (this.panelExist(this.state.panel)) { + elems.push( + this.panelRender(this.state.panel, this.state.panel_properties) + ) + } + if (this.state.debug) { + elems.push( + elem(DebugPanel, {key: 'debug'}) + ) + } + return
    {elems}
    + } + + panelExist(panel) { + return panel && this['render_'+panel] + } + + panelRender(panel_name, props) { + let panel = this['render_'+panel_name](props) + return elem('div', {key: 'panel'}, panel) + } + + render_splash(props) {return elem(Splash, props)} + render_wizard(props) {return elem(Wizard, props)} + render_greeter(props) {return elem(GreeterPanel, props)} + render_main(props) {return elem(MainPanel, props)}} diff --git a/src/leap/bitmask_js/app/components/spinner/index.js b/src/leap/bitmask_js/app/components/spinner/index.js new file mode 100644 index 00000000..ffc32850 --- /dev/null +++ b/src/leap/bitmask_js/app/components/spinner/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import './spinner.css'; + +class Spinner extends React.Component { + render() { + let props = {} + return
    +
    +
    +
    +
    + } +} + +export default Spinner \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/spinner/spinner.css b/src/leap/bitmask_js/app/components/spinner/spinner.css new file mode 100644 index 00000000..4e7f3ee8 --- /dev/null +++ b/src/leap/bitmask_js/app/components/spinner/spinner.css @@ -0,0 +1,26 @@ +.spinner { + height: 18px; + display: inline-block; +} + +.spinner > div { + width: 18px; + height: 18px; + background-color: #000; + vertical-align: middle; + border-radius: 100%; + display: inline-block; + animation: bouncedelay 1.5s infinite ease-in-out; + animation-fill-mode: both; +} + +.spinner .spin1 { animation-delay: -.46s } +.spinner .spin2 { animation-delay: -.24s } + +@keyframes bouncedelay { + 0%, 80%, 100% { + transform: scale(0.5); + } 40% { + transform: scale(0.9); + } +} diff --git a/src/leap/bitmask_js/app/components/splash.js b/src/leap/bitmask_js/app/components/splash.js new file mode 100644 index 00000000..46170d23 --- /dev/null +++ b/src/leap/bitmask_js/app/components/splash.js @@ -0,0 +1,132 @@ +/* + * A simple animated splash screen + */ + +import React from 'react' +import * as COLOR from '../lib/colors' + +const colorList = [ + COLOR.red200, COLOR.pink200, COLOR.purple200, COLOR.deepPurple200, + COLOR.indigo200, COLOR.blue200, COLOR.lightBlue200, COLOR.cyan200, + COLOR.teal200, COLOR.green200, COLOR.lightGreen200, COLOR.lime200, + COLOR.yellow200, COLOR.amber200, COLOR.orange200, COLOR.deepOrange200 +] + +export default class Splash extends React.Component { + + static get defaultProps() {return{ + speed: "fast", + mask: true, + onClick: null + }} + + constructor(props) { + super(props) + this.counter = 0 + this.interval = null + this.ctx = null + this.stepAngle = 0 + this.resize = this.resize.bind(this) + this.click = this.click.bind(this) + if (this.props.speed == "fast") { + this.fps = 30 + this.stepAngle = 0.005 + } else { + this.fps = 30 + this.stepAngle = 0.0005 + } + } + + componentDidMount() { + this.interval = setInterval(this.tick.bind(this), 1000/this.fps) + this.canvas = this.refs.canvas + this.ctx = this.canvas.getContext('2d') + window.addEventListener('resize', this.resize) + } + + componentWillUnmount() { + clearInterval(this.interval) + window.removeEventListener('resize', this.resize) + } + + click() { + if (this.props.onClick) { + this.props.onClick() + } + } + + tick() { + this.counter++ + this.updateCanvas() + } + + resize() { + this.canvas.width = window.innerWidth + this.canvas.height = window.innerHeight + this.updateCanvas() + } + + updateCanvas() { + const arcCount = 16 + const arcAngle = 1 / arcCount + const x = this.canvas.width / 2 + const y = this.canvas.height / 2 + const radius = screen.height + screen.width + + for (let i = 0; i < arcCount; i++) { + let startAngle = Math.PI * 2 * i/arcCount + this.stepAngle*this.counter + let endAngle = Math.PI * 2 * (i+1)/arcCount + this.stepAngle*this.counter + + this.ctx.fillStyle = colorList[i % colorList.length] + this.ctx.strokeStyle = colorList[i % colorList.length] + this.ctx.beginPath() + this.ctx.moveTo(x, y) + this.ctx.arc(x, y, radius, startAngle, endAngle) + this.ctx.lineTo(x, y) + this.ctx.fill() + this.ctx.stroke() + } + + } + + render () { + let overlay = null + let mask = null + if (this.props.onClick) { + overlay = React.DOM.div({ + style: { + position: 'absolute', + height: '100%', + width: '100%', + backgroundColor: 'transparent' + }, + onClick: this.click + }) + } + if (this.props.mask) { + mask = React.DOM.img({ + src: 'img/mask.svg', + style: { + position: 'absolute', + left: '50%', + top: '50%', + marginLeft: -330/2 + 'px', + marginTop: -174/2 + 'px', + } + }) + } + return React.DOM.div( + {style: {overflow: 'hidden'}}, + React.DOM.canvas({ + ref: 'canvas', + style: {position: 'absolute'}, + width: window.innerWidth, + height: window.innerHeight, + }), + mask, + overlay + ) + } + +} + diff --git a/src/leap/bitmask_js/app/components/wizard/add_provider_modal.js b/src/leap/bitmask_js/app/components/wizard/add_provider_modal.js new file mode 100644 index 00000000..bc5e0236 --- /dev/null +++ b/src/leap/bitmask_js/app/components/wizard/add_provider_modal.js @@ -0,0 +1,94 @@ +// +// A modal popup to add a new provider. +// + +import React from 'react' +import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, Modal } from 'react-bootstrap' +import Spinner from '../spinner' +import Validate from '../../lib/validate' +import App from '../../app' + +class AddProviderModal extends React.Component { + + static get defaultProps() {return{ + title: 'Add a provider', + onClose: null + }} + + constructor(props) { + super(props) + this.state = { + validationState: null, + errorMsg: null, + domain: "" + } + this.accept = this.accept.bind(this) + this.cancel = this.cancel.bind(this) + this.changed = this.changed.bind(this) + } + + accept() { + if (this.state.domain) { + App.providers.add(this.state.domain) + } + this.props.onClose() + } + + cancel() { + this.props.onClose() + } + + changed(e) { + let domain = e.target.value + let newState = null + let newMsg = null + + if (domain.length > 0) { + let error = Validate.domain(domain) + newState = error ? 'error' : 'success' + newMsg = error + } + this.setState({ + domain: domain, + validationState: newState, + errorMsg: newMsg + }) + } + + render() { + let help = null + if (this.state.errorMsg) { + help = {this.state.errorMsg} + } else { + help =   + } + let form =
    + + Domain + + + {help} + + +
    + + return( + + + {this.props.title} + + + {form} + + + ) + } +} + +export default AddProviderModal \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/wizard/index.js b/src/leap/bitmask_js/app/components/wizard/index.js new file mode 100644 index 00000000..613b88fd --- /dev/null +++ b/src/leap/bitmask_js/app/components/wizard/index.js @@ -0,0 +1,38 @@ +// +// The provider setup wizard +// + +import React from 'react' +import App from 'app' + +import ProviderSelectStage from './provider_select_stage' +import './wizard.less' + +export default class Wizard extends React.Component { + + constructor(props) { + super(props) + this.state = { + stage: 'provider' + } + } + + setStage(stage) { + this.setState({stage: stage}) + } + + render() { + let stage = null + switch(this.state.stage) { + case 'provider': + stage = + break + } + return( +
    + {stage} +
    + ) + } + +} diff --git a/src/leap/bitmask_js/app/components/wizard/provider_select_stage.js b/src/leap/bitmask_js/app/components/wizard/provider_select_stage.js new file mode 100644 index 00000000..20674be1 --- /dev/null +++ b/src/leap/bitmask_js/app/components/wizard/provider_select_stage.js @@ -0,0 +1,86 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' + +import App from 'app' +import ListEdit from 'components/list_edit' +import StageLayout from './stage_layout' +import AddProviderModal from './add_provider_modal' + +export default class ProviderSelectStage extends React.Component { + + static get defaultProps() {return{ + title: "Choose a provider", + subtitle: "This doesn't work yet" + }} + + constructor(props) { + super(props) + let domains = this.currentDomains() + this.state = { + domains: domains, + showModal: false + } + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.close = this.close.bind(this) + this.previous = this.previous.bind(this) + } + + currentDomains() { + // return(App.providers.domains().slice() || []) + return ['domain1', 'domain2', 'domain3'] + } + + add() { + this.setState({showModal: true}) + } + + remove(provider) { + // App.providers.remove(provider) + this.setState({domains: this.currentDomains()}) + } + + close() { + let domains = this.currentDomains() + if (domains.length != this.state.domains.length) { + // this is ugly, but i could not get selection working + // by passing it as a property + this.refs.list.setSelected(0) + } + this.setState({ + domains: domains, + showModal: false + }) + } + + previous() { + App.start() + } + + render() { + let modal = null + if (this.state.showModal) { + modal = + } + let buttons = ( + + + + + ) + let select = + return( + + {select} + {modal} + + ) + } +} diff --git a/src/leap/bitmask_js/app/components/wizard/stage_layout.js b/src/leap/bitmask_js/app/components/wizard/stage_layout.js new file mode 100644 index 00000000..31540221 --- /dev/null +++ b/src/leap/bitmask_js/app/components/wizard/stage_layout.js @@ -0,0 +1,37 @@ +import React from 'react' + +class StageLayout extends React.Component { + + static get defaultProps() {return{ + title: 'untitled', + subtitle: null, + buttons: null + }} + + constructor(props) { + super(props) + } + + render() { + let subtitle = null + if (this.props.subtitle) { + subtitle = {this.props.subtitle} + } + return( +
    +
    + {this.props.title} + {subtitle} +
    +
    + {this.props.children} +
    +
    + {this.props.buttons} +
    +
    + ) + } +} + +export default StageLayout \ No newline at end of file diff --git a/src/leap/bitmask_js/app/components/wizard/wizard.less b/src/leap/bitmask_js/app/components/wizard/wizard.less new file mode 100644 index 00000000..3336ffb8 --- /dev/null +++ b/src/leap/bitmask_js/app/components/wizard/wizard.less @@ -0,0 +1,36 @@ +.wizard .stage { + position: absolute; + height: 100%; + width: 100%; + + display: flex; + flex-direction: column; + flex: 1; +} + +.wizard .stage .footer { + flex: 0 0 auto; + background-color: #ddd; + padding: 20px; + text-align: right; +} + +.wizard .stage .header { + flex: 0 0 auto; + padding: 20px; + background-color: #333; + color: white; + font-size: 2em; + span { + margin-left: 10px; + font-size: 0.5em; + } +} + +.wizard .stage .body { + flex: 1 1 auto; + padding: 20px; + overflow: auto; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/leap/bitmask_js/app/css/bootstrap.less b/src/leap/bitmask_js/app/css/bootstrap.less new file mode 100644 index 00000000..3b772284 --- /dev/null +++ b/src/leap/bitmask_js/app/css/bootstrap.less @@ -0,0 +1,5 @@ +// +// require npm modules 'bootstrap' +// +@import "~bootstrap/less/bootstrap"; + diff --git a/src/leap/bitmask_js/app/css/common.css b/src/leap/bitmask_js/app/css/common.css new file mode 100644 index 00000000..4b48010a --- /dev/null +++ b/src/leap/bitmask_js/app/css/common.css @@ -0,0 +1,59 @@ +body { + padding: 0; + margin: 0; +} + +.debug-panel { + background-color: rgba(0,0,0,0.1); + position: absolute; + bottom: 0; + left: 50%; + z-index: 1000; + padding: 20px; +} +.debug-panel a { + cursor: pointer; + margin: 10px; +} + + +.area-light { + background-color: #f7f7f7; +} +.area-dark { + background-color: #e7e7e7; +} +.area-clear { + background-color: #fff; +} + +/* + * Greeter + */ +.greeter { + border-color: #555; + border-width: 8px; +} + +/* + * bootstrap + */ + +.help-block { + margin: 2px 0 0 0; + font-size: small; + opacity: 0.7; +} +.btn.btn-inverse { + color: white; + background-color: #333; +} +.btn.btn-flat { + border-color: transparent; + background-color: transparent; +} + +/*.btn.btn-default { + background-color: #eee !important; +} +*/ \ No newline at end of file diff --git a/src/leap/bitmask_js/app/img/arrow-down.svg b/src/leap/bitmask_js/app/img/arrow-down.svg new file mode 100644 index 00000000..14c4b5d1 --- /dev/null +++ b/src/leap/bitmask_js/app/img/arrow-down.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/arrow-up.svg b/src/leap/bitmask_js/app/img/arrow-up.svg new file mode 100644 index 00000000..30ea8fde --- /dev/null +++ b/src/leap/bitmask_js/app/img/arrow-up.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/cloud.svg b/src/leap/bitmask_js/app/img/cloud.svg new file mode 100644 index 00000000..bf7a94af --- /dev/null +++ b/src/leap/bitmask_js/app/img/cloud.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/disabled.svg b/src/leap/bitmask_js/app/img/disabled.svg new file mode 100644 index 00000000..90804d83 --- /dev/null +++ b/src/leap/bitmask_js/app/img/disabled.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/envelope.svg b/src/leap/bitmask_js/app/img/envelope.svg new file mode 100644 index 00000000..5775e097 --- /dev/null +++ b/src/leap/bitmask_js/app/img/envelope.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/gear.svg b/src/leap/bitmask_js/app/img/gear.svg new file mode 100644 index 00000000..be5a5b33 --- /dev/null +++ b/src/leap/bitmask_js/app/img/gear.svg @@ -0,0 +1,61 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/mask.svg b/src/leap/bitmask_js/app/img/mask.svg new file mode 100644 index 00000000..f254c5e0 --- /dev/null +++ b/src/leap/bitmask_js/app/img/mask.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/off.svg b/src/leap/bitmask_js/app/img/off.svg new file mode 100644 index 00000000..35c49a56 --- /dev/null +++ b/src/leap/bitmask_js/app/img/off.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/on.svg b/src/leap/bitmask_js/app/img/on.svg new file mode 100644 index 00000000..36938e0a --- /dev/null +++ b/src/leap/bitmask_js/app/img/on.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/planet.svg b/src/leap/bitmask_js/app/img/planet.svg new file mode 100644 index 00000000..5697d5d6 --- /dev/null +++ b/src/leap/bitmask_js/app/img/planet.svg @@ -0,0 +1,60 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/unknown.svg b/src/leap/bitmask_js/app/img/unknown.svg new file mode 100644 index 00000000..60038638 --- /dev/null +++ b/src/leap/bitmask_js/app/img/unknown.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/user.svg b/src/leap/bitmask_js/app/img/user.svg new file mode 100644 index 00000000..ad86a757 --- /dev/null +++ b/src/leap/bitmask_js/app/img/user.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/img/wait.svg b/src/leap/bitmask_js/app/img/wait.svg new file mode 100644 index 00000000..2ae0113a --- /dev/null +++ b/src/leap/bitmask_js/app/img/wait.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/leap/bitmask_js/app/index.html b/src/leap/bitmask_js/app/index.html new file mode 100644 index 00000000..440e0b6f --- /dev/null +++ b/src/leap/bitmask_js/app/index.html @@ -0,0 +1,13 @@ + + + + + Bitmask + + + + +
    + + + \ No newline at end of file diff --git a/src/leap/bitmask_js/app/main.js b/src/leap/bitmask_js/app/main.js new file mode 100644 index 00000000..0d712af6 --- /dev/null +++ b/src/leap/bitmask_js/app/main.js @@ -0,0 +1,23 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import PanelSwitcher from 'components/panel_switcher' +import App from 'app' + +class Main extends React.Component { + render() { + return React.createElement(PanelSwitcher) + } + + // + // main entry point for app execution + // + componentDidMount() { + App.start() + } +} + +ReactDOM.render( + React.createElement(Main), + document.getElementById('app') +) \ No newline at end of file diff --git a/src/leap/bitmask_js/app/models/account.js b/src/leap/bitmask_js/app/models/account.js new file mode 100644 index 00000000..367961bf --- /dev/null +++ b/src/leap/bitmask_js/app/models/account.js @@ -0,0 +1,88 @@ +// +// An account is an abstraction of a user and a provider. +// The user part is optional, so an Account might just represent a provider. +// + +import bitmask from 'lib/bitmask' + +export default class Account { + + constructor(address, props={}) { + if (!address.match('@')) { + this._address = '@' + address + } else { + this._address = address + } + this._authenticated = props.authenticated + } + + // + // currently, bitmask.js uses address for id, so we return address here too. + // also, we don't know uuid until after authentication. + // + // TODO: change to uuid when possible. + // + get id() { + return this._address + } + + get domain() { + return this._address.split('@')[1] + } + + get address() { + return this._address + } + + get userpart() { + return this._address.split('@')[0] + } + + get authenticated() { + return this._authenticated + } + + // + // returns a promise, fulfill is passed account object + // + login(password) { + return bitmask.user.auth(this.address, password).then( + response => { + if (response.uuid) { + this._uuid = response.uuid + this._authenticated = true + } + return this + } + ) + } + + // + // returns a promise, fulfill is passed account object + // + logout() { + return bitmask.user.logout(this.id).then( + response => { + this._authenticated = false + this._address = '@' + this.domain + return this + } + ) + } + + // + // returns a promise, fullfill is passed account object + // + static active() { + return bitmask.user.active().then( + response => { + if (response.user == '') { + return null + } else { + return new Account(response.user, {authenticated: true}) + } + } + ) + } + +} \ No newline at end of file diff --git a/src/leap/bitmask_js/notes.txt b/src/leap/bitmask_js/notes.txt new file mode 100644 index 00000000..39d60ed2 --- /dev/null +++ b/src/leap/bitmask_js/notes.txt @@ -0,0 +1,173 @@ + + +# BITMASK API + +https://0xacab.org/leap/leap_pycommon/blob/develop/src/leap/common/events/catalog.py + + + + +https://github.com/rofrischmann/react-layout-components/blob/develop/modules/components/Box.jsx +https://github.com/tastejs/todomvc/tree/master/examples + +ES6 + + https://github.com/DrkSephy/es6-cheatsheet + http://egorsmirnov.me/2015/06/14/react-and-es6-part2.html + +React component examples + + https://github.com/Khan/perseus/tree/master/src/components + https://react.parts/web + +Props versus States + + https://github.com/uberVU/react-guide/blob/master/props-vs-state.md + + +Localization + + https://github.com/martinandert/counterpart + http://www.localeplanet.com/ + +Flexbox + + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Using_CSS_flexible_boxes + +Style + + Flat/Low Saturation visual language: https://www.youtube.com/watch?v=jAhjPd4uNFY + + +testing +----------------------------------------- + +https://github.com/airbnb/enzyme + + +webpack +------------------------------------------ + +https://github.com/webpack/docs/wiki/optimization + +https://github.com/rauschma/webpack-es6-demo + +https://github.com/shakacode/react-webpack-rails-tutorial + +https://stackoverflow.com/questions/27639005/how-to-copy-static-files-to-build-directory-with-webpack#27651720 + +react ui components +-------------------------------------------- + +material design + + material-ui + http://www.material-ui.com + materialize + materialize looks to have the best css + https://react-materialize.github.io + http://materializecss.com + mui + https://www.muicss.com + react-material + react-polymer + +elemental + http://elemental-ui.com/ + react-only toolkit + +foundation + used by pixelated user agent + +grommet.io + +topcoat + react-topui + +belle + "configurable react components with great UX" + use belle or bootstrap theme + nikgraf.github.com/belle + +react-islands + react implementation of yandex's islands design + github.com/narqo/react-islands + +Pivotol UI + react ui components styles for the Pivotal brand + http://styleguide.pivotal.io + +react +------------------------ + +react with es6 classes: + +https://facebook.github.io/react/docs/reusable-components.html#es6-classes + + +redux +----------------- + +http://redux.js.org/ +https://github.com/reactjs/redux + Redux is a predictable state container for JavaScript apps. + functional programming + + oop with redux + https://github.com/newtoncodes/react-redux-oop + https://github.com/ddsol/redux-schema + + boilerplates + https://github.com/markerikson/react-redux-links/blob/master/boilerplates-and-starter-kits.md + https://github.com/tsaiDavid/simple-redux-boilerplate + +ec6 +------------------ + + http://frontendjournal.com/javascript-es6-learn-important-features-in-a-few-minutes/ + https://www.infoq.com/news/2015/03/react-es6-classes + https://github.com/google/traceur-compiler/wiki/Getting-Started + http://www.developerdrive.com/2011/08/top-10-must-follow-javascript-best-practices-2/ + + +react templates +--------------------------------- + +https://wix.github.io/react-templates/ + + +Toolchain +------------------- + +processors + traceur + babel + +front-end package management + bower - https://bower.io/ + component + npm + browserify + http://browserify.org/ + and many others + https://github.com/wilmoore/frontend-packagers + +module builder + browserify + webpack + +testing framework + https://github.com/karma-runner/karma + +full frameworks +------------------------ + +http://aurelia.io/ + + +watcherify +---------------------------- + + +#!/bin/bash +watchify client/main.js -o public/bundle.js -dv & +nodemon --exec npm run babel-node -- app.js diff --git a/src/leap/bitmask_js/package.json b/src/leap/bitmask_js/package.json new file mode 100644 index 00000000..128a9e71 --- /dev/null +++ b/src/leap/bitmask_js/package.json @@ -0,0 +1,33 @@ +{ + "name": "bitmask_js", + "version": "0.0.1", + "description": "bitmask user interface in javascript", + "license": "GPL-3.0", + "homepage": "https://bitmask.net", + "repository": "https://leap.se/git/bitmask_client.git", + "dependencies": {}, + "devDependencies": { + "babel": "^6.5.2", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.9.0", + "babel-preset-react": "^6.11.1", + "babel-preset-stage-0": "^6.5.0", + "bootstrap": "^3.3.7", + "copy-webpack-plugin": "^3.0.1", + "css-loader": "^0.23.1", + "less": "^2.7.1", + "less-loader": "^2.2.3", + "react": "^15.2.1", + "react-bootstrap": "^0.30.2", + "react-dom": "^15.2.1", + "style-loader": "^0.13.1", + "webpack": "^1.13.1", + "zxcvbn": "^4.3.0" + }, + "scripts": { + "open": "gnome-open http://localhost:7070", + "watch": "NODE_ENV=development webpack --watch", + "build": "NODE_ENV=development webpack", + "build:production": "NODE_ENV=production webpack" + } +} diff --git a/src/leap/bitmask_js/webpack.config.js b/src/leap/bitmask_js/webpack.config.js new file mode 100644 index 00000000..7b46e1b1 --- /dev/null +++ b/src/leap/bitmask_js/webpack.config.js @@ -0,0 +1,77 @@ +var path = require('path') +var webpack = require('webpack') +var CopyWebpackPlugin = require('copy-webpack-plugin'); + +var config = { + context: path.join(__dirname, 'app'), + entry: './main.js', + output: { + path: path.join(__dirname, 'public'), + filename: 'app.bundle.js' + }, + resolve: { + modulesDirectories: ['node_modules', './app'], + extensions: ['', '.js', '.jsx'] + }, + module: { + loaders: [ + // babel transform + { + test: /\.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + query: { + presets: ['react', 'es2015'] + } + }, + { + test: /\.css$/, + loader: "style!css" + }, + { + test: /\.less$/, + loader: "style!css!less?noIeCompat" + } + ] + }, + plugins: [ + // don't bundle when there is an error: + new webpack.NoErrorsPlugin(), + + // https://webpack.github.io/docs/code-splitting.html + // new webpack.optimize.CommonChunkPlugin('common.js') + + // https://github.com/kevlened/copy-webpack-plugin + new CopyWebpackPlugin([ + { from: 'css/*.css' }, + { from: 'img/*'}, + { from: 'index.html' }, + { from: '../node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'css' }, + { from: '../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2', to: 'fonts' }, + { from: '../node_modules/zxcvbn/dist/zxcvbn.js', to: 'js' } + ]) + ], + stats: { + colors: true + }, + // source-map can be used in production or development + // but it creates a separate file. + devtool: 'source-map' +} + +/* +if (process.env.NODE_ENV == 'production') { + // see https://github.com/webpack/docs/wiki/optimization + config.plugins.push( + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + output: { comments: false } + }), + new webpack.optimize.DedupePlugin() + ) +} else { + config.devtool = 'inline-source-map'; +} +*/ + +module.exports = config -- cgit v1.2.3