diff --git a/js/package.json b/js/package.json index 8a573c7da..ab56e4035 100644 --- a/js/package.json +++ b/js/package.json @@ -102,6 +102,7 @@ "postcss-nested": "^1.0.0", "postcss-simple-vars": "^3.0.0", "raw-loader": "^0.5.1", + "react-addons-perf": "~15.3.2", "react-addons-test-utils": "~15.3.2", "react-copy-to-clipboard": "^4.2.3", "react-dom": "~15.3.2", diff --git a/js/src/index.js b/js/src/index.js index fda785842..0e0433c1e 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -46,6 +46,12 @@ import './index.html'; injectTapEventPlugin(); +if (process.env.NODE_ENV === 'development') { + // Expose the React Performance Tools on the`window` object + const Perf = require('react-addons-perf'); + window.Perf = Perf; +} + const AUTH_HASH = '#/auth?'; const parityUrl = process.env.PARITY_URL || ( diff --git a/js/src/ui/ParityBackground/parityBackground.js b/js/src/ui/ParityBackground/parityBackground.js index 0916d3a85..5198195c0 100644 --- a/js/src/ui/ParityBackground/parityBackground.js +++ b/js/src/ui/ParityBackground/parityBackground.js @@ -16,26 +16,17 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; class ParityBackground extends Component { - static contextTypes = { - muiTheme: PropTypes.object.isRequired - } - static propTypes = { + style: PropTypes.object.isRequired, children: PropTypes.node, className: PropTypes.string, - gradient: PropTypes.string, - seed: PropTypes.any, - settings: PropTypes.object.isRequired, onClick: PropTypes.func - } + }; render () { - const { muiTheme } = this.context; - const { children, className, gradient, seed, settings, onClick } = this.props; - const style = muiTheme.parity.getBackgroundStyle(gradient, seed || settings.backgroundSeed); + const { children, className, style, onClick } = this.props; return (
{ + const { backgroundSeed } = state.settings; + const { seed } = props; + + const newSeed = seed || backgroundSeed; + + if (newSeed === _seed) { + return _props; + } + + _seed = newSeed; + _props = { style: muiTheme.parity.getBackgroundStyle(gradient, newSeed) }; + + return _props; + }; } export default connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps )(ParityBackground); diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 5a96f64ff..88249bb1c 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -16,6 +16,7 @@ import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; +import { isEqual } from 'lodash'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '../../../ui'; @@ -30,7 +31,6 @@ export default class Summary extends Component { link: PropTypes.string, name: PropTypes.string, noLink: PropTypes.bool, - children: PropTypes.node, handleAddSearchToken: PropTypes.func }; @@ -42,8 +42,42 @@ export default class Summary extends Component { name: 'Unnamed' }; + shouldComponentUpdate (nextProps) { + const prev = { + link: this.props.link, name: this.props.name, + noLink: this.props.noLink, + meta: this.props.account.meta, address: this.props.account.address + }; + + const next = { + link: nextProps.link, name: nextProps.name, + noLink: nextProps.noLink, + meta: nextProps.account.meta, address: nextProps.account.address + }; + + if (!isEqual(next, prev)) { + return true; + } + + const prevTokens = this.props.balance.tokens || []; + const nextTokens = nextProps.balance.tokens || []; + + if (prevTokens.length !== nextTokens.length) { + return true; + } + + const prevValues = prevTokens.map((t) => t.value.toNumber()); + const nextValues = nextTokens.map((t) => t.value.toNumber()); + + if (!isEqual(prevValues, nextValues)) { + return true; + } + + return false; + } + render () { - const { account, children, handleAddSearchToken } = this.props; + const { account, handleAddSearchToken } = this.props; const { tags } = account.meta; if (!account) { @@ -71,7 +105,6 @@ export default class Summary extends Component { byline={ addressComponent } /> { this.renderBalance() } - { children } ); } diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index f98a09ea3..0ed8b5256 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -30,3 +30,25 @@ right: 1em; top: 4em; } + +.loadings { + display: flex; + flex-wrap: wrap; + + .loading { + flex: 0 1 50%; + width: 50%; + height: 150px; + display: flex; + padding: 0.25em; + box-sizing: border-box; + + > div { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.8); + } + } +} diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index 0e7cd0500..0075a15a2 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -42,33 +42,63 @@ class Accounts extends Component { newDialog: false, sortOrder: '', searchValues: [], - searchTokens: [] + searchTokens: [], + show: false + } + + componentWillMount () { + window.setTimeout(() => { + this.setState({ show: true }); + }, 100); } render () { - const { accounts, hasAccounts, balances } = this.props; - const { searchValues, sortOrder } = this.state; - return (
{ this.renderNewDialog() } { this.renderActionbar() } - - - - + + { this.state.show ? this.renderAccounts() : this.renderLoading() }
); } + renderLoading () { + const { accounts } = this.props; + + const loadings = ((accounts && Object.keys(accounts)) || []).map((_, idx) => ( +
+
+
+ )); + + return ( +
+ { loadings } +
+ ); + } + + renderAccounts () { + const { accounts, hasAccounts, balances } = this.props; + const { searchValues, sortOrder } = this.state; + + return ( + + + + + ); + } + renderSearchButton () { const onChange = (searchTokens, searchValues) => { this.setState({ searchTokens, searchValues }); diff --git a/js/src/views/Application/Container/container.js b/js/src/views/Application/Container/container.js index a1b9124c7..d3908f570 100644 --- a/js/src/views/Application/Container/container.js +++ b/js/src/views/Application/Container/container.js @@ -22,6 +22,10 @@ import { Errors, ParityBackground, Tooltips } from '../../../ui'; import styles from '../application.css'; export default class Container extends Component { + static contextTypes = { + muiTheme: PropTypes.object.isRequired + }; + static propTypes = { children: PropTypes.node.isRequired, showFirstRun: PropTypes.bool, @@ -30,9 +34,10 @@ export default class Container extends Component { render () { const { children, showFirstRun, onCloseFirstRun } = this.props; + const { muiTheme } = this.context; return ( - + diff --git a/js/src/views/Application/TabBar/tabBar.css b/js/src/views/Application/TabBar/tabBar.css index b855d4298..8ee1254f8 100644 --- a/js/src/views/Application/TabBar/tabBar.css +++ b/js/src/views/Application/TabBar/tabBar.css @@ -23,6 +23,11 @@ .tabs { width: 100%; position: relative; + display: flex; + + & > * { + flex: 1; + } } .tabs button, @@ -38,6 +43,7 @@ button.tabactive, button.tabactive:hover { + color: white !important; background: rgba(0, 0, 0, 0.25) !important; border-radius: 4px 4px 0 0; } diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index 01ca4df0d..62f02b43b 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; -import { Tabs, Tab } from 'material-ui/Tabs'; +import { Tab as MUITab } from 'material-ui/Tabs'; import { Badge, Tooltip } from '../../../ui'; @@ -33,20 +33,138 @@ const TABMAP = { deploy: 'contract' }; +class Tab extends Component { + static propTypes = { + active: PropTypes.bool, + view: PropTypes.object, + children: PropTypes.node, + pendings: PropTypes.number, + onChange: PropTypes.func + }; + + shouldComponentUpdate (nextProps) { + return nextProps.active !== this.props.active || + (nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings); + } + + render () { + const { active, view, children } = this.props; + + const label = this.getLabel(view); + + return ( + + { children } + + ); + } + + getLabel (view) { + const { label } = view; + + if (view.id === 'signer') { + return this.renderSignerLabel(label); + } + + if (view.id === 'status') { + return this.renderStatusLabel(label); + } + + return this.renderLabel(label); + } + + renderLabel (name, bubble) { + return ( +
+ { name } + { bubble } +
+ ); + } + + renderSignerLabel (label) { + const { pendings } = this.props; + + if (pendings) { + const bubble = ( + + ); + + return this.renderLabel(label, bubble); + } + + return this.renderLabel(label); + } + + renderStatusLabel (label) { + // const { isTest, netChain } = this.props; + // const bubble = ( + // + // ); + + return this.renderLabel(label, null); + } + + handleClick = () => { + const { onChange, view } = this.props; + onChange(view); + } +} + class TabBar extends Component { static contextTypes = { router: PropTypes.object.isRequired - } + }; static propTypes = { + views: PropTypes.array.isRequired, + hash: PropTypes.string.isRequired, pending: PropTypes.array, isTest: PropTypes.bool, - netChain: PropTypes.string, - settings: PropTypes.object.isRequired - } + netChain: PropTypes.string + }; + + static defaultProps = { + pending: [] + }; state = { - activeRoute: '/accounts' + activeViewId: '' + }; + + setActiveView (props = this.props) { + const { hash, views } = props; + const view = views.find((view) => view.value === hash); + + this.setState({ activeViewId: view.id }); + } + + componentWillMount () { + this.setActiveView(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.hash !== this.props.hash) { + this.setActiveView(nextProps); + } + } + + shouldComponentUpdate (nextProps, nextState) { + return (nextProps.hash !== this.props.hash) || + (nextProps.pending.length !== this.props.pending.length) || + (nextState.activeViewId !== this.state.activeViewId); } render () { @@ -81,100 +199,64 @@ class TabBar extends Component { } renderTabs () { - const { settings } = this.props; - const windowHash = (window.location.hash || '').split('?')[0].split('/')[1]; - const hash = TABMAP[windowHash] || windowHash; + const { views, pending } = this.props; + const { activeViewId } = this.state; - const items = Object.keys(settings.views) - .filter((id) => settings.views[id].fixed || settings.views[id].active) - .map((id) => { - const view = settings.views[id]; - let label = this.renderLabel(view.label); - let body = null; + const items = views + .map((view, index) => { + const body = (view.id === 'accounts') + ? ( + + ) + : null; - if (id === 'accounts') { - body = ( - - ); - } else if (id === 'signer') { - label = this.renderSignerLabel(label); - } else if (id === 'status') { - label = this.renderStatusLabel(label); - } + const active = activeViewId === view.id; return ( + active={ active } + view={ view } + onChange={ this.onChange } + key={ index } + pendings={ pending.length } + > { body } ); }); return ( - + onChange={ this.onChange }> { items } - - ); - } - - renderLabel = (name, bubble) => { - return ( -
- { name } - { bubble }
); } - renderSignerLabel = (label) => { - const { pending } = this.props; - let bubble = null; - - if (pending && pending.length) { - bubble = ( - - ); - } - - return this.renderLabel(label, bubble); - } - - renderStatusLabel = (label) => { - // const { isTest, netChain } = this.props; - // const bubble = ( - // - // ); - - return this.renderLabel(label, null); - } - - onActivate = (activeRoute) => { + onChange = (view) => { const { router } = this.context; - return (event) => { - router.push(activeRoute); - this.setState({ activeRoute }); - }; + router.push(view.route); + this.setState({ activeViewId: view.id }); } } function mapStateToProps (state) { - const { settings } = state; + const { views } = state.settings; - return { settings }; + const filteredViews = Object + .keys(views) + .filter((id) => views[id].fixed || views[id].active) + .map((id) => ({ + ...views[id], + id + })); + + const windowHash = (window.location.hash || '').split('?')[0].split('/')[1]; + const hash = TABMAP[windowHash] || windowHash; + + return { views: filteredViews, hash }; } function mapDispatchToProps (dispatch) { diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 0f3380ca0..40fe659ad 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -28,6 +28,10 @@ import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text import styles from './parityBar.css'; class ParityBar extends Component { + static contextTypes = { + muiTheme: PropTypes.object.isRequired + }; + static propTypes = { pending: PropTypes.array, dapp: PropTypes.bool @@ -62,6 +66,7 @@ class ParityBar extends Component { renderBar () { const { dapp } = this.props; + const { muiTheme } = this.context; if (!dapp) { return null; @@ -75,7 +80,7 @@ class ParityBar extends Component { return (
- +