summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2016-09-21 15:39:03 -0700
committerKali Kaneko (leap communications) <kali@leap.se>2016-09-22 11:40:14 -0400
commit7569ac8bd58174095f3f897548e26d0ba905236c (patch)
tree839c300d7dff62900bcc91672a3b55cf62c31f6c
parent61b5734c12d6ab6733d3a832df8d99f1041bf355 (diff)
[feat] the setup wizard for the new ui
-rw-r--r--ui/README.md29
-rw-r--r--ui/app/app.js19
-rw-r--r--ui/app/components/center.js6
-rw-r--r--ui/app/components/greeter_panel.js6
-rw-r--r--ui/app/components/layout/index.js41
-rw-r--r--ui/app/components/layout/layout.less25
-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.less29
-rw-r--r--ui/app/components/login.js130
-rw-r--r--ui/app/components/main_panel/email_section.js2
-rw-r--r--ui/app/components/main_panel/index.js2
-rw-r--r--ui/app/components/main_panel/user_section.js10
-rw-r--r--ui/app/components/panel_switcher.js11
-rw-r--r--ui/app/components/wizard/add_provider_modal.js61
-rw-r--r--ui/app/components/wizard/index.js19
-rw-r--r--ui/app/components/wizard/provider_select_stage.js168
-rw-r--r--ui/app/components/wizard/register_stage.js102
-rw-r--r--ui/app/css/common.css20
-rw-r--r--ui/app/main.js2
-rw-r--r--ui/app/models/account.js21
-rw-r--r--ui/app/models/provider.js59
21 files changed, 679 insertions, 148 deletions
diff --git a/ui/README.md b/ui/README.md
index 3f276c0..fb409bf 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 57120a4..45f87dd 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 6fa6212..5e47d0c 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 4552db1..c95b183 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 0000000..8e7c4b5
--- /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 0000000..dae99aa
--- /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 0d557d2..58b6ec4 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 0000000..e2d2e55
--- /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 fe4ef5b..562ab5a 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>&nbsp;</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 a6525d9..a3ff11c 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 3cc6c11..a05f307 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 0b4ba13..acaea23 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 aaf2dc5..2746f00 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 bc5e023..d54ec08 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>&nbsp;</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 613b88f..75e3a1d 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 20674be..19799f8 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 0000000..9afa958
--- /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 acf164e..fcae7fa 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 b162895..8f42765 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 52fea93..726a8b8 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 0000000..2ed2dc8
--- /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)
+ }
+}
+
+
+