Method intercept dialog selection (#5875)

* Cleanup filter config

* Show single request of type (at a time)

* Align packages

* DappFilter -> DappRequests

* Methods dialog (WIP)

* Initial dapp methods modal

* Align dialog & popup permissions

* appname -> token mapping TODOs

* Case
This commit is contained in:
Jaco Greeff 2017-06-19 17:25:17 +02:00 committed by GitHub
parent d4532fac18
commit e479260e1f
19 changed files with 377 additions and 58 deletions

13
js/package-lock.json generated
View File

@ -28,13 +28,7 @@
"version": "file:src/shell" "version": "file:src/shell"
}, },
"@parity/ui": { "@parity/ui": {
"version": "file:src/ui", "version": "file:src/ui"
"dependencies": {
"react-event-listener": {
"version": "0.4.1",
"bundled": true
}
}
}, },
"@parity/views": { "@parity/views": {
"version": "file:src/views" "version": "file:src/views"
@ -7300,11 +7294,6 @@
"integrity": "sha1-EQvc9cRZxPd8uF7WNLzTOXU2ODs=", "integrity": "sha1-EQvc9cRZxPd8uF7WNLzTOXU2ODs=",
"dev": true "dev": true
}, },
"react-addons-shallow-compare": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.0.tgz",
"integrity": "sha1-t6Tl/58nBMIM9obdigXdCLJt4lI="
},
"react-addons-test-utils": { "react-addons-test-utils": {
"version": "15.4.2", "version": "15.4.2",
"resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz", "resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz",

View File

@ -22,7 +22,7 @@ import { connect } from 'react-redux';
import { Errors } from '@parity/ui'; import { Errors } from '@parity/ui';
import Connection from '../Connection'; import Connection from '../Connection';
import DappFilter from '../DappFilter'; import DappRequests from '../DappRequests';
import Extension from '../Extension'; import Extension from '../Extension';
import FirstRun from '../FirstRun'; import FirstRun from '../FirstRun';
import ParityBar from '../ParityBar'; import ParityBar from '../ParityBar';
@ -83,7 +83,7 @@ class Application extends Component {
: null : null
} }
<Connection /> <Connection />
<DappFilter /> <DappRequests />
<Requests /> <Requests />
<ParityBar dapp={ isMinimized } /> <ParityBar dapp={ isMinimized } />
</div> </div>

View File

@ -14,5 +14,4 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
export Store from './store'; export default from './methodCheck';
export default from './dappFilter';

View File

@ -0,0 +1,40 @@
// 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, { PropTypes } from 'react';
import { CheckboxTickedIcon, CheckboxUntickedIcon } from '@parity/ui/Icons';
export default function MethodCheck ({ checked, dappId, method, onToggle }) {
const _onClick = () => onToggle(method, dappId);
return (
<div onClick={ _onClick }>
{
checked
? <CheckboxTickedIcon />
: <CheckboxUntickedIcon />
}
</div>
);
}
MethodCheck.propTypes = {
checked: PropTypes.bool.isRequired,
dappId: PropTypes.string.isRequired,
method: PropTypes.string.isRequired,
onToggle: PropTypes.func.isRequired
};

View File

@ -0,0 +1,47 @@
/* 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/>.
*/
$border: 1px solid #ccc;
.modal {
td {
border-bottom: $border;
border-right: $border;
padding: 0.25em;
vertical-align: center;
&.check {
cursor: pointer;
font-size: 1.5em;
}
}
th {
height: 140px;
white-space: nowrap;
> div {
transform: translate(25px, 51px) rotate(315deg);
width: 30px;
> span {
border-bottom: $border;
padding: 5px 10px;
}
}
}
}

View File

@ -0,0 +1,95 @@
// 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 { Portal } from '@parity/ui';
import MethodCheck from './MethodCheck';
import styles from './dappMethods.css';
@observer
export default class DappsMethods extends Component {
static propTypes = {
methodsStore: PropTypes.object.isRequired,
visibleStore: PropTypes.object.isRequired
};
render () {
const { methodsStore, visibleStore } = this.props;
if (!methodsStore.modalOpen) {
return null;
}
return (
<Portal
className={ styles.modal }
onClose={ methodsStore.closeModal }
open
title={
<FormattedMessage
id='dapps.methods.label'
defaultMessage='allowed methods'
/>
}
>
<table>
<thead>
<tr>
<th>&nbsp;</th>
{
methodsStore.filteredRequests.map((method, requestIndex) => (
<th key={ requestIndex }>
<div>
<span>{ method }</span>
</div>
</th>
))
}
</tr>
</thead>
<tbody>
{
visibleStore.visibleApps.map(({ id, name }, dappIndex) => (
<tr key={ dappIndex }>
<td>{ name }</td>
{
methodsStore.filteredRequests.map((method, requestIndex) => (
<td
className={ styles.check }
key={ `${dappIndex}_${requestIndex}` }
>
<MethodCheck
checked={ methodsStore.permissions[`${method}:${id}`] || false }
dappId={ id }
method={ method }
onToggle={ methodsStore.toggleMethodPermission }
/>
</td>
))
}
</tr>
))
}
</tbody>
</table>
</Portal>
);
}
}

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 './dappMethods';

View File

@ -0,0 +1,59 @@
// 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, observable } from 'mobx';
import filteredRequests from '../DappRequests/filteredRequests';
export default class Store {
@observable filteredRequests = Object.keys(filteredRequests);
@observable modalOpen = false;
@observable permissions = {};
@action closeModal = () => {
this.modalOpen = false;
}
@action openModal = () => {
this.modalOpen = true;
}
@action addMethodPermission = (method, token) => {
const id = `${method}:${token}`;
this.permissions = Object.assign({}, this.permissions, {
[id]: true
});
}
@action toggleMethodPermission = (method, token) => {
const id = `${method}:${token}`;
this.permissions = Object.assign({}, this.permissions, {
[id]: !this.permissions[id]
});
}
static instance = null;
static get () {
if (!Store.instance) {
Store.instance = new Store();
}
return Store.instance;
}
}

View File

@ -27,8 +27,8 @@ export default function Request ({ className, approveRequest, denyRequest, queue
return ( return (
<div className={ className }> <div className={ className }>
<FormattedMessage <FormattedMessage
id='dappFilter.request.info' id='dappRequests.request.info'
defaultMessage='Received request from {method} from {from}' defaultMessage='Received request for {method} from {from}'
values={ { values={ {
from, from,
method method
@ -37,7 +37,7 @@ export default function Request ({ className, approveRequest, denyRequest, queue
<Button <Button
label={ label={
<FormattedMessage <FormattedMessage
id='dappFilter.request.buttons.approve' id='dappRequests.request.buttons.approve'
defaultMessage='Approve' defaultMessage='Approve'
/> />
} }
@ -46,7 +46,7 @@ export default function Request ({ className, approveRequest, denyRequest, queue
<Button <Button
label={ label={
<FormattedMessage <FormattedMessage
id='dappFilter.request.buttons.approveAll' id='dappRequests.request.buttons.approveAll'
defaultMessage='Approve All' defaultMessage='Approve All'
/> />
} }
@ -55,7 +55,7 @@ export default function Request ({ className, approveRequest, denyRequest, queue
<Button <Button
label={ label={
<FormattedMessage <FormattedMessage
id='dappFilter.request.buttons.reject' id='dappRequests.request.buttons.reject'
defaultMessage='Reject' defaultMessage='Reject'
/> />
} }

View File

@ -15,7 +15,7 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.filter { .requests {
background: #f80; background: #f80;
color: white; color: white;
bottom: 0; bottom: 0;

View File

@ -19,9 +19,9 @@ import React from 'react';
import Request from './Request'; import Request from './Request';
import Store from './store'; import Store from './store';
import styles from './dappFilter.css'; import styles from './dappRequests.css';
function DappFilter () { function DappRequests () {
const store = Store.get(); const store = Store.get();
if (!store || !store.hasRequests) { if (!store || !store.hasRequests) {
@ -29,9 +29,9 @@ function DappFilter () {
} }
return ( return (
<div className={ styles.filter }> <div className={ styles.requests }>
{ {
store.requests.map(({ queueId, request: { data } }) => ( store.squashedRequests.map(({ queueId, request: { data } }) => (
<Request <Request
className={ styles.request } className={ styles.request }
approveRequest={ store.approveRequest } approveRequest={ store.approveRequest }
@ -46,4 +46,4 @@ function DappFilter () {
); );
} }
export default observer(DappFilter); export default observer(DappRequests);

View File

@ -0,0 +1,19 @@
// 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 {
'parity_hashContent': {}
};

View File

@ -0,0 +1,23 @@
// 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 Store from './store';
export function setupProviderFilters (provider) {
return Store.create(provider);
}
export default from './dappRequests';

View File

@ -16,14 +16,16 @@
import { action, computed, observable } from 'mobx'; import { action, computed, observable } from 'mobx';
import filteredRequests from './filteredRequests';
import MethodsStore from '../DappMethods/store';
let nextQueueId = 0; let nextQueueId = 0;
export default class Store { export default class Store {
@observable permissions = []; @observable methodsStore = MethodsStore.get();
@observable requests = []; @observable requests = [];
constructor (provider, permissions) { constructor (provider) {
this.permissions = permissions;
this.provider = provider; this.provider = provider;
window.addEventListener('message', this.receiveMessage, false); window.addEventListener('message', this.receiveMessage, false);
@ -33,6 +35,21 @@ export default class Store {
return this.requests.length !== 0; return this.requests.length !== 0;
} }
@computed get squashedRequests () {
const duplicates = {};
return this.requests.filter(({ request: { data: { method, token } } }) => {
const id = `${token}:${method}`;
if (!duplicates[id]) {
duplicates[id] = true;
return true;
}
return false;
});
}
@action removeRequest = (_queueId) => { @action removeRequest = (_queueId) => {
this.requests = this.requests.filter(({ queueId }) => queueId !== _queueId); this.requests = this.requests.filter(({ queueId }) => queueId !== _queueId);
} }
@ -55,7 +72,8 @@ export default class Store {
const { request: { data: { method, token } } } = queued; const { request: { data: { method, token } } } = queued;
const requests = this.findMatchingRequests(method, token); const requests = this.findMatchingRequests(method, token);
this.addTokenPermission(method, token); // TODO: Use single-use token, map back to app name
this.methodsStore.addMethodPermission(method, token);
requests.forEach(this.approveSingleRequest); requests.forEach(this.approveSingleRequest);
} else { } else {
this.approveSingleRequest(queued); this.approveSingleRequest(queued);
@ -75,14 +93,6 @@ export default class Store {
}, '*'); }, '*');
} }
@action addTokenPermission = (method, token) => {
this.permissions.tokens[token] = Object.assign({ [method]: true }, this.permissions.tokens[token] || {});
}
@action setPermissions = (permissions) => {
this.permissions = permissions;
}
findRequest (_queueId) { findRequest (_queueId) {
return this.requests.find(({ queueId }) => queueId === _queueId); return this.requests.find(({ queueId }) => queueId === _queueId);
} }
@ -112,20 +122,25 @@ export default class Store {
return; return;
} }
if (this.permissions.filtered.includes(method)) { const filterId = `${method}:${token}`;
if (!this.permissions.tokens[token] || !this.permissions.tokens[token][method]) {
// TODO: Use single-use token, map back to app name
if (filteredRequests[method] && !this.methodsStore.permissions[filterId]) {
this.queueRequest({ data, origin, source }); this.queueRequest({ data, origin, source });
return; return;
} }
}
this.executeOnProvider(data, source); this.executeOnProvider(data, source);
} }
static instance = null; static instance = null;
static create (provider, permissions) { static create (provider) {
Store.instance = new Store(provider, permissions); if (!Store.instance) {
Store.instance = new Store(provider, {});
}
return Store.instance;
} }
static get () { static get () {

View File

@ -21,11 +21,13 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Actionbar, Button, Checkbox, DappCard, Page, SectionList } from '@parity/ui'; import { Actionbar, Button, Checkbox, DappCard, Page, SectionList } from '@parity/ui';
import { LockedIcon, VisibleIcon } from '@parity/ui/Icons'; import { LockedIcon, MethodsIcon, VisibleIcon } from '@parity/ui/Icons';
import DappsVisible from '../DappsVisible';
import DappAccounts from '../DappAccounts'; import DappAccounts from '../DappAccounts';
import PermissionStore from '../DappAccounts/store'; import PermissionStore from '../DappAccounts/store';
import DappMethods from '../DappMethods';
import MethodsStore from '../DappMethods/store';
import DappsVisible from '../DappsVisible';
import DappsStore from './dappsStore'; import DappsStore from './dappsStore';
@ -43,6 +45,7 @@ class Dapps extends Component {
}; };
store = DappsStore.get(this.context.api); store = DappsStore.get(this.context.api);
methodsStore = MethodsStore.get();
permissionStore = new PermissionStore(this.context.api); permissionStore = new PermissionStore(this.context.api);
componentWillMount () { componentWillMount () {
@ -81,6 +84,10 @@ class Dapps extends Component {
return ( return (
<div> <div>
<DappAccounts permissionStore={ this.permissionStore } /> <DappAccounts permissionStore={ this.permissionStore } />
<DappMethods
methodsStore={ this.methodsStore }
visibleStore={ this.store }
/>
<DappsVisible store={ this.store } /> <DappsVisible store={ this.store } />
<Actionbar <Actionbar
className={ styles.toolbar } className={ styles.toolbar }
@ -97,7 +104,7 @@ class Dapps extends Component {
label={ label={
<FormattedMessage <FormattedMessage
id='dapps.button.edit' id='dapps.button.edit'
defaultMessage='edit' defaultMessage='edit applications'
/> />
} }
onClick={ this.store.openModal } onClick={ this.store.openModal }
@ -108,10 +115,21 @@ class Dapps extends Component {
label={ label={
<FormattedMessage <FormattedMessage
id='dapps.button.accounts' id='dapps.button.accounts'
defaultMessage='visible accounts' defaultMessage='allowed accounts'
/> />
} }
onClick={ this.openPermissionsModal } onClick={ this.openPermissionsModal }
/>,
<Button
icon={ <MethodsIcon /> }
key='methods'
label={
<FormattedMessage
id='dapps.button.methods'
defaultMessage='allowed methods'
/>
}
onClick={ this.methodsStore.openModal }
/> />
] } ] }
/> />

View File

@ -41,7 +41,7 @@ import SecureApi from '~/secureApi';
import Application from './Application'; import Application from './Application';
import Dapp from './Dapp'; import Dapp from './Dapp';
import { Store as DappFilterStore } from './DappFilter'; import { setupProviderFilters } from './DappRequests';
import Dapps from './Dapps'; import Dapps from './Dapps';
injectTapEventPlugin(); injectTapEventPlugin();
@ -67,12 +67,7 @@ const api = new SecureApi(uiUrl, token);
patchApi(api); patchApi(api);
ContractInstances.get(api); ContractInstances.get(api);
DappFilterStore.create(api.provider, { setupProviderFilters(api.provider);
filtered: [
'parity_hashContent'
],
tokens: {}
});
const store = initStore(api, hashHistory); const store = initStore(api, hashHistory);

View File

@ -25,6 +25,8 @@ export const AttachFileIcon = (props) => <Icon name='attach' { ...props } />;
export const BackgroundIcon = (props) => <Icon name='image' { ...props } />; export const BackgroundIcon = (props) => <Icon name='image' { ...props } />;
export const CancelIcon = (props) => <Icon name='cancel' { ...props } />; export const CancelIcon = (props) => <Icon name='cancel' { ...props } />;
export const CheckIcon = (props) => <Icon name='check' { ...props } />; export const CheckIcon = (props) => <Icon name='check' { ...props } />;
export const CheckboxTickedIcon = (props) => <Icon name='check circle outline' { ...props } />;
export const CheckboxUntickedIcon = (props) => <Icon name='radio' { ...props } />;
export const CloseIcon = (props) => <Icon name='close' { ...props } />; export const CloseIcon = (props) => <Icon name='close' { ...props } />;
export const CompareIcon = (props) => <Icon name='exchange' { ...props } />; export const CompareIcon = (props) => <Icon name='exchange' { ...props } />;
export const ComputerIcon = (props) => <Icon name='desktop' { ...props } />; export const ComputerIcon = (props) => <Icon name='desktop' { ...props } />;
@ -51,6 +53,7 @@ export const LinkIcon = (props) => <Icon name='linkify' { ...props } />;
export const ListIcon = (props) => <Icon name='list ul' { ...props } />; export const ListIcon = (props) => <Icon name='list ul' { ...props } />;
export const LockedIcon = (props) => <Icon name='unlock alternate' { ...props } />; export const LockedIcon = (props) => <Icon name='unlock alternate' { ...props } />;
export const MembershipIcon = (props) => <Icon name='id card outline' { ...props } />; export const MembershipIcon = (props) => <Icon name='id card outline' { ...props } />;
export const MethodsIcon = (props) => <Icon name='map signs' { ...props } />;
export const MoveIcon = (props) => <Icon name='move' { ...props } />; export const MoveIcon = (props) => <Icon name='move' { ...props } />;
export const NextIcon = (props) => <Icon name='chevron right' { ...props } />; export const NextIcon = (props) => <Icon name='chevron right' { ...props } />;
export const PauseIcon = (props) => <Icon name='pause' { ...props } />; export const PauseIcon = (props) => <Icon name='pause' { ...props } />;

View File

@ -31,7 +31,7 @@
"react-copy-to-clipboard": "4.2.3", "react-copy-to-clipboard": "4.2.3",
"react-datetime": "2.8.10", "react-datetime": "2.8.10",
"react-dropzone": "3.7.3", "react-dropzone": "3.7.3",
"react-event-listener": "0.4.1", "react-event-listener": "^0.4.0",
"react-portal": "3.0.0", "react-portal": "3.0.0",
"react-qr-reader": "1.0.3", "react-qr-reader": "1.0.3",
"recharts": "0.15.2", "recharts": "0.15.2",