diff options
-rw-r--r-- | ui/README.md | 29 | ||||
-rw-r--r-- | ui/app/app.js | 19 | ||||
-rw-r--r-- | ui/app/components/center.js | 6 | ||||
-rw-r--r-- | ui/app/components/greeter_panel.js | 6 | ||||
-rw-r--r-- | ui/app/components/layout/index.js | 41 | ||||
-rw-r--r-- | ui/app/components/layout/layout.less | 25 | ||||
-rw-r--r-- | ui/app/components/list_editor/index.js (renamed from ui/app/components/list_edit.js) | 65 | ||||
-rw-r--r-- | ui/app/components/list_editor/list_editor.less | 29 | ||||
-rw-r--r-- | ui/app/components/login.js | 130 | ||||
-rw-r--r-- | ui/app/components/main_panel/email_section.js | 2 | ||||
-rw-r--r-- | ui/app/components/main_panel/index.js | 2 | ||||
-rw-r--r-- | ui/app/components/main_panel/user_section.js | 10 | ||||
-rw-r--r-- | ui/app/components/panel_switcher.js | 11 | ||||
-rw-r--r-- | ui/app/components/wizard/add_provider_modal.js | 61 | ||||
-rw-r--r-- | ui/app/components/wizard/index.js | 19 | ||||
-rw-r--r-- | ui/app/components/wizard/provider_select_stage.js | 168 | ||||
-rw-r--r-- | ui/app/components/wizard/register_stage.js | 102 | ||||
-rw-r--r-- | ui/app/css/common.css | 20 | ||||
-rw-r--r-- | ui/app/main.js | 2 | ||||
-rw-r--r-- | ui/app/models/account.js | 21 | ||||
-rw-r--r-- | ui/app/models/provider.js | 59 |
21 files changed, 679 insertions, 148 deletions
diff --git a/ui/README.md b/ui/README.md index 3f276c00..fb409bf5 100644 --- a/ui/README.md +++ b/ui/README.md @@ -84,7 +84,10 @@ way. We have enabled these plugins: * babel-presets-stage-0: Allows the use of some ES7 proposals, even though these are not standardized yet. Makes classes nicer. -* babel-polyfill: This is not part of the babel transpiling, but is distributed by babel. This polyfill will give you a full ES2015 environment even if the browser is missing some javascript features. We include this in the 'entry' option of the webpack config. https://babeljs.io/docs/usage/polyfill/ +* babel-polyfill: This is not part of the babel transpiling, but is distributed + by babel. This polyfill will give you a full ES2015 environment even if the + browser is missing some javascript features. We include this in the 'entry' + option of the webpack config. https://babeljs.io/docs/usage/polyfill/ **react** @@ -110,3 +113,27 @@ To integrate Bootstrap with React: 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. +Known Issues +----------------------------------------------------------------- + +Wizard + +* In the wizard, the username field gets deselected. +* User sign up does not work, getting an error from the backend: + No such subcommand: create +* This wizard is kind of ugly + The list of providers should have icons, be sortable, filterable. + The list of providers does not show seeded providers (backend is not returning them) + The provider details should show human readable output, not codes. + +Main window + +* UI doesn't subscribe to events yet, won't get updated if + user has logged out via the command line interface. +* The backend doesn't have a concept of multiple accounts that are + all authenticated yet. +* If you cancel the wizard, it does not select the appropriate account + in the main window. +* Removing accounts doesn't do anything +* Collapsing account list looks weird, and is state is not remembered + diff --git a/ui/app/app.js b/ui/app/app.js index 57120a4e..45f87ddf 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -8,10 +8,18 @@ class Application { // // main entry point for the application // + initialize() { + if (this.debugging()) { + this.show(this.debug_panel) + } else { + this.start() + } + } + start() { Account.active().then(account => { if (account == null) { - this.show('greeter', {onLogin: this.onLogin.bind(this)}) + this.show('greeter') } else { this.show('main', {initialAccount: account}) } @@ -20,13 +28,14 @@ class Application { }) } - onLogin(account) { - this.show('main', {initialAccount: account}) - } - show(panel, properties) { this.switcher.show(panel, properties) } + + debugging() { + this.debug_panel = window.location.hash.replace('#', '') + return this.debug_panel && this.debug_panel != 'main' + } } var App = new Application diff --git a/ui/app/components/center.js b/ui/app/components/center.js index 6fa62128..5e47d0cc 100644 --- a/ui/app/components/center.js +++ b/ui/app/components/center.js @@ -7,7 +7,8 @@ import React from 'react' class Center extends React.Component { static get defaultProps() {return{ - width: null + width: null, + direction: 'both', }} constructor(props) { @@ -19,8 +20,9 @@ class Center extends React.Component { if (this.props.width) { style = {width: this.props.width + 'px'} } + let className = "center-container center-" + this.props.direction return ( - <div className="center-container"> + <div className={className}> <div className="center-item" style={style}> {this.props.children} </div> diff --git a/ui/app/components/greeter_panel.js b/ui/app/components/greeter_panel.js index 4552db18..c95b183a 100644 --- a/ui/app/components/greeter_panel.js +++ b/ui/app/components/greeter_panel.js @@ -16,12 +16,16 @@ export default class GreeterPanel extends React.Component { App.show('wizard') } + onLogin(account) { + App.show('main', {initialAccount: account}) + } + render () { return <div> <Splash speed="slow" mask={false} /> <Center width="400"> <Area position="top" type="light" className="greeter"> - <Login {...this.props} rememberAllowed={false}/> + <Login onLogin={this.onLogin.bind(this)} rememberAllowed={false}/> </Area> <Area position="bottom" type="dark" className="greeter"> <Glyphicon glyph="user" /> diff --git a/ui/app/components/layout/index.js b/ui/app/components/layout/index.js new file mode 100644 index 00000000..8e7c4b58 --- /dev/null +++ b/ui/app/components/layout/index.js @@ -0,0 +1,41 @@ +import React from 'react' + +import './layout.less' + +class HorizontalLayout extends React.Component { + static get defaultProps() {return{ + equalWidths: false + }} + + constructor(props) { + super(props) + } + + render() { + let className = "horizontal-layout" + if (this.props.equalWidths) { + className = className + " equal" + this.props.children.length + } + return ( + <div className={className}> + {this.props.children} + </div> + ) + } +} + +class Column extends React.Component { + constructor(props) { + super(props) + } + + render() { + return ( + <div className="layout-column"> + {this.props.children} + </div> + ) + } +} + +export {HorizontalLayout, Column}
\ No newline at end of file diff --git a/ui/app/components/layout/layout.less b/ui/app/components/layout/layout.less new file mode 100644 index 00000000..dae99aa3 --- /dev/null +++ b/ui/app/components/layout/layout.less @@ -0,0 +1,25 @@ +@gutter: 20px; + +.horizontal-layout { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + > .layout-column { + display: -webkit-flex; + display: flex; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + margin-right: @gutter; + } + > .layout-column:last-child { + margin-right: 0px; + } +} + +.horizontal-layout.equal1 > .layout-column {width: 100%;} +.horizontal-layout.equal2 > .layout-column {width: 50%;} +.horizontal-layout.equal3 > .layout-column {width: 33.33%;} +.horizontal-layout.equal4 > .layout-column {width: 25%;}
\ No newline at end of file diff --git a/ui/app/components/list_edit.js b/ui/app/components/list_editor/index.js index 0d557d22..58b6ec44 100644 --- a/ui/app/components/list_edit.js +++ b/ui/app/components/list_editor/index.js @@ -6,22 +6,7 @@ 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" -} +import './list_editor.less' class ListEdit extends React.Component { @@ -32,35 +17,29 @@ class ListEdit extends React.Component { 'bbbbbbb', 'ccccccc' ], - selected: null, + selected: null, // string of the selected item onRemove: null, onAdd: null, + onSelect: 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 - }) + row(str) { + return this.props.items.indexOf(str) } click(e) { let row = parseInt(e.target.value) if (row >= 0) { - this.setState({selected: row}) + if (this.props.onSelect) { + this.props.onSelect(this.props.items[row]) + } } } @@ -71,13 +50,17 @@ class ListEdit extends React.Component { } 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)}) + if (this.props.onRemove) { + let currentRow = this.row(this.props.selected) + let newSelected = null + if (this.props.items.length == currentRow + 1) { + // if we remove the last item, set the new selected to be + // the new last item. + newSelected = this.props.items[currentRow - 1] + } else { + newSelected = this.props.items[currentRow + 1] } - this.props.onRemove(this.props.items[this.state.selected]) + this.props.onRemove(this.props.selected, newSelected) } } @@ -85,23 +68,23 @@ class ListEdit extends React.Component { let options = null if (this.props.items) { options = this.props.items.map((item, i) => { - return <option style={OPTION_CSS} key={i} value={i}>{item}</option> + return <option className="list-option" key={i} value={i}>{item}</option> }, this) } return( - <div style={CONTAINER_CSS}> + <div className="list-editor"> <FormControl - value={this.state.selected} - style={SELECT_CSS} className="select-list" + value={this.row(this.props.selected)} + className="list-select" componentClass="select" size="5" onChange={this.click}> {options} </FormControl> - <ButtonToolbar className="pull-right" style={TOOLBAR_CSS}> + <ButtonToolbar className="pull-right list-toolbar"> <ButtonGroup> <Button onClick={this.add}> <Glyphicon glyph="plus" /> </Button> - <Button disabled={this.state.selected < 0} onClick={this.remove}> + <Button disabled={this.props.selected < 0} onClick={this.remove}> <Glyphicon glyph="minus" /> </Button> </ButtonGroup> diff --git a/ui/app/components/list_editor/list_editor.less b/ui/app/components/list_editor/list_editor.less new file mode 100644 index 00000000..e2d2e55c --- /dev/null +++ b/ui/app/components/list_editor/list_editor.less @@ -0,0 +1,29 @@ +.list-editor { + + display: -webkit-flex; + display: flex; + + -webkit-flex-direction: column; + flex-direction: column; + + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + + .list-select { + padding: 0px; + flex: 1 1 1000px; + -webkit-flex: 1 1 1000px; + overflow-y: scroll; + } + + .list-option { + padding: 10px; + } + + .list-toolbar { + padding-top: 10px; + + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/login.js b/ui/app/components/login.js index fe4ef5b2..562ab5ac 100644 --- a/ui/app/components/login.js +++ b/ui/app/components/login.js @@ -14,7 +14,9 @@ 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 + address: null, // if set, only allow this username@domain + onLogin: null, // callback + mode: "login" // one of "login" or "signup" }} constructor(props) { @@ -27,14 +29,18 @@ class Login extends React.Component { authError: false, // authentication error message - username: "etest1@riseup.net", + username: this.props.address, usernameState: null, // username validation state usernameError: false, // username help message - password: "whatever", + password: null, passwordState: null, // password validation state passwordError: false, // password help message + password2: null, // password confirmation + password2State: null, // password confirm validation state + password2Error: false, // password confirm help message + disabled: false, remember: false // remember is checked? } @@ -42,9 +48,10 @@ class Login extends React.Component { // 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) + this.onPassword = this.onPassword.bind(this) + this.onPassword2 = this.onPassword2.bind(this) + this.onSubmit = this.onSubmit.bind(this) + this.onRemember = this.onRemember.bind(this) } componentDidMount() { @@ -56,8 +63,14 @@ class Login extends React.Component { let submitButton = "" let usernameHelp = null let passwordHelp = null + let password2Help = null + let password2Elem = null let message = null + let buttonText = "Log In" + /* + * disabled for now + * if (this.props.rememberAllowed) { let props = { style: {marginTop: "0px"}, @@ -74,6 +87,7 @@ class Login extends React.Component { </Checkbox> } } + */ if (this.state.authError) { // style may be: success, warning, danger, info @@ -108,6 +122,25 @@ class Login extends React.Component { //passwordHelp = <HelpBlock> </HelpBlock> } + if (this.props.mode == 'signup') { + buttonText = 'Sign Up' + if (this.state.password2Error) { + + } + password2Elem = ( + <FormGroup controlId="loginPassword2" validationState={this.state.password2State}> + <ControlLabel>Repeat Password</ControlLabel> + <FormControl + type="password" + ref="password" + value={this.state.password2 || ""} + onChange={this.onPassword2} /> + {this.state.password2State == 'success' ? null : <FormControl.Feedback/>} + {password2Help} + </FormGroup> + ) + } + let buttonProps = { type: "button", onClick: this.onSubmit, @@ -116,11 +149,16 @@ class Login extends React.Component { if (this.state.loading) { submitButton = <Button block {...buttonProps}><Spinner /></Button> } else { - submitButton = <Button block {...buttonProps}>Log In</Button> + submitButton = <Button block {...buttonProps}>{buttonText}</Button> } let usernameref = null - if (this.props.domain) { + let usernameDisabled = false + let usernameValue = this.state.username || "" + if (this.props.address) { + usernameDisabled = true + usernameValue = this.props.address + } else if (this.props.domain) { usernameref = function(c) { if (c != null) { let textarea = ReactDOM.findDOMNode(c) @@ -142,7 +180,8 @@ class Login extends React.Component { rows="1" ref={usernameref} autoFocus - value={this.state.username} + value={usernameValue} + disabled={usernameDisabled} onChange={this.onUsernameChange} onBlur={this.onUsernameBlur} /> {this.state.usernameState == 'success' ? null : <FormControl.Feedback/>} @@ -154,12 +193,13 @@ class Login extends React.Component { <FormControl type="password" ref="password" - value={this.state.password} + value={this.state.password || ""} onChange={this.onPassword} /> {this.state.passwordState == 'success' ? null : <FormControl.Feedback/>} {passwordHelp} </FormGroup> + {password2Elem} {submitButton} {rememberCheck} </form> @@ -226,6 +266,12 @@ class Login extends React.Component { } } + onPassword2(e) { + let password2 = e.target.value + this.setState({password2: password2}) + this.validatePassword2(password2, this.state.password) + } + onRemember(e) { let currentValue = e.target.value == 'on' ? true : false let value = !currentValue @@ -258,15 +304,43 @@ class Login extends React.Component { passwordState: state, passwordError: message }) + this.validatePassword2(this.state.password2, password) + } + + validatePassword2(password2, password) { + if (password2) { + if (password != password2) { + this.setState({ + password2State: 'error', + password2Error: "Does not match" + }) + } else { + this.setState({ + password2State: 'success', + password2Error: null + }) + } + } else { + this.setState({ + password2State: null, + password2Error: null + }) + } } maySubmit() { - return( + let ok = ( !this.stateLoading && !this.state.usernameError && - this.state.username != "" && - this.state.password != "" + this.state.username && + this.state.password ) + + if (this.props.mode == 'login') { + return ok + } else if (this.props.mode == 'signup') { + return ok && this.state.password2 == this.state.password + } } onSubmit(e) { @@ -274,6 +348,14 @@ class Login extends React.Component { if (!this.maySubmit()) { return } this.setState({loading: true}) + if (this.props.mode == 'login') { + this.doLogin() + } else if (this.props.mode == 'signup') { + this.doSignup() + } + } + + doLogin() { let account = Account.find(this.state.username) account.login(this.state.password).then( account => { @@ -283,9 +365,27 @@ class Login extends React.Component { } }, error => { - console.log(error) if (error == "") { - error = 'Something failed, but we did not get a message' + error = "Something failed, but we did not get a message" + } + this.setState({ + loading: false, + usernameState: 'error', + passwordState: 'error', + authError: error + }) + } + ) + } + + doSignup() { + Account.create(this.state.username, this.state.password).then( + account => { + this.doLogin() + }, + error => { + if (error == "") { + error = "Something failed, but we did not get a message" } this.setState({ loading: false, diff --git a/ui/app/components/main_panel/email_section.js b/ui/app/components/main_panel/email_section.js index a6525d92..a3ff11c2 100644 --- a/ui/app/components/main_panel/email_section.js +++ b/ui/app/components/main_panel/email_section.js @@ -19,8 +19,6 @@ export default class EmailSection extends React.Component { this.openKeys = this.openKeys.bind(this) this.openApp = this.openApp.bind(this) this.openPrefs = this.openPrefs.bind(this) - - console.log('email constructor') } openKeys() {} diff --git a/ui/app/components/main_panel/index.js b/ui/app/components/main_panel/index.js index 3cc6c11f..a05f3077 100644 --- a/ui/app/components/main_panel/index.js +++ b/ui/app/components/main_panel/index.js @@ -32,9 +32,7 @@ export default class MainPanel extends React.Component { componentWillMount() { if (this.props.initialAccount) { - console.log(Account.list) Account.add(this.props.initialAccount) - Account.add(new DummyAccount(this.props.initialAccount)) this.setState({ account: this.props.initialAccount, accounts: Account.list diff --git a/ui/app/components/main_panel/user_section.js b/ui/app/components/main_panel/user_section.js index 0b4ba136..acaea234 100644 --- a/ui/app/components/main_panel/user_section.js +++ b/ui/app/components/main_panel/user_section.js @@ -61,9 +61,17 @@ export default class UserSection extends React.Component { </SectionLayout> ) } else { + let address = null + if (this.props.account.userpart) { + address = this.props.account.address + } return ( <SectionLayout icon="user" className="wide-margin"> - <Login onLogin={this.props.onLogin} domain={this.props.account.domain} /> + <Login + onLogin={this.props.onLogin} + domain={this.props.account.domain} + address={address} + /> </SectionLayout> ) } diff --git a/ui/app/components/panel_switcher.js b/ui/app/components/panel_switcher.js index aaf2dc5b..2746f005 100644 --- a/ui/app/components/panel_switcher.js +++ b/ui/app/components/panel_switcher.js @@ -17,7 +17,7 @@ export default class PanelSwitcher extends React.Component { this.state = { panel: null, panel_properties: null, - debug: false + debug: true } App.switcher = this } @@ -33,10 +33,11 @@ export default class PanelSwitcher extends React.Component { this.panelRender(this.state.panel, this.state.panel_properties) ) } - if (this.state.debug) { - elems.push( - elem(DebugPanel, {key: 'debug'}) - ) + if (this.state.debug && this.state.panel) { + window.location.hash = this.state.panel + //elems.push( + // elem(DebugPanel, {key: 'debug'}) + //) } return <div id="root">{elems}</div> } diff --git a/ui/app/components/wizard/add_provider_modal.js b/ui/app/components/wizard/add_provider_modal.js index bc5e0236..d54ec085 100644 --- a/ui/app/components/wizard/add_provider_modal.js +++ b/ui/app/components/wizard/add_provider_modal.js @@ -3,12 +3,13 @@ // 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' +import { FormGroup, ControlLabel, FormControl, HelpBlock, Button, ButtonToolbar, Modal } from 'react-bootstrap' -class AddProviderModal extends React.Component { +import Spinner from 'components/spinner' +import Validate from 'lib/validate' +import Provider from 'models/provider' + +export default class AddProviderModal extends React.Component { static get defaultProps() {return{ title: 'Add a provider', @@ -18,20 +19,36 @@ class AddProviderModal extends React.Component { constructor(props) { super(props) this.state = { - validationState: null, + validationState: null, // one of 'success', 'error', 'warning' errorMsg: null, - domain: "" + domain: "", + working: false, // true if waiting for something } this.accept = this.accept.bind(this) this.cancel = this.cancel.bind(this) this.changed = this.changed.bind(this) } - accept() { + accept(e=null) { + if (e) { + e.preventDefault() // don't reload the page please! + } if (this.state.domain) { - App.providers.add(this.state.domain) + this.setState({working: true}) + Provider.setup(this.state.domain).then( + provider => { + this.props.onClose(provider) + // this.setState({working: false}) + }, + error => { + this.setState({ + validationState: 'warning', + errorMsg: error, + working: false + }) + } + ) } - this.props.onClose() } cancel() { @@ -44,9 +61,9 @@ class AddProviderModal extends React.Component { let newMsg = null if (domain.length > 0) { - let error = Validate.domain(domain) - newState = error ? 'error' : 'success' - newMsg = error + let msg = Validate.domain(domain) + newState = msg ? 'error' : 'success' + newMsg = msg } this.setState({ domain: domain, @@ -57,11 +74,21 @@ class AddProviderModal extends React.Component { render() { let help = null + let addButton = null if (this.state.errorMsg) { help = <HelpBlock>{this.state.errorMsg}</HelpBlock> } else { help = <HelpBlock> </HelpBlock> } + if (this.state.working) { + addButton = <Button><Spinner /></Button> + } else if (this.state.validationState == 'warning') { + addButton = <Button onClick={this.accept}>Retry</Button> + } else if (this.state.validationState == 'error') { + addButton = <Button disabled={true}>Add</Button> + } else { + addButton = <Button onClick={this.accept}>Add</Button> + } let form = <form onSubmit={this.accept} autoComplete="off"> <FormGroup controlId="addprovider" validationState={this.state.validationState}> <ControlLabel>Domain</ControlLabel> @@ -72,10 +99,12 @@ class AddProviderModal extends React.Component { value={this.state.domain} onChange={this.changed} onBlur={this.changed} /> - <FormControl.Feedback/> {help} </FormGroup> - <Button onClick={this.accept}>Add</Button> + <ButtonToolbar> + {addButton} + <Button onClick={this.cancel}>Cancel</Button> + </ButtonToolbar> </form> return( @@ -90,5 +119,3 @@ class AddProviderModal extends React.Component { ) } } - -export default AddProviderModal
\ No newline at end of file diff --git a/ui/app/components/wizard/index.js b/ui/app/components/wizard/index.js index 613b88fd..75e3a1d8 100644 --- a/ui/app/components/wizard/index.js +++ b/ui/app/components/wizard/index.js @@ -6,26 +6,27 @@ import React from 'react' import App from 'app' import ProviderSelectStage from './provider_select_stage' +import RegisterStage from './register_stage' import './wizard.less' export default class Wizard extends React.Component { + static get defaultProps() {return{ + stage: "provider" + }} + constructor(props) { super(props) - this.state = { - stage: 'provider' - } - } - - setStage(stage) { - this.setState({stage: stage}) } render() { let stage = null - switch(this.state.stage) { + switch(this.props.stage) { case 'provider': - stage = <ProviderSelectStage /> + stage = <ProviderSelectStage {...this.props}/> + break + case 'register': + stage = <RegisterStage {...this.props}/> break } return( diff --git a/ui/app/components/wizard/provider_select_stage.js b/ui/app/components/wizard/provider_select_stage.js index 20674be1..19799f86 100644 --- a/ui/app/components/wizard/provider_select_stage.js +++ b/ui/app/components/wizard/provider_select_stage.js @@ -2,7 +2,11 @@ import React from 'react' import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' import App from 'app' -import ListEdit from 'components/list_edit' +import Provider from 'models/provider' + +import ListEditor from 'components/list_editor' +import {HorizontalLayout, Column} from 'components/layout' + import StageLayout from './stage_layout' import AddProviderModal from './add_provider_modal' @@ -10,75 +14,163 @@ export default class ProviderSelectStage extends React.Component { static get defaultProps() {return{ title: "Choose a provider", - subtitle: "This doesn't work yet" + initialProvider: null }} constructor(props) { super(props) - let domains = this.currentDomains() this.state = { - domains: domains, - showModal: false + domains: [], // array of domains, as strings + showModal: false, + selected: null, // domain of selected item + provider: null, // Provider object, if selected + error: null // error message } - this.add = this.add.bind(this) - this.remove = this.remove.bind(this) - this.close = this.close.bind(this) - this.previous = this.previous.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.select = this.select.bind(this) + this.close = this.close.bind(this) + this.cancel = this.cancel.bind(this) + this.next = this.next.bind(this) + } + + componentWillMount() { + this.refreshList({ + provider: this.props.initialProvider, + selected: (this.props.initialProvider ? this.props.initialProvider.domain : null) + }) } - currentDomains() { - // return(App.providers.domains().slice() || []) - return ['domain1', 'domain2', 'domain3'] + // + // newState is the state to apply after + // domains are refreshed + // + refreshList(newState=null) { + Provider.list(true).then(domains => { + this.setState(Object.assign({domains: domains}, newState)) + if (domains.length > 0) { + let domain = this.state.selected + if (domains.includes(domain)) { + this.select(domain) + } else { + this.select(domains[0]) + } + } else { + this.select(null) + } + }) } add() { this.setState({showModal: true}) } - remove(provider) { - // App.providers.remove(provider) - this.setState({domains: this.currentDomains()}) + remove(domain, newactive) { + Provider.delete(domain).then( + response => { + this.refreshList({selected: newactive}) + }, + error => { + console.log(error) + } + ) } - 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) - } + select(domain) { this.setState({ - domains: domains, - showModal: false + selected: domain }) + if (domain) { + Provider.get(domain).then( + provider => { + this.setState({ + provider: provider + }) + }, + error => { + this.setState({ + provider: null, + error: error + }) + } + ) + } else { + this.setState({ + provider: null, + error: null + }) + } + } + + close(provider=null) { + if (provider) { + this.refreshList({ + showModal: false, + provider: provider, + selected: provider.domain + }) + } else { + this.setState({ + showModal: false + }) + } } - previous() { + cancel() { App.start() } + next() { + App.show('wizard', { + stage: 'register', + provider: this.state.provider + }) + } + render() { let modal = null + let info = null + if (this.state.provider) { + info = ( + <div> + <h1 className="first">{this.state.provider.name}</h1> + <h3>{this.state.provider.domain}</h3> + <p>{this.state.provider.description}</p> + <p><b>Enrollment Policy:</b> {this.state.provider.enrollment_policy}</p> + <p><b>Services</b>: {this.state.provider.services}</p> + <p><b>Languages</b>: {this.state.provider.languages.join(', ')}</p> + </div> + ) + } else if (this.state.error) { + info = <div>{this.state.error}</div> + } if (this.state.showModal) { modal = <AddProviderModal onClose={this.close} /> } let buttons = ( - <ButtonToolbar className="pull-right"> - <Button onClick={this.previous}> - <Glyphicon glyph="chevron-left" /> - Previous - </Button> - <Button> - Next - <Glyphicon glyph="chevron-right" /> - </Button> - </ButtonToolbar> + <div> + <ButtonToolbar className="pull-left"> + <Button onClick={this.cancel}> + Cancel + </Button> + </ButtonToolbar> + <ButtonToolbar className="pull-right"> + <Button onClick={this.next}> + Next + <Glyphicon glyph="chevron-right" /> + </Button> + </ButtonToolbar> + </div> ) - let select = <ListEdit ref="list" items={this.state.domains} - onRemove={this.remove} onAdd={this.add} /> + let editlist = <ListEditor ref="list" items={this.state.domains} + selected={this.state.selected} onRemove={this.remove} onAdd={this.add} + onSelect={this.select} /> return( <StageLayout title={this.props.title} subtitle={this.props.subtitle} buttons={buttons}> - {select} + <HorizontalLayout equalWidths={true}> + <Column>{editlist}</Column> + <Column>{info}</Column> + </HorizontalLayout> {modal} </StageLayout> ) diff --git a/ui/app/components/wizard/register_stage.js b/ui/app/components/wizard/register_stage.js new file mode 100644 index 00000000..9afa9587 --- /dev/null +++ b/ui/app/components/wizard/register_stage.js @@ -0,0 +1,102 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, + Glyphicon, Tabs, Tab} from 'react-bootstrap' + +import App from 'app' +import Provider from 'models/provider' +import Login from 'components/login' +import Center from 'components/center' + +import StageLayout from './stage_layout' + +export default class RegisterStage extends React.Component { + + static get defaultProps() {return{ + provider: null + }} + + constructor(props) { + super(props) + this.state = { + activeTab: 'signup', // either 'login' or 'signup' + error: null // error message + } + // this.add = this.add.bind(this) + // this.remove = this.remove.bind(this) + // this.select = this.select.bind(this) + this.selectTab = this.selectTab.bind(this) + this.previous = this.previous.bind(this) + this.cancel = this.cancel.bind(this) + this.login = this.login.bind(this) + } + + previous() { + App.show('wizard', { + stage: 'provider', + initialProvider: this.props.provider + }) + } + + cancel() { + App.start() + } + + login(account) { + App.show('main', {initialAccount: account}) + } + + selectTab(key) { + this.setState({ + activeTab: key + }) + } + + render() { + let info = null + if (this.props.provider) { + info = ( + <div> + <h1 className="first">{this.props.provider.name}</h1> + <h3>{this.props.provider.domain}</h3> + <p>{this.props.provider.description}</p> + <p><b>Enrollment Policy:</b> {this.props.provider.enrollment_policy}</p> + <p><b>Services</b>: {this.props.provider.services}</p> + <p><b>Languages</b>: {this.props.provider.languages.join(', ')}</p> + </div> + ) + } + let buttons = ( + <div> + <ButtonToolbar className="pull-left"> + <Button onClick={this.cancel}> + Cancel + </Button> + </ButtonToolbar> + <ButtonToolbar className="pull-right"> + <Button onClick={this.previous}> + <Glyphicon glyph="chevron-left" /> + Previous + </Button> + </ButtonToolbar> + </div> + ) + return( + <StageLayout title={this.props.provider.domain} buttons={buttons}> + <Tabs activeKey={this.state.activeTab} onSelect={this.selectTab} animation={false} id="login-tabs"> + <Tab eventKey="signup" title="Sign up"> + <div className="vspacer" /> + <Center direction="horizontal" width={400}> + <Login mode="signup" domain={this.props.provider.domain} onLogin={this.login} /> + </Center> + </Tab> + <Tab eventKey="login" title="Log In"> + <div className="vspacer" /> + <Center direction="horizontal" width={400}> + <Login domain={this.props.provider.domain} onLogin={this.login} /> + </Center> + </Tab> + </Tabs> + </StageLayout> + ) + } +} diff --git a/ui/app/css/common.css b/ui/app/css/common.css index acf164ee..fcae7faa 100644 --- a/ui/app/css/common.css +++ b/ui/app/css/common.css @@ -63,12 +63,21 @@ body { */ .center-container { - position: absolute; + display: -webkit-flex; + display: flex; -webkit-flex-flow: row nowrap; + flex-flow: row nowrap; -webkit-justify-content: center; + justify-content: center; -webkit-align-content: center; + align-content: center; -webkit-align-items: center; + align-items: center; +} + +.center-container.center-both { + position: absolute; top: 0px; left: 0px; height: 100%; @@ -77,4 +86,13 @@ body { .center-container .center-item { -webkit-flex: 0 1 auto; + flex: 0 1 auto; } + +/* + * typography + */ + +h1.first { margin-top: 0px } + +.vspacer {margin-top: 20px }
\ No newline at end of file diff --git a/ui/app/main.js b/ui/app/main.js index b1628953..8f427659 100644 --- a/ui/app/main.js +++ b/ui/app/main.js @@ -16,7 +16,7 @@ class Main extends React.Component { } componentDidMount() { - App.start() + App.initialize() } } diff --git a/ui/app/models/account.js b/ui/app/models/account.js index 52fea93d..726a8b8c 100644 --- a/ui/app/models/account.js +++ b/ui/app/models/account.js @@ -57,6 +57,11 @@ export default class Account { return bitmask.bonafide.user.auth(this.address, password).then( response => { if (response.uuid) { + // currently, only one account can be authenticated at once + Account.list.forEach(account => { + account._authenticated = false + // if(account.id != this.id) {account.logout()} + }) this._uuid = response.uuid this._authenticated = true } @@ -131,13 +136,15 @@ export default class Account { return i.id != account.id }) } - // return Account.list - // return new Promise(function(resolve, reject) { - // window.setTimeout(function() { - // resolve(['@blah', '@lala']) - // }, 1000) - // }) - // } + + static create(address, password) { + return bitmask.bonafide.user.create(address, password).then( + response => { + console.log(response) + return new Account(address) + } + ) + } } Account.list = [] diff --git a/ui/app/models/provider.js b/ui/app/models/provider.js new file mode 100644 index 00000000..2ed2dc8c --- /dev/null +++ b/ui/app/models/provider.js @@ -0,0 +1,59 @@ +import bitmask from 'lib/bitmask' + +var LOCALE = 'en' + +export default class Provider { + + constructor(props) { + this._name = props.name + this._description = props.description + let k = null + for (k in props) { + if (k != 'description' && k != 'name') { + this[k] = props[k] + } + } + } + + get name() { + return this._name[LOCALE] + } + + get description() { + return this._description[LOCALE] + } + + static setup(domain) { + return bitmask.bonafide.provider.create(domain).then( + response => { + console.log("Provider configured: " + response.domain) + return new Provider(response) + } + ) + } + + static get(domain) { + return bitmask.bonafide.provider.read(domain).then( + response => { + return new Provider(response) + } + ) + } + + static list(seeded=false) { + return bitmask.bonafide.provider.list(seeded).then( + response => { + return response.map( + i => { return i['domain'] } + ) + } + ) + } + + static delete(domain) { + return bitmask.bonafide.provider.delete(domain) + } +} + + + |