Vault Management UI (round 2) (#4631)

* Add VaultMeta edit dialog

* Updated (WIP)

* Meta & password edit completed

* Added SelectionList component for selections

* Use SelectionList in DappPermisions

* AddDapps uses SelectionList

* Fix AccountCard to consistent height

* Convert Signer defaults to SelectionList

* Subtle selection border

* Display account vault information

* Allow invalid addresses to display icons (e.g. vaults)

* Display vault on edit meta

* Convert VaultAccounts to SelectionList

* Allow editing of Vault in meta

* Add tests for SectionList component

* Add tests for VaultSelector component

* Auto-focus description field (aligns with #4657)

* Apply scroll fixes from lates commit in #4621

* Remove unneeded logs

* Remove extra div, fixing ParityBar overflow

* Disable save if password don't match

* s/disabled/readOnly/

* string -> bool
This commit is contained in:
Jaco Greeff
2017-02-24 18:05:04 +01:00
committed by GitHub
parent 9ff427caaf
commit 570e6f32b0
23 changed files with 1298 additions and 67 deletions

View File

@@ -36,6 +36,7 @@
.addressline,
.infoline,
.uuidline,
.vault,
.title {
margin-left: 72px;
}
@@ -59,6 +60,15 @@
display: inline-block;
}
.vault {
line-height: 32px;
.text {
display: inline-block;
opacity: 0.25;
}
}
.addressline {
display: flex;
}

View File

@@ -65,6 +65,7 @@ export default class Header extends Component {
<CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div>
</div>
{ this.renderVault() }
{ this.renderUuid() }
<div className={ styles.infoline }>
{ meta.description }
@@ -154,4 +155,25 @@ export default class Header extends Component {
</div>
);
}
renderVault () {
const { account } = this.props;
const { meta } = account;
if (!meta || !meta.vault) {
return null;
}
return (
<div className={ styles.vault }>
<IdentityIcon
address={ meta.vault }
inline
/>
<div className={ styles.text }>
{ meta.vault }
</div>
</div>
);
}
}

View File

@@ -27,10 +27,12 @@ export default class Store {
@observable isBusyCreate = false;
@observable isBusyLoad = false;
@observable isBusyLock = false;
@observable isBusyMeta = false;
@observable isBusyUnlock = false;
@observable isModalAccountsOpen = false;
@observable isModalCreateOpen = false;
@observable isModalLockOpen = false;
@observable isModalMetaOpen = false;
@observable isModalUnlockOpen = false;
@observable selectedAccounts = {};
@observable vault = null;
@@ -41,7 +43,9 @@ export default class Store {
@observable vaultDescription = '';
@observable vaultPassword = '';
@observable vaultPasswordHint = '';
@observable vaultPasswordOld = '';
@observable vaultPasswordRepeat = '';
@observable vaultTags = [];
constructor (api) {
this._api = api;
@@ -59,7 +63,9 @@ export default class Store {
this.setVaultDescription('');
this.setVaultPassword('');
this.setVaultPasswordHint('');
this.setVaultPasswordOld('');
this.setVaultPasswordRepeat('');
this.setVaultTags([]);
});
}
@@ -79,6 +85,10 @@ export default class Store {
this.isBusyLock = isBusy;
}
@action setBusyMeta = (isBusy) => {
this.isBusyMeta = isBusy;
}
@action setBusyUnlock = (isBusy) => {
this.isBusyUnlock = isBusy;
}
@@ -104,6 +114,13 @@ export default class Store {
});
}
@action setModalMetaOpen = (isOpen) => {
transaction(() => {
this.setBusyMeta(false);
this.isModalMetaOpen = isOpen;
});
}
@action setModalUnlockOpen = (isOpen) => {
transaction(() => {
this.setBusyUnlock(false);
@@ -161,10 +178,18 @@ export default class Store {
this.vaultPasswordHint = hint;
}
@action setVaultPasswordOld = (password) => {
this.vaultPasswordOld = password;
}
@action setVaultPasswordRepeat = (password) => {
this.vaultPasswordRepeat = password;
}
@action setVaultTags = (tags) => {
this.vaultTags = tags;
}
@action toggleSelectedAccount = (address) => {
this.setSelectedAccounts(Object.assign({}, this.selectedAccounts, {
[address]: !this.selectedAccounts[address] })
@@ -183,6 +208,10 @@ export default class Store {
this.setModalLockOpen(false);
}
closeMetaModal () {
this.setModalMetaOpen(false);
}
closeUnlockModal () {
this.setModalUnlockOpen(false);
}
@@ -209,6 +238,20 @@ export default class Store {
});
}
openMetaModal (name) {
transaction(() => {
this.clearVaultFields();
this.setVaultName(name);
if (this.vault && this.vault.meta) {
this.setVaultDescription(this.vault.meta.description);
this.setVaultPasswordHint(this.vault.meta.passwordHint);
}
this.setModalMetaOpen(true);
});
}
openUnlockModal (name) {
transaction(() => {
this.setVaultName(name);
@@ -268,7 +311,8 @@ export default class Store {
.then(() => {
return this._api.parity.setVaultMeta(this.vaultName, {
description: this.vaultDescription,
passwordHint: this.vaultPasswordHint
passwordHint: this.vaultPasswordHint,
tags: this.vaultTags
});
})
.then(this.loadVaults)
@@ -282,6 +326,48 @@ export default class Store {
});
}
editVaultMeta () {
this.setBusyMeta(true);
return this._api.parity
.setVaultMeta(this.vaultName, {
description: this.vaultDescription,
passwordHint: this.vaultPasswordHint,
tags: this.vaultTags
})
.then(this.loadVaults)
.then(() => {
this.setBusyMeta(false);
})
.catch((error) => {
console.error('editVaultMeta', error);
this.setBusyMeta(false);
throw error;
});
}
editVaultPassword () {
this.setBusyMeta(true);
return this._api.parity
.closeVault(this.vaultName)
.then(() => {
return this._api.parity.openVault(this.vaultName, this.vaultPasswordOld);
})
.then(() => {
return this._api.parity.changeVaultPassword(this.vaultName, this.vaultPassword);
})
.then(() => {
this.setBusyMeta(false);
})
.catch((error) => {
console.error('editVaultPassword', error);
this.loadVaults();
this.setBusyMeta(false);
throw new Error('Unable to change the vault password');
});
}
openVault () {
this.setBusyUnlock(true);
@@ -307,8 +393,28 @@ export default class Store {
outAccounts.map((address) => this._api.parity.changeVault(address, ''))
])
.then(this.loadVaults)
.then(() => {
this.setBusyAccounts(false);
})
.catch((error) => {
console.error('moveAccounts', error);
this.setBusyAccounts(false);
throw error;
});
}
moveAccount (vaultName, address) {
this.setBusyAccounts(true);
return this._api.parity
.changeVault(address, vaultName)
.then(this.loadVaults)
.then(() => {
this.setBusyAccounts(false);
})
.catch((error) => {
console.error('moveAccount', error);
this.setBusyAccounts(false);
throw error;
});
}

View File

@@ -44,6 +44,8 @@ describe('modals/Vaults/Store', () => {
store.setVaultPassword('blah');
store.setVaultPasswordRepeat('bleh');
store.setVaultPasswordHint('hint');
store.setVaultPasswordOld('old');
store.setVaultTags('tags');
store.clearVaultFields();
});
@@ -55,6 +57,8 @@ describe('modals/Vaults/Store', () => {
expect(store.vaultPassword).to.equal('');
expect(store.vaultPasswordRepeat).to.equal('');
expect(store.vaultPasswordHint).to.equal('');
expect(store.vaultPasswordOld).to.equal('');
expect(store.vaultTags.length).to.equal(0);
});
});
@@ -90,6 +94,14 @@ describe('modals/Vaults/Store', () => {
});
});
describe('setBusyMeta', () => {
it('sets the flag', () => {
store.setBusyMeta('busy');
expect(store.isBusyMeta).to.equal('busy');
});
});
describe('setBusyUnlock', () => {
it('sets the flag', () => {
store.setBusyUnlock('busy');
@@ -122,6 +134,14 @@ describe('modals/Vaults/Store', () => {
});
});
describe('setModalMetaOpen', () => {
it('sets the flag', () => {
store.setModalMetaOpen('opened');
expect(store.isModalMetaOpen).to.equal('opened');
});
});
describe('setModalUnlockOpen', () => {
beforeEach(() => {
store.setVaultPassword('testing');
@@ -233,6 +253,14 @@ describe('modals/Vaults/Store', () => {
});
});
describe('setVaultTags', () => {
it('sets the tags', () => {
store.setVaultTags('test');
expect(store.vaultTags).to.equal('test');
});
});
describe('toggleSelectedAccount', () => {
beforeEach(() => {
store.toggleSelectedAccount('123');
@@ -301,6 +329,17 @@ describe('modals/Vaults/Store', () => {
});
});
describe('closeMetaModal', () => {
beforeEach(() => {
store.setModalMetaOpen(true);
store.closeMetaModal();
});
it('sets the opened state to false', () => {
expect(store.isModalMetaOpen).to.be.false;
});
});
describe('closeUnlockModal', () => {
beforeEach(() => {
store.setModalUnlockOpen(true);
@@ -364,6 +403,20 @@ describe('modals/Vaults/Store', () => {
});
});
describe('openMetaModal', () => {
beforeEach(() => {
store.openMetaModal('testing');
});
it('sets the opened state to true', () => {
expect(store.isModalMetaOpen).to.be.true;
});
it('stores the name', () => {
expect(store.vaultName).to.equal('testing');
});
});
describe('openUnlockModal', () => {
beforeEach(() => {
store.openUnlockModal('testing');
@@ -443,6 +496,7 @@ describe('modals/Vaults/Store', () => {
store.setVaultPassword('testCreatePassword');
store.setVaultPasswordRepeat('testCreatePassword');
store.setVaultPasswordHint('testCreateHint');
store.setVaultTags('testTags');
return store.createVault();
});
@@ -463,11 +517,71 @@ describe('modals/Vaults/Store', () => {
it('calls into parity_setVaultMeta', () => {
expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', {
description: 'testDescription',
passwordHint: 'testCreateHint'
passwordHint: 'testCreateHint',
tags: 'testTags'
});
});
});
describe('editVaultMeta', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyMeta');
store.setVaultDescription('testDescription');
store.setVaultName('testCreateName');
store.setVaultPasswordHint('testCreateHint');
store.setVaultTags('testTags');
return store.editVaultMeta();
});
afterEach(() => {
store.setBusyMeta.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyMeta).to.have.been.calledWith(true);
expect(store.isBusyMeta).to.be.false;
});
it('calls into parity_setVaultMeta', () => {
expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', {
description: 'testDescription',
passwordHint: 'testCreateHint',
tags: 'testTags'
});
});
});
describe('editVaultPassword', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyMeta');
store.setVaultName('testName');
store.setVaultPasswordOld('oldPassword');
store.setVaultPassword('newPassword');
return store.editVaultPassword();
});
afterEach(() => {
store.setBusyMeta.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyMeta).to.have.been.calledWith(true);
expect(store.isBusyMeta).to.be.false;
});
it('calls into parity_openVault', () => {
expect(api.parity.openVault).to.have.been.calledWith('testName', 'oldPassword');
});
it('calls into parity_changeVaultPassword', () => {
expect(api.parity.changeVaultPassword).to.have.been.calledWith('testName', 'newPassword');
});
});
describe('openVault', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyUnlock');
@@ -512,5 +626,25 @@ describe('modals/Vaults/Store', () => {
expect(api.parity.changeVault).to.have.been.calledWith('C', '');
});
});
describe('moveAccount', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyAccounts');
return store.moveAccount('testVault', 'A');
});
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');
});
});
});
});

View File

@@ -19,9 +19,9 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { VaultAccounts, VaultCreate, VaultLock, VaultUnlock } from '~/modals';
import { VaultAccounts, VaultCreate, VaultLock, VaultMeta, VaultUnlock } from '~/modals';
import { Button, Container, Page, SectionList, VaultCard } from '~/ui';
import { AccountsIcon, AddIcon, LockedIcon, UnlockedIcon } from '~/ui/Icons';
import { AccountsIcon, AddIcon, EditIcon, LockedIcon, UnlockedIcon } from '~/ui/Icons';
import Store from './store';
import styles from './vaults.css';
@@ -70,6 +70,7 @@ class Vaults extends Component {
<VaultAccounts vaultStore={ this.vaultStore } />
<VaultCreate vaultStore={ this.vaultStore } />
<VaultLock vaultStore={ this.vaultStore } />
<VaultMeta vaultStore={ this.vaultStore } />
<VaultUnlock vaultStore={ this.vaultStore } />
{ this.renderList() }
</Page>
@@ -109,6 +110,10 @@ class Vaults extends Component {
this.onOpenAccounts(name);
return false;
};
const onClickEdit = () => {
this.onOpenEdit(name);
return false;
};
const onClickOpen = () => {
isOpen
? this.onOpenLockVault(name)
@@ -133,13 +138,24 @@ class Vaults extends Component {
}
onClick={ onClickAccounts }
/>,
<Button
icon={ <EditIcon /> }
key='edit'
label={
<FormattedMessage
id='vaults.button.edit'
defaultMessage='edit'
/>
}
onClick={ onClickEdit }
/>,
<Button
icon={ <LockedIcon /> }
key='close'
label={
<FormattedMessage
id='vaults.button.close'
defaultMessage='close vault'
defaultMessage='close'
/>
}
onClick={ onClickOpen }
@@ -152,7 +168,7 @@ class Vaults extends Component {
label={
<FormattedMessage
id='vaults.button.open'
defaultMessage='open vault'
defaultMessage='open'
/>
}
onClick={ onClickOpen }
@@ -172,10 +188,18 @@ class Vaults extends Component {
this.vaultStore.openCreateModal();
}
onOpenEdit = (name) => {
this.vaultStore.openMetaModal(name);
}
onOpenLockVault = (name) => {
this.vaultStore.openLockModal(name);
}
onOpenMeta = (name) => {
this.vaultStore.openMetaModal(name);
}
onOpenUnlockVault = (name) => {
this.vaultStore.openUnlockModal(name);
}

View File

@@ -42,7 +42,7 @@ function render (props = {}) {
return component;
}
describe('modals/Vaults', () => {
describe('views/Vaults', () => {
beforeEach(() => {
render();
});
@@ -150,6 +150,22 @@ describe('modals/Vaults', () => {
});
});
describe('onOpenEdit', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openMetaModal');
instance.onOpenEdit('testing');
});
afterEach(() => {
instance.vaultStore.openMetaModal.restore();
});
it('calls into vaultStore.openMetaModal', () => {
expect(instance.vaultStore.openMetaModal).to.have.been.calledWith('testing');
});
});
describe('onOpenLockVault', () => {
beforeEach(() => {
sinon.spy(instance.vaultStore, 'openLockModal');

View File

@@ -63,7 +63,8 @@ export function createApi () {
getVaultMeta: sinon.stub().resolves(TEST_VAULTS_META),
newVault: sinon.stub().resolves(true),
openVault: sinon.stub().resolves(true),
setVaultMeta: sinon.stub().resolves(true)
setVaultMeta: sinon.stub().resolves(true),
changeVaultPassword: sinon.stub().resolves(true)
}
};
}