Vault Management UI (first round) (#4446)

* Add RPCs for parity_vault (create, open, list, etc.)

* WIP

* WIP

* WIP

* WIP (create should create)

* Create & close working

* WIP

* WIP

* WIP

* Open & Close now working

* WIP

* WIP

* Merge relevant changes from js-home

* Hover actions

* WIP (start of account assignment)

* Open, Close & Account assignment works

* Fix margins

* UI updates

* Update tests

* Add the parity_{get|set}VaultMeta calls

* Handle metadata

* Adjust padding in Open/Close modals

* moveAccounts take both in and out

* Adjust padding

* Fix stretch

* Optimize hover stretch

* pre-merge

* Cleanup variable naming (duplication)

* Rename Vault{Close,Open} -> Vault{Lock,Unlock}

* clearVaultFields uses setters

* TODO for small Portal sizes

* Vaults rendering tests

* .only

* libusb compile

* VaultCard rendering tests

* Update message keys (rename gone rouge)

* Display passwordHint op vault unlock

* Update failing tests

* Manually dispatch allAccountsInfo when move completed

* Open/Close always shows vault image in colour

* Password submit submits modal (PR comment)

* Add link to account
This commit is contained in:
Jaco Greeff 2017-02-20 16:40:01 +01:00 committed by Gav Wood
parent ac6180a6fe
commit 9e210e5eda
52 changed files with 3722 additions and 192 deletions

View File

@ -119,7 +119,6 @@ export default class Personal {
case 'parity_removeAddress':
case 'parity_setAccountName':
case 'parity_setAccountMeta':
case 'parity_changeVault':
this._accountsInfo();
return;

View File

@ -19,23 +19,24 @@
line-height: 1.618em;
}
.password {
flex: 0 1 50%;
width: 50%;
box-sizing: border-box;
&:nth-child(odd) {
padding-right: 0.25rem;
}
&:nth-child(even) {
padding-left: 0.25rem;
}
}
/* TODO: 2 column layout can be made generic, now duplicated in Vaults */
.passwords {
display: flex;
flex-wrap: wrap;
.password {
box-sizing: border-box;
flex: 0 1 50%;
width: 50%;
&:nth-child(odd) {
padding-right: 0.25rem;
}
&:nth-child(even) {
padding-left: 0.25rem;
}
}
}
.identities, .selector {

View File

@ -18,37 +18,44 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
export default {
duplicateName: (
<FormattedMessage
id='errors.duplicateName'
defaultMessage='the name already exists'
/>
),
noFile: (
<FormattedMessage
id='createAccount.error.noFile'
id='errors.noFile'
defaultMessage='select a valid wallet file to import'
/>
),
noKey: (
<FormattedMessage
id='createAccount.error.noKey'
id='errors.noKey'
defaultMessage='you need to provide the raw private key'
/>
),
noMatchPassword: (
<FormattedMessage
id='createAccount.error.noMatchPassword'
id='errors.noMatchPassword'
defaultMessage='the supplied passwords does not match'
/>
),
noName: (
<FormattedMessage
id='createAccount.error.noName'
defaultMessage='you need to specify a valid name for the account'
id='errors.noName'
defaultMessage='you need to specify a valid name'
/>
),
invalidKey: (
<FormattedMessage
id='createAccount.error.invalidKey'
id='errors.invalidKey'
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"'
/>
)

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaultAccounts';

View File

@ -0,0 +1,48 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
/* TODO: These overlap with DappPermissions now, make DRY */
/* (selection component or just styles?) */
.iconDisabled {
opacity: 0.15;
}
.item {
display: flex;
flex: 1;
position: relative;
.overlay {
position: absolute;
right: 0.5em;
top: 0.5em;
}
}
.selected,
.unselected {
margin-bottom: 0.25em;
width: 100%;
&:focus {
outline: none;
}
}
.selected {
background: rgba(255, 255, 255, 0.15) !important;
}

View File

@ -0,0 +1,195 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { personalAccountsInfo } from '~/redux/providers/personalActions';
import { AccountCard, Button, Portal, SectionList } from '~/ui';
import { CancelIcon, CheckIcon } from '~/ui/Icons';
import styles from './vaultAccounts.css';
@observer
class VaultAccounts extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
personalAccountsInfo: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired
};
render () {
const { accounts } = this.props;
const { isBusyAccounts, isModalAccountsOpen, selectedAccounts } = this.props.vaultStore;
if (!isModalAccountsOpen) {
return null;
}
const vaultAccounts = Object
.keys(accounts)
.filter((address) => accounts[address].uuid)
.map((address) => accounts[address]);
return (
<Portal
buttons={ [
<Button
disabled={ isBusyAccounts }
icon={ <CancelIcon /> }
key='cancel'
label={
<FormattedMessage
id='vaults.accounts.button.cancel'
defaultMessage='Cancel'
/>
}
onClick={ this.onClose }
/>,
<Button
disabled={ isBusyAccounts }
icon={ <CheckIcon /> }
key='execute'
label={
<FormattedMessage
id='vaults.accounts.button.execute'
defaultMessage='Set'
/>
}
onClick={ this.onExecute }
/>
] }
busy={ isBusyAccounts }
onClose={ this.onClose }
open
title={
<FormattedMessage
id='vaults.accounts.title'
defaultMessage='Manage Vault Accounts'
/>
}
>
<SectionList
items={ vaultAccounts }
noStretch
renderItem={ this.renderAccount }
selectedAccounts={ selectedAccounts }
/>
</Portal>
);
}
// TODO: There are a lot of similarities between the dapp permissions selector
// (although that has defaults) and this one. A genrerix multi-select component
// would be applicable going forward. (Originals passed in, new selections back)
renderAccount = (account) => {
const { vaultName, selectedAccounts } = this.props.vaultStore;
const isInVault = account.meta.vault === vaultName;
const isSelected = isInVault
? !selectedAccounts[account.address]
: selectedAccounts[account.address];
const onSelect = () => {
this.props.vaultStore.toggleSelectedAccount(account.address);
};
return (
<div className={ styles.item }>
<AccountCard
account={ account }
className={
isSelected
? styles.selected
: styles.unselected
}
onClick={ onSelect }
/>
<div className={ styles.overlay }>
{
isSelected
? <CheckIcon onClick={ onSelect } />
: <CheckIcon className={ styles.iconDisabled } onClick={ onSelect } />
}
</div>
</div>
);
}
onClose = () => {
this.props.vaultStore.closeAccountsModal();
}
onExecute = () => {
const { api } = this.context;
const { accounts, personalAccountsInfo, vaultStore } = this.props;
const { vaultName, selectedAccounts } = this.props.vaultStore;
const vaultAccounts = Object
.keys(accounts)
.filter((address) => accounts[address].uuid && selectedAccounts[address])
.map((address) => accounts[address]);
return vaultStore
.moveAccounts(
vaultName,
vaultAccounts
.filter((account) => account.meta.vault !== vaultName)
.map((account) => account.address),
vaultAccounts
.filter((account) => account.meta.vault === vaultName)
.map((account) => account.address)
)
.catch(this.props.newError)
.then(() => {
// TODO: We manually call parity_allAccountsInfo after all the promises
// have been resolved. If bulk moves do become available in the future,
// subscriptions can transparently take care of this instead of calling
// and manually dispatching an update. (Using subscriptions currently
// means allAccountsInfo is called after each and every move call)
return api.parity
.allAccountsInfo()
.then(personalAccountsInfo);
})
.then(this.onClose);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return { accounts };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError,
personalAccountsInfo
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(VaultAccounts);

View File

@ -0,0 +1,179 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import VaultAccounts from './';
const ACCOUNT_A = '0x1234567890123456789012345678901234567890';
const ACCOUNT_B = '0x0123456789012345678901234567890123456789';
const ACCOUNT_C = '0x9012345678901234567890123456789012345678';
const ACCOUNT_D = '0x8901234567890123456789012345678901234567';
const VAULTNAME = 'testVault';
const ACCOUNTS = {
[ACCOUNT_A]: {
address: ACCOUNT_A,
uuid: null
},
[ACCOUNT_B]: {
address: ACCOUNT_B,
uuid: ACCOUNT_B,
meta: {
vault: 'somethingElse'
}
},
[ACCOUNT_C]: {
address: ACCOUNT_C,
uuid: ACCOUNT_C,
meta: {
vault: VAULTNAME
}
},
[ACCOUNT_D]: {
address: ACCOUNT_D,
uuid: ACCOUNT_D,
meta: {
vault: VAULTNAME
}
}
};
let api;
let component;
let instance;
let reduxStore;
let vaultStore;
function createApi () {
api = {
parity: {
allAccountsInfo: sinon.stub().resolves({})
}
};
return api;
}
function createReduxStore () {
reduxStore = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: ACCOUNTS
}
};
}
};
return reduxStore;
}
function createVaultStore () {
vaultStore = {
isBusyAccounts: false,
isModalAccountsOpen: true,
selectedAccounts: { [ACCOUNT_B]: true, [ACCOUNT_C]: true },
vaultName: VAULTNAME,
closeAccountsModal: sinon.stub(),
moveAccounts: sinon.stub().resolves(true),
toggleSelectedAccount: sinon.stub()
};
return vaultStore;
}
function render () {
component = shallow(
<VaultAccounts vaultStore={ createVaultStore() } />,
{
context: {
store: createReduxStore()
}
}
).find('VaultAccounts').shallow({
context: {
api: createApi()
}
});
instance = component.instance();
return component;
}
describe('modals/VaultAccounts', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('SectionList', () => {
let sectionList;
beforeEach(() => {
sectionList = component.find('SectionList');
});
it('has the filtered accounts', () => {
expect(sectionList.props().items).to.deep.equal([
ACCOUNTS[ACCOUNT_B], ACCOUNTS[ACCOUNT_C], ACCOUNTS[ACCOUNT_D]
]);
});
it('renders via renderAccount', () => {
expect(sectionList.props().renderItem).to.equal(instance.renderAccount);
});
});
});
describe('event handlers', () => {
describe('onClose', () => {
beforeEach(() => {
instance.onClose();
});
it('calls into closeAccountsModal', () => {
expect(vaultStore.closeAccountsModal).to.have.been.called;
});
});
describe('onExecute', () => {
beforeEach(() => {
sinon.spy(instance, 'onClose');
return instance.onExecute();
});
afterEach(() => {
instance.onClose.restore();
});
it('calls into moveAccounts', () => {
expect(vaultStore.moveAccounts).to.have.been.calledWith(VAULTNAME, [ACCOUNT_B], [ACCOUNT_C]);
});
it('closes modal', () => {
expect(instance.onClose).to.have.been.called;
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaultCreate';

View File

@ -0,0 +1,38 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.body {
/* TODO: These styles are shared with CreateAccount - DRY up */
.passwords {
display: flex;
flex-wrap: wrap;
.password {
box-sizing: border-box;
flex: 0 1 50%;
width: 50%;
&:nth-child(odd) {
padding-right: 0.25rem;
}
&:nth-child(even) {
padding-left: 0.25rem;
}
}
}
}

View File

@ -0,0 +1,227 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { Button, Input, Portal } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import { CheckIcon, CloseIcon } from '~/ui/Icons';
import styles from './vaultCreate.css';
@observer
class VaultCreate extends Component {
static propTypes = {
newError: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired
}
render () {
const { isBusyCreate, isModalCreateOpen, vaultDescription, vaultName, vaultNameError, vaultPassword, vaultPasswordHint, vaultPasswordRepeat, vaultPasswordRepeatError } = this.props.vaultStore;
const hasError = !!vaultNameError || !!vaultPasswordRepeatError;
if (!isModalCreateOpen) {
return null;
}
return (
<Portal
busy={ isBusyCreate }
buttons={ [
<Button
disabled={ isBusyCreate }
icon={ <CloseIcon /> }
key='close'
label={
<FormattedMessage
id='vaults.create.button.close'
defaultMessage='close'
/>
}
onClick={ this.onClose }
/>,
<Button
disabled={ hasError || isBusyCreate }
icon={ <CheckIcon /> }
key='vault'
label={
<FormattedMessage
id='vaults.create.button.vault'
defaultMessage='create vault'
/>
}
onClick={ this.onCreate }
/>
] }
onClose={ this.onClose }
open
title={
<FormattedMessage
id='vaults.create.title'
defaultMessage='Create a new vault'
/>
}
>
<div className={ styles.body }>
<Input
error={ vaultNameError }
hint={
<FormattedMessage
id='vaults.create.name.hint'
defaultMessage='a name for the vault'
/>
}
label={
<FormattedMessage
id='vaults.create.name.label'
defaultMessage='vault name'
/>
}
onChange={ this.onEditName }
value={ vaultName }
/>
<Input
hint={
<FormattedMessage
id='vaults.create.description.hint'
defaultMessage='an extended description for the vault'
/>
}
label={
<FormattedMessage
id='vaults.create.descriptions.label'
defaultMessage='(optional) description'
/>
}
onChange={ this.onEditDescription }
value={ vaultDescription }
/>
<Input
hint={
<FormattedMessage
id='vaults.create.hint.hint'
defaultMessage='(optional) a hint to help with remembering the password'
/>
}
label={
<FormattedMessage
id='vaults.create.hint.label'
defaultMessage='password hint'
/>
}
onChange={ this.onEditPasswordHint }
value={ vaultPasswordHint }
/>
<div className={ styles.passwords }>
<div className={ styles.password }>
<Input
hint={
<FormattedMessage
id='vaults.create.password.hint'
defaultMessage='a strong, unique password'
/>
}
label={
<FormattedMessage
id='vaults.create.password.label'
defaultMessage='password'
/>
}
onChange={ this.onEditPassword }
type='password'
value={ vaultPassword }
/>
</div>
<div className={ styles.password }>
<Input
error={ vaultPasswordRepeatError }
hint={
<FormattedMessage
id='vaults.create.password2.hint'
defaultMessage='verify your password'
/>
}
label={
<FormattedMessage
id='vaults.create.password2.label'
defaultMessage='password (repeat)'
/>
}
onChange={ this.onEditPasswordRepeat }
type='password'
value={ vaultPasswordRepeat }
/>
</div>
</div>
<PasswordStrength input={ vaultPassword } />
</div>
</Portal>
);
}
onEditDescription = (event, description) => {
this.props.vaultStore.setVaultDescription(description);
}
onEditName = (event, name) => {
this.props.vaultStore.setVaultName(name);
}
onEditPassword = (event, password) => {
this.props.vaultStore.setVaultPassword(password);
}
onEditPasswordHint = (event, hint) => {
this.props.vaultStore.setVaultPasswordHint(hint);
}
onEditPasswordRepeat = (event, password) => {
this.props.vaultStore.setVaultPasswordRepeat(password);
}
onCreate = () => {
const { vaultNameError, vaultPasswordRepeatError } = this.props.vaultStore;
if (vaultNameError || vaultPasswordRepeatError) {
return;
}
return this.props.vaultStore
.createVault()
.catch(this.props.newError)
.then(this.onClose);
}
onClose = () => {
this.props.vaultStore.closeCreateModal();
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(VaultCreate);

View File

@ -0,0 +1,162 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import VaultCreate from './';
let component;
let instance;
let reduxStore;
let vaultStore;
function vaultReduxStore () {
reduxStore = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: sinon.stub()
};
return reduxStore;
}
function vaultVaultStore () {
vaultStore = {
isBusyCreate: false,
isModalCreateOpen: true,
vaultDescription: 'initialDesc',
vaultName: 'initialName',
vaultPassword: 'initialPassword',
vaultPasswordRepeat: 'initialPassword',
vaultPasswordHint: 'initialHint',
closeCreateModal: sinon.stub(),
createVault: sinon.stub().resolves(true),
setVaultDescription: sinon.stub(),
setVaultName: sinon.stub(),
setVaultPassword: sinon.stub(),
setVaultPasswordHint: sinon.stub(),
setVaultPasswordRepeat: sinon.stub()
};
return vaultStore;
}
function render () {
component = shallow(
<VaultCreate vaultStore={ vaultVaultStore() } />,
{
context: {
store: vaultReduxStore()
}
}
).find('VaultCreate').shallow();
instance = component.instance();
return component;
}
describe('modals/VaultCreate', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('event handlers', () => {
describe('onClose', () => {
beforeEach(() => {
instance.onClose();
});
it('calls into closeCreateModal', () => {
expect(vaultStore.closeCreateModal).to.have.been.called;
});
});
describe('onCreate', () => {
beforeEach(() => {
sinon.spy(instance, 'onClose');
return instance.onCreate();
});
afterEach(() => {
instance.onClose.restore();
});
it('calls into createVault', () => {
expect(vaultStore.createVault).to.have.been.called;
});
it('closes modal', () => {
expect(instance.onClose).to.have.been.called;
});
});
describe('onEditDescription', () => {
beforeEach(() => {
instance.onEditDescription(null, 'testDescription');
});
it('calls setVaultDescription', () => {
expect(vaultStore.setVaultDescription).to.have.been.calledWith('testDescription');
});
});
describe('onEditName', () => {
beforeEach(() => {
instance.onEditName(null, 'testName');
});
it('calls setVaultName', () => {
expect(vaultStore.setVaultName).to.have.been.calledWith('testName');
});
});
describe('onEditPassword', () => {
beforeEach(() => {
instance.onEditPassword(null, 'testPassword');
});
it('calls setVaultPassword', () => {
expect(vaultStore.setVaultPassword).to.have.been.calledWith('testPassword');
});
});
describe('onEditPasswordHint', () => {
beforeEach(() => {
instance.onEditPasswordHint(null, 'testPasswordHint');
});
it('calls setVaultPasswordHint', () => {
expect(vaultStore.setVaultPasswordHint).to.have.been.calledWith('testPasswordHint');
});
});
describe('onEditPasswordRepeat', () => {
beforeEach(() => {
instance.onEditPasswordRepeat(null, 'testPassword');
});
it('calls setVaultPasswordRepeat', () => {
expect(vaultStore.setVaultPasswordRepeat).to.have.been.calledWith('testPassword');
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaultLock';

View File

@ -0,0 +1,92 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { ConfirmDialog, VaultCard } from '~/ui';
import styles from '../VaultUnlock/vaultUnlock.css';
@observer
class VaultLock extends Component {
static propTypes = {
newError: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired
}
render () {
const { isBusyLock, isModalLockOpen, vault } = this.props.vaultStore;
if (!isModalLockOpen) {
return null;
}
return (
<ConfirmDialog
busy={ isBusyLock }
disabledConfirm={ isBusyLock }
disabledDeny={ isBusyLock }
onConfirm={ this.onExecute }
onDeny={ this.onClose }
open
title={
<FormattedMessage
id='vaults.confirmClose.title'
defaultMessage='Close Vault'
/>
}
>
<div className={ styles.textbox }>
<FormattedMessage
id='vaults.confirmClose.info'
defaultMessage="You are about to close a vault. Any accounts associated with the vault won't be visible after this operation concludes. To view the associated accounts, open the vault again."
/>
</div>
<VaultCard.Layout
withBorder
vault={ vault }
/>
</ConfirmDialog>
);
}
onExecute = () => {
return this.props.vaultStore
.closeVault()
.catch(this.props.newError)
.then(this.onClose);
}
onClose = () => {
this.props.vaultStore.closeLockModal();
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(VaultLock);

View File

@ -0,0 +1,131 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import VaultLock from './';
const VAULT = {
name: 'testVault'
};
let component;
let instance;
let reduxStore;
let vaultStore;
function createReduxStore () {
reduxStore = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
return reduxStore;
}
function createVaultStore () {
vaultStore = {
isBusyLock: false,
isModalLockOpen: true,
vault: VAULT,
vaultName: VAULT.name,
vaults: [VAULT],
closeLockModal: sinon.stub(),
closeVault: sinon.stub().resolves(true)
};
return vaultStore;
}
function render () {
component = shallow(
<VaultLock vaultStore={ createVaultStore() } />,
{
context: {
store: createReduxStore()
}
}
).find('VaultLock').shallow();
instance = component.instance();
return component;
}
describe('modals/VaultLock', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('ConfirmDialog', () => {
let dialog;
beforeEach(() => {
dialog = component.find('ConfirmDialog');
});
it('renders the dialog', () => {
expect(dialog.get(0)).to.be.ok;
});
it('passes onConfirm as onExecute', () => {
expect(dialog.props().onConfirm).to.equal(instance.onExecute);
});
it('passes onDeny as onClose', () => {
expect(dialog.props().onDeny).to.equal(instance.onClose);
});
});
describe('event methods', () => {
describe('onExecute', () => {
beforeEach(() => {
sinon.stub(instance, 'onClose');
return instance.onExecute();
});
afterEach(() => {
instance.onClose.restore();
});
it('closes the modal', () => {
expect(instance.onClose).to.have.been.called;
});
it('calls into vaultStore.closeVault', () => {
expect(vaultStore.closeVault).to.have.been.called;
});
});
describe('onClose', () => {
beforeEach(() => {
instance.onClose();
});
it('calls into closeLockModal', () => {
expect(vaultStore.closeLockModal).to.have.been.called;
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaultUnlock';

View File

@ -0,0 +1,27 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.passwordHint {
color: rgba(255, 255, 255, 0.5);
font-size: 0.75em;
text-align: left;
}
.textbox {
line-height: 1.5em;
margin-bottom: 1.5em;
}

View File

@ -0,0 +1,118 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { ConfirmDialog, Input, VaultCard } from '~/ui';
import styles from './vaultUnlock.css';
@observer
class VaultUnlock extends Component {
static propTypes = {
newError: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired
}
render () {
const { isBusyUnlock, isModalUnlockOpen, vault, vaultPassword } = this.props.vaultStore;
if (!isModalUnlockOpen) {
return null;
}
return (
<ConfirmDialog
busy={ isBusyUnlock }
disabledConfirm={ isBusyUnlock }
disabledDeny={ isBusyUnlock }
onConfirm={ this.onExecute }
onDeny={ this.onClose }
open
title={
<FormattedMessage
id='vaults.confirmOpen.title'
defaultMessage='Open Vault'
/>
}
>
<div className={ styles.textbox }>
<FormattedMessage
id='vaults.confirmOpen.info'
defaultMessage='You are about to open a vault. After confirming your password, all accounts associated with this vault will be visible. Closing the vault will remove the accounts from view until the vault is opened again.'
/>
</div>
<VaultCard.Layout
withBorder
vault={ vault }
/>
<Input
hint={
<FormattedMessage
id='vaults.confirmOpen.password.hint'
defaultMessage='the password specified when creating the vault'
/>
}
label={
<FormattedMessage
id='vaults.confirmOpen.password.label'
defaultMessage='vault password'
/>
}
onChange={ this.onEditPassword }
onSubmit={ this.onExecute }
type='password'
value={ vaultPassword }
/>
<div className={ styles.passwordHint }>
{ vault.meta.passwordHint }
</div>
<br />
</ConfirmDialog>
);
}
onEditPassword = (event, password) => {
this.props.vaultStore.setVaultPassword(password);
}
onClose = () => {
this.props.vaultStore.closeUnlockModal();
}
onExecute = () => {
return this.props.vaultStore
.openVault()
.catch(this.props.newError)
.then(this.onClose);
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(VaultUnlock);

View File

@ -0,0 +1,146 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import VaultUnlock from './';
const VAULT = {
name: 'testVault',
meta: {
passwordHint: 'some hint'
}
};
let component;
let instance;
let reduxStore;
let vaultStore;
function createReduxStore () {
reduxStore = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
return reduxStore;
}
function createVaultStore () {
vaultStore = {
isBusyUnlock: false,
isModalUnlockOpen: true,
vault: VAULT,
vaultName: VAULT.name,
vaultPassword: 'testPassword',
vaults: [VAULT],
closeUnlockModal: sinon.stub(),
openVault: sinon.stub().resolves(true),
setVaultPassword: sinon.stub()
};
return vaultStore;
}
function render () {
component = shallow(
<VaultUnlock vaultStore={ createVaultStore() } />,
{
context: {
store: createReduxStore()
}
}
).find('VaultUnlock').shallow();
instance = component.instance();
return component;
}
describe('modals/VaultUnlock', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('ConfirmDialog', () => {
let dialog;
beforeEach(() => {
dialog = component.find('ConfirmDialog');
});
it('renders the dialog', () => {
expect(dialog.get(0)).to.be.ok;
});
it('passes onConfirm as onExecute', () => {
expect(dialog.props().onConfirm).to.equal(instance.onExecute);
});
it('passes onDeny as onClose', () => {
expect(dialog.props().onDeny).to.equal(instance.onClose);
});
});
describe('event methods', () => {
describe('onExecute', () => {
beforeEach(() => {
sinon.stub(instance, 'onClose');
return instance.onExecute();
});
afterEach(() => {
instance.onClose.restore();
});
it('closes the modal', () => {
expect(instance.onClose).to.have.been.called;
});
it('calls into vaultStore.openVault', () => {
expect(vaultStore.openVault).to.have.been.called;
});
});
describe('onClose', () => {
beforeEach(() => {
instance.onClose();
});
it('calls into closeUnlockModal', () => {
expect(vaultStore.closeUnlockModal).to.have.been.called;
});
});
describe('onEditPassword', () => {
beforeEach(() => {
instance.onEditPassword(null, 'someVaultPassword');
});
it('calls into vaultStore.setVaultPassword', () => {
expect(vaultStore.setVaultPassword).to.have.been.calledWith('someVaultPassword');
});
});
});
});

View File

@ -14,44 +14,26 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DappPermissions from './DappPermissions';
import DappsVisible from './AddDapps';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
import ExecuteContract from './ExecuteContract';
import FirstRun from './FirstRun';
import LoadContract from './LoadContract';
import SaveContract from './SaveContract';
import Shapeshift from './Shapeshift';
import Verification from './Verification';
import Transfer from './Transfer';
import PasswordManager from './PasswordManager';
import UpgradeParity from './UpgradeParity';
import WalletSettings from './WalletSettings';
export {
AddAddress,
AddContract,
CreateAccount,
CreateWallet,
DappPermissions,
DappsVisible,
DeleteAccount,
DeployContract,
EditMeta,
ExecuteContract,
FirstRun,
LoadContract,
SaveContract,
Shapeshift,
Verification,
Transfer,
PasswordManager,
UpgradeParity,
WalletSettings
};
export AddAddress from './AddAddress';
export AddContract from './AddContract';
export CreateAccount from './CreateAccount';
export CreateWallet from './CreateWallet';
export DappPermissions from './DappPermissions';
export DappsVisible from './AddDapps';
export DeleteAccount from './DeleteAccount';
export DeployContract from './DeployContract';
export EditMeta from './EditMeta';
export ExecuteContract from './ExecuteContract';
export FirstRun from './FirstRun';
export LoadContract from './LoadContract';
export PasswordManager from './PasswordManager';
export SaveContract from './SaveContract';
export Shapeshift from './Shapeshift';
export Transfer from './Transfer';
export UpgradeParity from './UpgradeParity';
export VaultAccounts from './VaultAccounts';
export VaultCreate from './VaultCreate';
export VaultLock from './VaultLock';
export VaultUnlock from './VaultUnlock';
export Verification from './Verification';
export WalletSettings from './WalletSettings';

View File

@ -19,7 +19,7 @@ import {
Contract, Contracts, Dapp, Dapps, HistoryStore, Home,
Settings, SettingsBackground, SettingsParity, SettingsProxy,
SettingsViews, Signer, Status,
Wallet, Web, WriteContract
Vaults, Wallet, Web, WriteContract
} from '~/views';
import builtinDapps from '~/views/Dapps/builtin.json';
@ -57,6 +57,7 @@ const accountsRoutes = [
accountsHistory.add(params.address, 'account');
}
},
{ path: '/vaults', component: Vaults },
{
path: '/wallet/:address',
component: Wallet,

View File

@ -14,6 +14,7 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.body {
text-align: center;
}

View File

@ -20,7 +20,7 @@ import { FormattedMessage } from 'react-intl';
import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button';
import Modal from '../Modal';
import Portal from '../Portal';
import { CancelIcon, CheckIcon } from '../Icons';
import styles from './confirmDialog.css';
@ -42,47 +42,58 @@ export default class ConfirmDialog extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
disabledConfirm: PropTypes.bool,
disabledDeny: PropTypes.bool,
busy: PropTypes.bool,
iconConfirm: PropTypes.node,
iconDeny: PropTypes.node,
labelConfirm: PropTypes.string,
labelDeny: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired,
open: PropTypes.bool,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired
visible: PropTypes.bool
}
render () {
const { children, className, title, visible } = this.props;
const { busy, children, className, disabledConfirm, disabledDeny, iconConfirm, iconDeny, labelConfirm, labelDeny, onConfirm, onDeny, open, title, visible } = this.props;
// TODO: visible is for compatibility with existing, open aligns with Portal.
// (Cleanup once all uses of ConfirmDialog has been migrated)
if (!visible && !open) {
return null;
}
return (
<Modal
<Portal
buttons={ [
<Button
disabled={ disabledDeny }
icon={ iconDeny || <CancelIcon /> }
key='deny'
label={ labelDeny || DEFAULT_NO }
onClick={ onDeny }
/>,
<Button
disabled={ disabledConfirm }
icon={ iconConfirm || <CheckIcon /> }
key='confirm'
label={ labelConfirm || DEFAULT_YES }
onClick={ onConfirm }
/>
] }
busy={ busy }
className={ className }
actions={ this.renderActions() }
isSmallModal
onClose={ onDeny }
title={ title }
visible={ visible }
open
>
<div className={ styles.body }>
{ children }
</div>
</Modal>
</Portal>
);
}
renderActions () {
const { iconConfirm, iconDeny, labelConfirm, labelDeny, onConfirm, onDeny } = this.props;
return [
<Button
icon={ iconDeny || <CancelIcon /> }
label={ labelDeny || DEFAULT_NO }
onClick={ onDeny }
/>,
<Button
icon={ iconConfirm || <CheckIcon /> }
label={ labelConfirm || DEFAULT_YES }
onClick={ onConfirm }
/>
];
}
}

View File

@ -15,41 +15,24 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React, { PropTypes } from 'react';
import React from 'react';
import sinon from 'sinon';
import muiTheme from '../Theme';
import ConfirmDialog from './';
let component;
let instance;
let onConfirm;
let onDeny;
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
settings: {
backgroundSeed: 'xyz'
}
};
}
};
}
function render (props = {}) {
onConfirm = sinon.stub();
onDeny = sinon.stub();
if (props.visible === undefined) {
props.visible = true;
if (props.open === undefined) {
props.open = true;
}
const baseComponent = shallow(
component = shallow(
<ConfirmDialog
{ ...props }
title='test title'
@ -62,57 +45,54 @@ function render (props = {}) {
</ConfirmDialog>
);
instance = baseComponent.instance();
component = baseComponent.find('Connect(Modal)').shallow({
childContextTypes: {
muiTheme: PropTypes.object,
store: PropTypes.object
},
context: {
muiTheme,
store: createRedux()
}
});
return component;
}
describe('ui/ConfirmDialog', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(render()).to.be.ok;
expect(component).to.be.ok;
});
it('renders the body as provided', () => {
expect(render().find('div[id="testContent"]').text()).to.equal('some test content');
expect(component.find('div[id="testContent"]').text()).to.equal('some test content');
});
describe('properties', () => {
describe('Portal properties', () => {
let props;
beforeEach(() => {
props = render().props();
});
it('passes the actions', () => {
expect(props.actions).to.deep.equal(instance.renderActions());
props = component.find('Portal').props();
});
it('passes title', () => {
expect(props.title).to.equal('test title');
});
it('passes visiblity flag', () => {
expect(props.visible).to.be.true;
it('passes open flag', () => {
expect(props.open).to.be.true;
});
});
describe('renderActions', () => {
describe('defaults', () => {
it('passes the small flag', () => {
expect(props.isSmallModal).to.be.true;
});
it('maps onClose to onDeny', () => {
expect(props.onClose).to.equal(onDeny);
});
describe('buttons', () => {
let buttons;
beforeEach(() => {
render();
buttons = instance.renderActions();
buttons = component.props().buttons;
});
it('passes the buttons', () => {
expect(buttons.length).to.equal(2);
});
it('renders with supplied onConfim/onDeny callbacks', () => {
@ -129,29 +109,27 @@ describe('ui/ConfirmDialog', () => {
expect(buttons[0].props.icon.type.displayName).to.equal('ContentClear');
expect(buttons[1].props.icon.type.displayName).to.equal('NavigationCheck');
});
});
describe('overrides', () => {
let buttons;
beforeEach(() => {
render({
labelConfirm: 'labelConfirm',
labelDeny: 'labelDeny',
iconConfirm: 'iconConfirm',
iconDeny: 'iconDeny'
describe('overrides', () => {
beforeEach(() => {
render({
labelConfirm: 'labelConfirm',
labelDeny: 'labelDeny',
iconConfirm: 'iconConfirm',
iconDeny: 'iconDeny'
});
buttons = component.props().buttons;
});
buttons = instance.renderActions();
});
it('renders supplied labels', () => {
expect(buttons[0].props.label).to.equal('labelDeny');
expect(buttons[1].props.label).to.equal('labelConfirm');
});
it('renders supplied labels', () => {
expect(buttons[0].props.label).to.equal('labelDeny');
expect(buttons[1].props.label).to.equal('labelConfirm');
});
it('renders supplied icons', () => {
expect(buttons[0].props.icon).to.equal('iconDeny');
expect(buttons[1].props.icon).to.equal('iconConfirm');
it('renders supplied icons', () => {
expect(buttons[0].props.icon).to.equal('iconDeny');
expect(buttons[1].props.icon).to.equal('iconConfirm');
});
});
});
});

View File

@ -26,6 +26,7 @@ $smallFontSize: 0.75rem;
color: $bylineColor;
display: -webkit-box;
line-height: $bylineLineHeight;
min-height: $bylineMaxHeight;
max-height: $bylineMaxHeight;
overflow: hidden;
position: relative;
@ -45,5 +46,8 @@ $smallFontSize: 0.75rem;
.title {
line-height: $titleLineHeight;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}

View File

@ -16,33 +16,39 @@
*/
$background: rgba(18, 18, 18, 0.85);
$backgroundOverlay: rgba(18, 18, 18, 1);
$backgroundHover: rgba(18, 18, 18, 1);
$transitionAll: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
.container {
background: $background;
flex: 1;
height: 100%;
padding: 0em;
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
position: relative;
transition: $transitionAll;
width: 100%;
.hoverOverlay {
background: $backgroundOverlay;
display: none;
background: $background;
left: 0;
margin-top: -1.5em;
opacity: inherit;
padding: 0 1.5em 1.5em 1.5em;
position: absolute;
right: 0;
top: 100%;
transition: $transitionAll;
transform: scale(0.5, 0);
transform-origin: top center;
z-index: 100;
}
&:hover {
background: $backgroundOverlay;
background: $backgroundHover;
.hoverOverlay {
display: block;
background: $backgroundHover;
transform: scale(1, 1);
}
}
}

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export AccountsIcon from 'material-ui/svg-icons/action/account-balance-wallet';
export AddIcon from 'material-ui/svg-icons/content/add';
export AttachFileIcon from 'material-ui/svg-icons/editor/attach-file';
export CancelIcon from 'material-ui/svg-icons/content/clear';
@ -28,6 +29,7 @@ export DeleteIcon from 'material-ui/svg-icons/action/delete';
export DoneIcon from 'material-ui/svg-icons/action/done-all';
export EditIcon from 'material-ui/svg-icons/content/create';
export FingerprintIcon from 'material-ui/svg-icons/action/fingerprint';
export KeyIcon from 'material-ui/svg-icons/communication/vpn-key';
export LinkIcon from 'material-ui/svg-icons/content/link';
export LockedIcon from 'material-ui/svg-icons/action/lock';
export MoveIcon from 'material-ui/svg-icons/action/open-with';
@ -41,6 +43,7 @@ export SnoozeIcon from 'material-ui/svg-icons/av/snooze';
export StarCircleIcon from 'material-ui/svg-icons/action/stars';
export StarIcon from 'material-ui/svg-icons/toggle/star';
export StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border';
export UnlockedIcon from 'material-ui/svg-icons/action/lock-open';
export VerifyIcon from 'material-ui/svg-icons/action/verified-user';
export VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
export VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';

View File

@ -0,0 +1,28 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import { createElement } from 'react';
import * as Icons from './';
describe('ui/Icons', () => {
Object.keys(Icons).forEach((icon) => {
it(`contains & renders ${icon}`, () => {
expect(shallow(createElement(Icons[icon]))).to.be.ok;
});
});
});

View File

@ -31,22 +31,20 @@ export default class Page extends Component {
render () {
const { buttons, className, children, title } = this.props;
const classes = `${styles.layout} ${className}`;
let actionbar = null;
if (title || buttons) {
actionbar = (
<Actionbar
buttons={ buttons }
title={ title }
/>
);
}
return (
<div>
{ actionbar }
<div className={ classes }>
{
title || buttons
? (
<Actionbar
buttons={ buttons }
title={ title }
/>
)
: null
}
<div className={ [styles.layout, className].join(' ') }>
{ children }
</div>
</div>

View File

@ -0,0 +1,79 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Page from './';
const BUTTONS = ['buttonA', 'buttonB'];
const CLASSNAME = 'testClass';
const TESTTEXT = 'testing children';
const TITLE = 'test title';
let component;
function render () {
component = shallow(
<Page
buttons={ BUTTONS }
className={ CLASSNAME }
title={ TITLE }
>
<div id='testContent'>
{ TESTTEXT }
</div>
</Page>
);
return component;
}
describe('ui/Page', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders the children', () => {
expect(component.find('div[id="testContent"]').text()).to.equal(TESTTEXT);
});
describe('components', () => {
describe('ActionBar', () => {
let actions;
beforeEach(() => {
actions = component.find('Actionbar');
});
it('renders the actionbar', () => {
expect(actions.get(0)).to.be.ok;
});
it('passes the provided title', () => {
expect(actions.props().title).to.equal(TITLE);
});
it('passed the provided buttons', () => {
expect(actions.props().buttons).to.equal(BUTTONS);
});
});
});
});

View File

@ -17,6 +17,7 @@
$modalMargin: 1.5em;
$modalPadding: 1.5em;
$modalPaddingChild: 3em;
$modalBackZ: 2500;
/* This should be the default case, the Portal used as a stand-alone modal */
@ -50,7 +51,7 @@ $popoverZ: 3600;
left: 0;
right: 0;
opacity: 0.25;
z-index: -1;
z-index: 0;
}
.overlay {
@ -67,11 +68,24 @@ $popoverZ: 3600;
}
&.modal {
bottom: $modalBottom;
left: $modalLeft;
right: $modalRight;
top: $modalTop;
z-index: $modalZ;
&:not(.small) {
bottom: $modalBottom;
left: $modalLeft;
right: $modalRight;
top: $modalTop;
z-index: $modalZ;
}
/* TODO: Small Portals don't adjust their overall height like we have with the
/* rest, so really tiny screens and large small Portals (it shouldn't be be done,
/* but may well be) will scretch to non-visible areas.
*/
&.small {
margin: 1.5em auto;
max-width: 768px;
position: relative;
width: 75%;
}
}
&.popover {
@ -100,8 +114,11 @@ $popoverZ: 3600;
.childContainer {
flex: 1;
margin: 0 -$modalPadding;
overflow-x: hidden;
overflow-y: auto;
padding: 0 $modalPaddingChild;
z-index: 1;
}
.closeIcon {

View File

@ -43,6 +43,7 @@ export default class Portal extends Component {
className: PropTypes.string,
hideClose: PropTypes.bool,
isChildModal: PropTypes.bool,
isSmallModal: PropTypes.bool,
onKeyDown: PropTypes.func,
steps: PropTypes.array,
title: nodeOrStringProptype()
@ -63,7 +64,7 @@ export default class Portal extends Component {
}
render () {
const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props;
const { activeStep, busy, busySteps, children, className, isChildModal, isSmallModal, open, steps, title } = this.props;
if (!open) {
return null;
@ -85,6 +86,9 @@ export default class Portal extends Component {
isChildModal
? styles.popover
: styles.modal,
isSmallModal
? styles.small
: null,
className
].join(' ')
}

View File

@ -15,12 +15,17 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.title {
.title,
.subtitle {
.steps {
margin: -0.5em 0 -1em 0;
}
.waiting {
margin: 1em -1em -1em -1em;
margin: 1em -1.5em 0 -1.5em;
}
}
.subtitle {
opacity: 0.75;
}

View File

@ -30,15 +30,18 @@ import styles from './title.css';
export default class Title extends Component {
static propTypes = {
activeStep: PropTypes.number,
description: nodeOrStringProptype(),
busy: PropTypes.bool,
busySteps: PropTypes.array,
byline: nodeOrStringProptype(),
className: PropTypes.string,
isSubTitle: PropTypes.bool,
steps: PropTypes.array,
title: nodeOrStringProptype()
}
render () {
const { activeStep, className, steps, title } = this.props;
const { activeStep, byline, className, description, isSubTitle, steps, title } = this.props;
if (!title && !steps) {
return null;
@ -47,10 +50,17 @@ export default class Title extends Component {
return (
<div
className={
[styles.title, className].join(' ')
[
isSubTitle
? styles.subtitle
: styles.title,
className
].join(' ')
}
>
<ContainerTitle
byline={ byline }
description={ description }
title={
steps
? steps[activeStep || 0]

View File

@ -0,0 +1,90 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Title from './';
let component;
let instance;
function render (props = {}) {
component = shallow(
<Title
activeStep={ 0 }
byline='testByline'
className='testClass'
description='testDescription'
title='testTitle'
{ ...props }
/>
);
instance = component.instance();
return component;
}
describe('ui/Title', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('instance methods', () => {
describe('renderSteps', () => {
let stepper;
beforeEach(() => {
render({ steps: ['stepA', 'stepB'] });
stepper = shallow(instance.renderSteps());
});
it('renders the Stepper', () => {
expect(stepper.find('Stepper').get(0)).to.be.ok;
});
});
describe('renderTimeline', () => {
let steps;
beforeEach(() => {
render({ steps: ['stepA', 'StepB'] });
steps = instance.renderTimeline();
});
it('renders the Step', () => {
expect(steps.length).to.equal(2);
});
});
describe('renderWaiting', () => {
let waiting;
beforeEach(() => {
render({ busy: true });
waiting = shallow(instance.renderWaiting());
});
it('renders the LinearProgress', () => {
expect(waiting.find('LinearProgress').get(0)).to.be.ok;
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './layout';

View File

@ -0,0 +1,45 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$imageHeight: 56px;
.layout {
display: flex;
min-height: $imageHeight;
overflow: hidden;
text-align: left;
&.border {
background: rgba(0, 0, 0, 0.25);
padding: 1.5em;
}
.identityIcon {
margin-right: 1em;
vertical-align: top;
&.locked {
filter: grayscale(100%);
opacity: 0.33;
}
}
.info {
flex: 1;
overflow: hidden;
}
}

View File

@ -0,0 +1,66 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import Title from '~/ui/Title';
import IdentityIcon from '~/ui/IdentityIcon';
import styles from './layout.css';
export default class Layout extends Component {
static propTypes = {
vault: PropTypes.object.isRequired,
withBorder: PropTypes.bool
};
render () {
const { vault, withBorder } = this.props;
const { isOpen, meta, name } = vault;
return (
<div
className={
[
styles.layout,
withBorder
? styles.border
: null
].join(' ')
}
>
<IdentityIcon
address={ name }
center
className={
[
styles.identityIcon,
isOpen || withBorder
? styles.unlocked
: styles.locked
].join(' ')
}
/>
<div className={ styles.info }>
<Title
byline={ meta.description }
title={ name }
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,90 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Layout from './';
const DESCRIPTION = 'some description';
const NAME = 'testName';
let component;
function render () {
component = shallow(
<Layout
vault={ {
isOpen: true,
meta: {
description: DESCRIPTION,
passwordHint: 'some hint'
},
name: NAME
} }
/>
);
return component;
}
describe('ui/VaultCard/Layout', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('IdentityIcon', () => {
let icon;
beforeEach(() => {
icon = component.find('Connect(IdentityIcon)');
});
it('renders', () => {
expect(icon.get(0)).to.be.ok;
});
it('passes the name as address key', () => {
expect(icon.props().address).to.equal(NAME);
});
});
describe('Title', () => {
let title;
beforeEach(() => {
title = component.find('Title');
});
it('renders', () => {
expect(title.get(0)).to.be.ok;
});
it('passes the name as title', () => {
expect(title.props().title).to.equal(NAME);
});
it('passes the description as byline', () => {
expect(title.props().byline).to.equal(DESCRIPTION);
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaultCard';

View File

@ -0,0 +1,62 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.container {
text-align: left;
.accounts {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 1.5em;
.account {
margin: 0.5em;
}
}
.empty {
margin-top: 1.5em;
opacity: 0.5;
text-align: center;
}
.buttons {
margin: -1em -1em 0.75em -1em;
text-align: right;
button.status {
min-width: 2em !important;
}
button:not(.status) {
display: none !important;
}
}
&:hover {
.buttons {
button.status {
display: none !important;
}
button:not(.status) {
display: inline-block !important;
}
}
}
}

View File

@ -0,0 +1,102 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import Button from '~/ui/Button';
import Container from '~/ui/Container';
import IdentityIcon from '~/ui/IdentityIcon';
import { LockedIcon, UnlockedIcon } from '~/ui/Icons';
import Layout from './Layout';
import styles from './vaultCard.css';
export default class VaultCard extends Component {
static propTypes = {
accounts: PropTypes.array,
buttons: PropTypes.array,
vault: PropTypes.object.isRequired
};
static Layout = Layout;
render () {
const { buttons, vault } = this.props;
const { isOpen } = vault;
return (
<Container
className={ styles.container }
hover={
isOpen
? this.renderAccounts()
: null
}
>
<div className={ styles.buttons }>
<Button
className={ styles.status }
disabled
icon={
isOpen
? <UnlockedIcon />
: <LockedIcon />
}
key='status'
/>
{ buttons }
</div>
<Layout vault={ vault } />
</Container>
);
}
renderAccounts () {
const { accounts } = this.props;
if (!accounts || !accounts.length) {
return (
<div className={ styles.empty }>
<FormattedMessage
id='vaults.accounts.empty'
defaultMessage='There are no accounts in this vault'
/>
</div>
);
}
return (
<div className={ styles.accounts }>
{
accounts.map((address) => {
return (
<Link to={ `/accounts/${address}` }>
<IdentityIcon
address={ address }
center
className={ styles.account }
key={ address }
/>
</Link>
);
})
}
</div>
);
}
}

View File

@ -0,0 +1,94 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import VaultCard from './';
const VAULT = { name: 'testing', isOpen: true };
let component;
let instance;
function render (props = {}) {
component = shallow(
<VaultCard
vault={ VAULT }
{ ...props }
/>
);
instance = component.instance();
return component;
}
describe('ui/VaultCard', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('Layout', () => {
let layout;
beforeEach(() => {
layout = component.find('Layout');
});
it('renders', () => {
expect(layout.get(0)).to.be.ok;
});
it('passes the vault', () => {
expect(layout.props().vault).to.deep.equal(VAULT);
});
});
});
describe('instance methods', () => {
describe('renderAccounts', () => {
it('renders empty when no accounts supplied', () => {
expect(
shallow(instance.renderAccounts()).find('FormattedMessage').props().id
).to.equal('vaults.accounts.empty');
});
describe('with accounts', () => {
const ACCOUNTS = ['0x123', '0x456'];
let identities;
beforeEach(() => {
render({ accounts: ACCOUNTS });
identities = shallow(instance.renderAccounts()).find('Connect(IdentityIcon)');
});
it('renders the accounts when supplied', () => {
expect(identities).to.have.length(2);
});
it('renders accounts with correct address', () => {
console.log(identities.get(0));
expect(identities.get(0).props.address).to.equal(ACCOUNTS[0]);
});
});
});
});
});

View File

@ -53,4 +53,5 @@ export Title from './Title';
export Tooltips, { Tooltip } from './Tooltips';
export TxHash from './TxHash';
export TxList from './TxList';
export VaultCard from './VaultCard';
export Warning from './Warning';

View File

@ -14,16 +14,17 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { uniq, isEqual, pickBy, omitBy } from 'lodash';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq, isEqual, pickBy, omitBy } from 'lodash';
import List from './List';
import { CreateAccount, CreateWallet } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
import { AddIcon, KeyIcon } from '~/ui/Icons';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './accounts.css';
@ -37,7 +38,6 @@ class Accounts extends Component {
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired,
balances: PropTypes.object
}
@ -52,6 +52,7 @@ class Accounts extends Component {
}
componentWillMount () {
// FIXME: Messy, figure out what it fixes and do it elegantly
window.setTimeout(() => {
this.setState({ show: true });
}, 100);
@ -204,16 +205,41 @@ class Accounts extends Component {
const { accounts } = this.props;
const buttons = [
<Link
to='/vaults'
key='vaults'
>
<Button
icon={ <KeyIcon /> }
label={
<FormattedMessage
id='accounts.button.vaults'
defaultMessage='vaults'
/>
}
onClick={ this.onVaultsClick }
/>
</Link>,
<Button
key='newAccount'
icon={ <ContentAdd /> }
label='new account'
icon={ <AddIcon /> }
label={
<FormattedMessage
id='accounts.button.newAccount'
defaultMessage='new account'
/>
}
onClick={ this.onNewAccountClick }
/>,
<Button
key='newWallet'
icon={ <ContentAdd /> }
label='new wallet'
icon={ <AddIcon /> }
label={
<FormattedMessage
id='accounts.button.newWallet'
defaultMessage='new wallet'
/>
}
onClick={ this.onNewWalletClick }
/>,
<ActionbarExport
@ -228,7 +254,12 @@ class Accounts extends Component {
return (
<Actionbar
className={ styles.toolbar }
title='Accounts Overview'
title={
<FormattedMessage
id='accounts.title'
defaultMessage='Accounts Overview'
/>
}
buttons={ buttons }
>
<Tooltip

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './vaults';

View File

@ -0,0 +1,323 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, computed, observable, transaction } from 'mobx';
// TODO: We need to move this to a generic location, it should most probably be
// merged with the other valitation errors. Import here better than duplication.
import ERRORS from '~/modals/CreateAccount/errors';
let instance;
export default class Store {
@observable isBusyAccounts = false;
@observable isBusyCreate = false;
@observable isBusyLoad = false;
@observable isBusyLock = false;
@observable isBusyUnlock = false;
@observable isModalAccountsOpen = false;
@observable isModalCreateOpen = false;
@observable isModalLockOpen = false;
@observable isModalUnlockOpen = false;
@observable selectedAccounts = {};
@observable vault = null;
@observable vaults = [];
@observable vaultNames = [];
@observable vaultName = '';
@observable vaultNameError = ERRORS.noName;
@observable vaultDescription = '';
@observable vaultPassword = '';
@observable vaultPasswordHint = '';
@observable vaultPasswordRepeat = '';
constructor (api) {
this._api = api;
}
@computed get vaultPasswordRepeatError () {
return this.vaultPassword === this.vaultPasswordRepeat
? null
: ERRORS.noMatchPassword;
}
@action clearVaultFields = () => {
transaction(() => {
this.setVaultName('');
this.setVaultDescription('');
this.setVaultPassword('');
this.setVaultPasswordHint('');
this.setVaultPasswordRepeat('');
});
}
@action setBusyAccounts = (isBusy) => {
this.isBusyAccounts = isBusy;
}
@action setBusyCreate = (isBusy) => {
this.isBusyCreate = isBusy;
}
@action setBusyLoad = (isBusy) => {
this.isBusyLoad = isBusy;
}
@action setBusyLock = (isBusy) => {
this.isBusyLock = isBusy;
}
@action setBusyUnlock = (isBusy) => {
this.isBusyUnlock = isBusy;
}
@action setModalAccountsOpen = (isOpen) => {
transaction(() => {
this.setBusyAccounts(false);
this.isModalAccountsOpen = isOpen;
});
}
@action setModalCreateOpen = (isOpen) => {
transaction(() => {
this.setBusyCreate(false);
this.isModalCreateOpen = isOpen;
});
}
@action setModalLockOpen = (isOpen) => {
transaction(() => {
this.setBusyLock(false);
this.isModalLockOpen = isOpen;
});
}
@action setModalUnlockOpen = (isOpen) => {
transaction(() => {
this.setBusyUnlock(false);
this.setVaultPassword('');
this.isModalUnlockOpen = isOpen;
});
}
@action setSelectedAccounts = (selectedAccounts) => {
this.selectedAccounts = selectedAccounts;
}
@action setVaults = (allVaults, openedVaults, metaData) => {
transaction(() => {
this.vaultNames = allVaults.map((name) => name.toLowerCase());
this.vaults = allVaults.map((name, index) => {
return {
meta: metaData[index] || {},
name,
isOpen: openedVaults.includes(name)
};
});
});
}
@action setVaultDescription = (description) => {
this.vaultDescription = description;
}
@action setVaultName = (name) => {
let nameError = null;
if (!name || !name.trim().length) {
nameError = ERRORS.noName;
} else {
const lowerName = name.toLowerCase();
if (this.vaultNames.includes(lowerName)) {
nameError = ERRORS.duplicateName;
}
}
transaction(() => {
this.vault = this.vaults.find((vault) => vault.name === name);
this.vaultName = name;
this.vaultNameError = nameError;
});
}
@action setVaultPassword = (password) => {
this.vaultPassword = password;
}
@action setVaultPasswordHint = (hint) => {
this.vaultPasswordHint = hint;
}
@action setVaultPasswordRepeat = (password) => {
this.vaultPasswordRepeat = password;
}
@action toggleSelectedAccount = (address) => {
this.setSelectedAccounts(Object.assign({}, this.selectedAccounts, {
[address]: !this.selectedAccounts[address] })
);
}
closeAccountsModal () {
this.setModalAccountsOpen(false);
}
closeCreateModal () {
this.setModalCreateOpen(false);
}
closeLockModal () {
this.setModalLockOpen(false);
}
closeUnlockModal () {
this.setModalUnlockOpen(false);
}
openAccountsModal (name) {
transaction(() => {
this.setVaultName(name);
this.setSelectedAccounts({});
this.setModalAccountsOpen(true);
});
}
openCreateModal () {
transaction(() => {
this.clearVaultFields();
this.setModalCreateOpen(true);
});
}
openLockModal (name) {
transaction(() => {
this.setVaultName(name);
this.setModalLockOpen(true);
});
}
openUnlockModal (name) {
transaction(() => {
this.setVaultName(name);
this.setModalUnlockOpen(true);
});
}
loadVaults = () => {
this.setBusyLoad(true);
return Promise
.all([
this._api.parity.listVaults(),
this._api.parity.listOpenedVaults()
])
.then(([allVaults, openedVaults]) => {
return Promise
.all(allVaults.map((name) => this._api.parity.getVaultMeta(name)))
.then((metaData) => {
transaction(() => {
this.setBusyLoad(false);
this.setVaults(allVaults, openedVaults, metaData);
});
});
})
.catch((error) => {
console.warn('loadVaults', error);
this.setBusyLoad(false);
});
}
closeVault () {
this.setBusyLock(true);
return this._api.parity
.closeVault(this.vaultName)
.then(this.loadVaults)
.then(() => {
this.setBusyLock(false);
})
.catch((error) => {
console.error('closeVault', error);
this.setBusyLock(false);
throw error;
});
}
createVault () {
if (this.vaultNameError || this.vaultPasswordRepeatError) {
return Promise.reject();
}
this.setBusyCreate(true);
return this._api.parity
.newVault(this.vaultName, this.vaultPassword)
.then(() => {
return this._api.parity.setVaultMeta(this.vaultName, {
description: this.vaultDescription,
passwordHint: this.vaultPasswordHint
});
})
.then(this.loadVaults)
.then(() => {
this.setBusyCreate(false);
})
.catch((error) => {
console.error('createVault', error);
this.setBusyCreate(false);
throw error;
});
}
openVault () {
this.setBusyUnlock(true);
return this._api.parity
.openVault(this.vaultName, this.vaultPassword)
.then(this.loadVaults)
.then(() => {
this.setBusyUnlock(false);
})
.catch((error) => {
console.error('openVault', error);
this.setBusyUnlock(false);
throw error;
});
}
moveAccounts (vaultName, inAccounts, outAccounts) {
this.setBusyAccounts(true);
return Promise
.all([
inAccounts.map((address) => this._api.parity.changeVault(address, vaultName)),
outAccounts.map((address) => this._api.parity.changeVault(address, ''))
])
.then(this.loadVaults)
.catch((error) => {
console.error('moveAccounts', error);
throw error;
});
}
static get (api) {
if (!instance) {
instance = new Store(api);
}
return instance;
}
}

View File

@ -0,0 +1,516 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import Vaults from './';
import ERRORS from '~/modals/CreateAccount/errors';
import { createApi, TEST_VAULTS_ALL, TEST_VAULTS_META, TEST_VAULTS_OPEN } from './vaults.test.js';
let api;
let store;
function create () {
api = createApi();
store = new Vaults.Store(api);
return store;
}
describe('modals/Vaults/Store', () => {
beforeEach(() => {
create();
});
describe('@action', () => {
describe('clearVaultFields', () => {
beforeEach(() => {
store.setVaultDescription('testing desc');
store.setVaultName('testing 123');
store.setVaultPassword('blah');
store.setVaultPasswordRepeat('bleh');
store.setVaultPasswordHint('hint');
store.clearVaultFields();
});
it('resets create fields', () => {
expect(store.vaultDescription).to.equal('');
expect(store.vaultName).to.equal('');
expect(store.vaultNameError).not.to.be.null;
expect(store.vaultPassword).to.equal('');
expect(store.vaultPasswordRepeat).to.equal('');
expect(store.vaultPasswordHint).to.equal('');
});
});
describe('setBusyAccounts', () => {
it('sets the flag', () => {
store.setBusyAccounts('busy');
expect(store.isBusyAccounts).to.equal('busy');
});
});
describe('setBusyCreate', () => {
it('sets the flag', () => {
store.setBusyCreate('busy');
expect(store.isBusyCreate).to.equal('busy');
});
});
describe('setBusyLoad', () => {
it('sets the flag', () => {
store.setBusyLoad('busy');
expect(store.isBusyLoad).to.equal('busy');
});
});
describe('setBusyLock', () => {
it('sets the flag', () => {
store.setBusyLock('busy');
expect(store.isBusyLock).to.equal('busy');
});
});
describe('setBusyUnlock', () => {
it('sets the flag', () => {
store.setBusyUnlock('busy');
expect(store.isBusyUnlock).to.equal('busy');
});
});
describe('setModalAccountsOpen', () => {
it('sets the flag', () => {
store.setModalAccountsOpen('opened');
expect(store.isModalAccountsOpen).to.equal('opened');
});
});
describe('setModalCreateOpen', () => {
it('sets the flag', () => {
store.setModalCreateOpen('opened');
expect(store.isModalCreateOpen).to.equal('opened');
});
});
describe('setModalLockOpen', () => {
it('sets the flag', () => {
store.setModalLockOpen('opened');
expect(store.isModalLockOpen).to.equal('opened');
});
});
describe('setModalUnlockOpen', () => {
beforeEach(() => {
store.setVaultPassword('testing');
store.setModalUnlockOpen('opened');
});
it('sets the flag', () => {
expect(store.isModalUnlockOpen).to.equal('opened');
});
it('rests the password to empty', () => {
expect(store.vaultPassword).to.equal('');
});
});
describe('setSelectedAccounts', () => {
it('sets the selected accounts', () => {
store.setSelectedAccounts('testing');
expect(store.selectedAccounts).to.equal('testing');
});
});
describe('setVaults', () => {
beforeEach(() => {
store.setVaults(['TEST', 'some'], ['TEST'], ['metaTest', 'metaSome']);
});
it('stores the available vault names (lookup)', () => {
expect(store.vaultNames.peek()).to.deep.equal(['test', 'some']);
});
it('sets all vaults with correct flags', () => {
expect(store.vaults.peek()).to.deep.equal([
{ name: 'TEST', meta: 'metaTest', isOpen: true },
{ name: 'some', meta: 'metaSome', isOpen: false }
]);
});
});
describe('setVaultDescription', () => {
it('sets the description', () => {
store.setVaultDescription('test');
expect(store.vaultDescription).to.equal('test');
});
});
describe('setVaultName', () => {
it('sets the name as passed', () => {
store.setVaultName('testing');
expect(store.vaultName).to.equal('testing');
});
it('sets the vault when found', () => {
store.setVaults(['testing'], [], ['meta']);
store.setVaultName('testing');
expect(store.vault).to.deep.equal({
isOpen: false,
meta: 'meta',
name: 'testing'
});
});
it('clears the vault when not found', () => {
store.setVaults(['testing'], [], ['meta']);
store.setVaultName('testing2');
expect(store.vault).not.to.be.ok;
});
it('sets error noName error when empty', () => {
store.setVaultName(null);
expect(store.vaultNameError).to.equal(ERRORS.noName);
});
it('sets error duplicateName when duplicated', () => {
store.setVaults(['testDupe'], [], ['testing']);
store.setVaultName('testDUPE');
expect(store.vaultNameError).to.equal(ERRORS.duplicateName);
});
});
describe('setVaultPassword', () => {
it('sets the password', () => {
store.setVaultPassword('testPassword');
expect(store.vaultPassword).to.equal('testPassword');
});
});
describe('setVaultPasswordRepeat', () => {
it('sets the password', () => {
store.setVaultPasswordRepeat('testPassword');
expect(store.vaultPasswordRepeat).to.equal('testPassword');
});
});
describe('setVaultPasswordHint', () => {
it('sets the password hint', () => {
store.setVaultPasswordHint('test hint');
expect(store.vaultPasswordHint).to.equal('test hint');
});
});
describe('toggleSelectedAccount', () => {
beforeEach(() => {
store.toggleSelectedAccount('123');
});
it('adds the selected account', () => {
expect(store.selectedAccounts['123']).to.be.true;
});
it('reverses when toggled again', () => {
store.toggleSelectedAccount('123');
expect(store.selectedAccounts['123']).to.be.false;
});
});
});
describe('@computed', () => {
describe('createPasswordRepeatError', () => {
beforeEach(() => {
store.setVaultPassword('blah');
store.setVaultPasswordRepeat('bleh');
});
it('has error when passwords do not match', () => {
expect(store.vaultPasswordRepeatError).not.to.be.null;
});
it('has no error when passwords match', () => {
store.setVaultPasswordRepeat('blah');
expect(store.vaultPasswordRepeatError).to.be.null;
});
});
});
describe('operations', () => {
describe('closeAccountsModal', () => {
beforeEach(() => {
store.setModalAccountsOpen(true);
store.closeAccountsModal();
});
it('sets the opened state to false', () => {
expect(store.isModalAccountsOpen).to.be.false;
});
});
describe('closeCreateModal', () => {
beforeEach(() => {
store.setModalCreateOpen(true);
store.closeCreateModal();
});
it('sets the opened state to false', () => {
expect(store.isModalCreateOpen).to.be.false;
});
});
describe('closeLockModal', () => {
beforeEach(() => {
store.setModalLockOpen(true);
store.closeLockModal();
});
it('sets the opened state to false', () => {
expect(store.isModalLockOpen).to.be.false;
});
});
describe('closeUnlockModal', () => {
beforeEach(() => {
store.setModalUnlockOpen(true);
store.closeUnlockModal();
});
it('sets the opened state to false', () => {
expect(store.isModalUnlockOpen).to.be.false;
});
});
describe('openAccountsModal', () => {
beforeEach(() => {
store.setSelectedAccounts({ '123': true, '456': false });
store.openAccountsModal('testing');
});
it('sets the opened state to true', () => {
expect(store.isModalAccountsOpen).to.be.true;
});
it('stores the name', () => {
expect(store.vaultName).to.equal('testing');
});
it('empties the selectedAccounts', () => {
expect(Object.keys(store.selectedAccounts).length).to.equal(0);
});
});
describe('openCreateModal', () => {
beforeEach(() => {
sinon.spy(store, 'clearVaultFields');
store.openCreateModal();
});
afterEach(() => {
store.clearVaultFields.restore();
});
it('sets the opened state to true', () => {
expect(store.isModalCreateOpen).to.be.true;
});
it('clears the create fields', () => {
expect(store.clearVaultFields).to.have.been.called;
});
});
describe('openLockModal', () => {
beforeEach(() => {
store.openLockModal('testing');
});
it('sets the opened state to true', () => {
expect(store.isModalLockOpen).to.be.true;
});
it('stores the name', () => {
expect(store.vaultName).to.equal('testing');
});
});
describe('openUnlockModal', () => {
beforeEach(() => {
store.openUnlockModal('testing');
});
it('sets the opened state to true', () => {
expect(store.isModalUnlockOpen).to.be.true;
});
it('stores the name', () => {
expect(store.vaultName).to.equal('testing');
});
});
describe('loadVaults', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyLoad');
sinon.spy(store, 'setVaults');
return store.loadVaults();
});
afterEach(() => {
store.setBusyLoad.restore();
store.setVaults.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyLoad).to.have.been.calledWith(true);
expect(store.isBusyLoad).to.be.false;
});
it('calls parity_listVaults', () => {
expect(api.parity.listVaults).to.have.been.called;
});
it('calls parity_listOpenedVaults', () => {
expect(api.parity.listOpenedVaults).to.have.been.called;
});
it('sets the vaults', () => {
expect(store.setVaults).to.have.been.calledWith(TEST_VAULTS_ALL, TEST_VAULTS_OPEN, [
TEST_VAULTS_META, TEST_VAULTS_META, TEST_VAULTS_META
]);
});
});
describe('closeVault', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyLock');
store.setVaultName('testVault');
return store.closeVault();
});
afterEach(() => {
store.setBusyLock.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyLock).to.have.been.calledWith(true);
expect(store.isBusyLock).to.be.false;
});
it('calls into parity_closeVault', () => {
expect(api.parity.closeVault).to.have.been.calledWith('testVault');
});
});
describe('createVault', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyCreate');
store.setVaultDescription('testDescription');
store.setVaultName('testCreateName');
store.setVaultPassword('testCreatePassword');
store.setVaultPasswordRepeat('testCreatePassword');
store.setVaultPasswordHint('testCreateHint');
return store.createVault();
});
afterEach(() => {
store.setBusyCreate.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyCreate).to.have.been.calledWith(true);
expect(store.isBusyCreate).to.be.false;
});
it('calls into parity_newVault', () => {
expect(api.parity.newVault).to.have.been.calledWith('testCreateName', 'testCreatePassword');
});
it('calls into parity_setVaultMeta', () => {
expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', {
description: 'testDescription',
passwordHint: 'testCreateHint'
});
});
});
describe('openVault', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyUnlock');
store.setVaultName('testVault');
return store.openVault();
});
afterEach(() => {
store.setBusyUnlock.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyUnlock).to.have.been.calledWith(true);
expect(store.isBusyUnlock).to.be.false;
});
it('calls into parity_openVault', () => {
expect(api.parity.openVault).to.have.been.calledWith('testVault');
});
});
describe('moveAccounts', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyAccounts');
return store.moveAccounts('testVault', ['A', 'B'], ['C']);
});
afterEach(() => {
store.setBusyAccounts.restore();
});
it('sets the busy flag', () => {
expect(store.setBusyAccounts).to.have.been.calledWith(true);
});
it('calls into parity_changeVault', () => {
expect(api.parity.changeVault).to.have.been.calledWith('A', 'testVault');
expect(api.parity.changeVault).to.have.been.calledWith('B', 'testVault');
expect(api.parity.changeVault).to.have.been.calledWith('C', '');
});
});
});
});

View File

@ -0,0 +1,25 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.empty {
width: 100%;
display: block;
span {
color: #aaa;
}
}

View File

@ -0,0 +1,193 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { VaultAccounts, VaultCreate, VaultLock, VaultUnlock } from '~/modals';
import { Button, Container, Page, SectionList, VaultCard } from '~/ui';
import { AccountsIcon, AddIcon, LockedIcon, UnlockedIcon } from '~/ui/Icons';
import Store from './store';
import styles from './vaults.css';
@observer
class Vaults extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired
};
static Store = Store;
vaultStore = Store.get(this.context.api);
componentWillMount () {
return this.vaultStore.loadVaults();
}
render () {
return (
<Page
buttons={ [
<Button
icon={ <AddIcon /> }
key='create'
label={
<FormattedMessage
id='vaults.button.add'
defaultMessage='create vault'
/>
}
onClick={ this.onOpenCreate }
/>
] }
title={
<FormattedMessage
id='vaults.title'
defaultMessage='Vault Management'
/>
}
>
<VaultAccounts vaultStore={ this.vaultStore } />
<VaultCreate vaultStore={ this.vaultStore } />
<VaultLock vaultStore={ this.vaultStore } />
<VaultUnlock vaultStore={ this.vaultStore } />
{ this.renderList() }
</Page>
);
}
renderList () {
const { vaults } = this.vaultStore;
if (!vaults || !vaults.length) {
return (
<Container className={ styles.empty }>
<FormattedMessage
id='vaults.empty'
defaultMessage='There are currently no vaults to display.'
/>
</Container>
);
}
return (
<SectionList
items={ vaults }
renderItem={ this.renderVault }
/>
);
}
renderVault = (vault) => {
const { accounts } = this.props;
const { isOpen, name } = vault;
const vaultAccounts = Object
.keys(accounts)
.filter((address) => accounts[address].uuid && accounts[address].meta.vault === vault.name);
const onClickAccounts = () => {
this.onOpenAccounts(name);
return false;
};
const onClickOpen = () => {
isOpen
? this.onOpenLockVault(name)
: this.onOpenUnlockVault(name);
return false;
};
return (
<VaultCard
accounts={ vaultAccounts }
buttons={
isOpen
? [
<Button
icon={ <AccountsIcon /> }
key='accounts'
label={
<FormattedMessage
id='vaults.button.accounts'
defaultMessage='accounts'
/>
}
onClick={ onClickAccounts }
/>,
<Button
icon={ <LockedIcon /> }
key='close'
label={
<FormattedMessage
id='vaults.button.close'
defaultMessage='close vault'
/>
}
onClick={ onClickOpen }
/>
]
: [
<Button
icon={ <UnlockedIcon /> }
key='open'
label={
<FormattedMessage
id='vaults.button.open'
defaultMessage='open vault'
/>
}
onClick={ onClickOpen }
/>
]
}
vault={ vault }
/>
);
}
onOpenAccounts = (name) => {
this.vaultStore.openAccountsModal(name);
}
onOpenCreate = () => {
this.vaultStore.openCreateModal();
}
onOpenLockVault = (name) => {
this.vaultStore.openLockModal(name);
}
onOpenUnlockVault = (name) => {
this.vaultStore.openUnlockModal(name);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return { accounts };
}
export default connect(
mapStateToProps,
null
)(Vaults);

View File

@ -0,0 +1,185 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import { createApi, createReduxStore } from './vaults.test.js';
import Vaults from './';
let api;
let component;
let instance;
let store;
function render (props = {}) {
api = createApi();
store = createReduxStore();
component = shallow(
<Vaults />,
{
context: { store }
}
).find('Vaults').shallow({ context: { api } });
instance = component.instance();
return component;
}
describe('modals/Vaults', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('instance methods', () => {
describe('componentWillMount', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'loadVaults');
return instance.componentWillMount();
});
afterEach(() => {
instance.vaultStore.loadVaults.restore();
});
it('calls into vaultStore.loadVaults', () => {
expect(instance.vaultStore.loadVaults).to.have.been.called;
});
});
describe('renderList', () => {
it('renders empty when no vaults', () => {
instance.vaultStore.setVaults([], [], []);
expect(
shallow(instance.renderList()).find('FormattedMessage').props().id
).to.equal('vaults.empty');
});
describe('SectionList', () => {
let list;
beforeEach(() => {
instance.vaultStore.setVaults(['testing'], [], ['meta']);
list = instance.renderList();
});
it('renders', () => {
expect(list).to.ok;
});
it('passes the vaults', () => {
expect(list.props.items.peek()).to.deep.equal(instance.vaultStore.vaults.peek());
});
it('renders via renderItem', () => {
expect(list.props.renderItem).to.deep.equal(instance.renderVault);
});
});
});
describe('renderVault', () => {
const VAULT = { name: 'testing', isOpen: true, meta: 'meta' };
let card;
beforeEach(() => {
card = instance.renderVault(VAULT);
});
it('renders', () => {
expect(card).to.be.ok;
});
it('passes the vault', () => {
expect(card.props.vault).to.deep.equal(VAULT);
});
});
});
describe('event methods', () => {
describe('onOpenAccounts', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openAccountsModal');
instance.onOpenAccounts('testing');
});
afterEach(() => {
instance.vaultStore.openAccountsModal.restore();
});
it('calls into vaultStore.openAccountsModal', () => {
expect(instance.vaultStore.openAccountsModal).to.have.been.calledWith('testing');
});
});
describe('onOpenCreate', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openCreateModal');
instance.onOpenCreate();
});
afterEach(() => {
instance.vaultStore.openCreateModal.restore();
});
it('calls into vaultStore.openCreateModal', () => {
expect(instance.vaultStore.openCreateModal).to.have.been.called;
});
});
describe('onOpenLockVault', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openLockModal');
instance.onOpenLockVault('testing');
});
afterEach(() => {
instance.vaultStore.openLockModal.restore();
});
it('calls into vaultStore.openLockModal', () => {
expect(instance.vaultStore.openLockModal).to.have.been.calledWith('testing');
});
});
describe('onOpenUnlockVault', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openUnlockModal');
instance.onOpenUnlockVault('testing');
});
afterEach(() => {
instance.vaultStore.openUnlockModal.restore();
});
it('calls into vaultStore.openUnlockModal', () => {
expect(instance.vaultStore.openUnlockModal).to.have.been.calledWith('testing');
});
});
});
});

View File

@ -0,0 +1,90 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ACCOUNT_A = '0x1234567890123456789012345678901234567890';
const ACCOUNT_B = '0x0123456789012345678901234567890123456789';
const ACCOUNT_C = '0x9012345678901234567890123456789012345678';
const ACCOUNT_D = '0x8901234567890123456789012345678901234567';
const TEST_VAULTS_ALL = ['vault1', 'vault2', 'vault3'];
const TEST_VAULTS_OPEN = ['vault2'];
const TEST_VAULTS_META = { something: 'test' };
const TEST_ACCOUNTS = {
[ACCOUNT_A]: {
address: ACCOUNT_A,
uuid: null
},
[ACCOUNT_B]: {
address: ACCOUNT_B,
uuid: ACCOUNT_B,
meta: {
vault: 'somethingElse'
}
},
[ACCOUNT_C]: {
address: ACCOUNT_C,
uuid: ACCOUNT_C,
meta: {
vault: 'test'
}
},
[ACCOUNT_D]: {
address: ACCOUNT_D,
uuid: ACCOUNT_D,
meta: {
vault: 'test'
}
}
};
export function createApi () {
return {
parity: {
listOpenedVaults: sinon.stub().resolves(TEST_VAULTS_OPEN),
listVaults: sinon.stub().resolves(TEST_VAULTS_ALL),
changeVault: sinon.stub().resolves(true),
closeVault: sinon.stub().resolves(true),
getVaultMeta: sinon.stub().resolves(TEST_VAULTS_META),
newVault: sinon.stub().resolves(true),
openVault: sinon.stub().resolves(true),
setVaultMeta: sinon.stub().resolves(true)
}
};
}
export function createReduxStore () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: TEST_ACCOUNTS
}
};
}
};
}
export {
TEST_ACCOUNTS,
TEST_VAULTS_ALL,
TEST_VAULTS_META,
TEST_VAULTS_OPEN
};

View File

@ -29,6 +29,7 @@ export ParityBar from './ParityBar';
export Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
export Signer from './Signer';
export Status from './Status';
export Vaults from './Vaults';
export Wallet from './Wallet';
export Web from './Web';
export WriteContract from './WriteContract';