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

@@ -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';