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