Feature selector (#4074)

* WIP

* ParityBar verification

* import from index.js

* i18n expansion & tests

* Features component

* Adapt language selector to use features

* Add features to settings view

* typo

* Convert logging

* Fix earlier merge issues resulting in test failures

* Lint failure fixes (new rules)

* Fix additional listing rules

* Re-add FormattedMessage (missing after merge), fix tests

* Fix loader overrides

* grumble: split item rendering (& test)

* grumble: allow enable/disable while testing (default on)

* grumble: move LanguageSelector below Features

* grumble: don't pass visiblity prop (& update tests)

* grumble: missing observable (onClick misbehaving)

* grumble: don't reset to defaults per session

* Fix to single store instance
This commit is contained in:
Jaco Greeff 2017-01-24 17:20:10 +01:00 committed by GitHub
parent 5b2dd8deb2
commit 155bbc328f
24 changed files with 1319 additions and 175 deletions

27
js/src/i18n/constants.js Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2015, 2016 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/>.
const DEFAULT_LOCALE = 'en';
const DEFAULT_LOCALES = process.env.NODE_ENV === 'production'
? ['en']
: ['en', 'de'];
const LS_STORE_KEY = '_parity::locale';
export {
DEFAULT_LOCALE,
DEFAULT_LOCALES,
LS_STORE_KEY
};

View File

@ -0,0 +1,30 @@
// Copyright 2015, 2016 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 { DEFAULT_LOCALE } from './constants';
import languages from './languages';
const keys = Object.keys(languages);
describe('i18n/languages', () => {
it('has a language list', () => {
expect(keys.length > 1).to.be.true;
});
it('includes DEFAULT_LOCALE as a language', () => {
expect(keys.includes(DEFAULT_LOCALE)).to.be.true;
});
});

View File

@ -21,39 +21,32 @@ import de from 'react-intl/locale-data/de';
import en from 'react-intl/locale-data/en'; import en from 'react-intl/locale-data/en';
import store from 'store'; import store from 'store';
import { DEFAULT_LOCALE, DEFAULT_LOCALES, LS_STORE_KEY } from './constants';
import languages from './languages'; import languages from './languages';
import deMessages from './de'; import deMessages from './de';
import enMessages from './en'; import enMessages from './en';
const LS_STORE_KEY = '_parity::locale';
let instance = null; let instance = null;
const isProduction = process.env.NODE_ENV === 'production';
const DEFAULT = 'en';
const LANGUAGES = flatten({ languages }); const LANGUAGES = flatten({ languages });
const MESSAGES = { const MESSAGES = {
de: Object.assign(flatten(deMessages), LANGUAGES), de: Object.assign(flatten(deMessages), LANGUAGES),
en: Object.assign(flatten(enMessages), LANGUAGES) en: Object.assign(flatten(enMessages), LANGUAGES)
}; };
const LOCALES = isProduction
? ['en']
: ['en', 'de'];
addLocaleData([...de, ...en]); addLocaleData([...de, ...en]);
export default class Store { export default class Store {
@observable locale = DEFAULT; @observable locale = DEFAULT_LOCALE;
@observable locales = LOCALES; @observable locales = DEFAULT_LOCALES;
@observable messages = MESSAGES[DEFAULT]; @observable messages = MESSAGES[DEFAULT_LOCALE];
@observable isDevelopment = !isProduction;
constructor () { constructor () {
const savedLocale = store.get(LS_STORE_KEY); const savedLocale = store.get(LS_STORE_KEY);
this.locale = (savedLocale && LOCALES.includes(savedLocale)) this.locale = (savedLocale && DEFAULT_LOCALES.includes(savedLocale))
? savedLocale ? savedLocale
: DEFAULT; : DEFAULT_LOCALE;
this.messages = MESSAGES[this.locale]; this.messages = MESSAGES[this.locale];
} }

73
js/src/i18n/store.spec.js Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2015, 2016 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';
import { DEFAULT_LOCALE, DEFAULT_LOCALES, LS_STORE_KEY } from './constants';
import { LocaleStore } from './';
let localeStore;
describe('i18n/Store', () => {
before(() => {
localeStore = LocaleStore.get();
store.set(LS_STORE_KEY, 'testing');
});
it('creates a default instance', () => {
expect(localeStore).to.be.ok;
});
it('sets the default locale to default (invalid localStorage)', () => {
expect(localeStore.locale).to.equal(DEFAULT_LOCALE);
});
it('loads the locale from localStorage (valid localStorage)', () => {
const testLocale = DEFAULT_LOCALES[DEFAULT_LOCALES.length - 1];
store.set(LS_STORE_KEY, testLocale);
const testStore = new LocaleStore();
expect(testStore.locale).to.equal(testLocale);
});
it('lists the locales', () => {
expect(localeStore.locales.length > 1).to.be.true;
});
it('lists locals including default', () => {
expect(localeStore.locales.includes(DEFAULT_LOCALE)).to.be.true;
});
describe('@action', () => {
describe('setLocale', () => {
const testLocale = DEFAULT_LOCALES[DEFAULT_LOCALES.length - 1];
beforeEach(() => {
localeStore.setLocale(testLocale);
});
it('sets the locale as passed', () => {
expect(localeStore.locale).to.equal(testLocale);
});
it('sets the locale in localStorage', () => {
expect(store.get(LS_STORE_KEY)).to.equal(testLocale);
});
});
});
});

View File

@ -0,0 +1,21 @@
// Copyright 2015, 2016 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/>.
const LS_STORE_KEY = '_parity::features';
export {
LS_STORE_KEY
};

View File

@ -0,0 +1,61 @@
// Copyright 2015, 2016 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/>.
const MODES = {
DEVELOPMENT: 1000, // only in dev mode, disabled by default, can be toggled
TESTING: 1011, // feature is available in dev mode
PRODUCTION: 1022 // feature is available
};
const FEATURES = {
LANGUAGE: 'LANGUAGE',
LOGLEVELS: 'LOGLEVELS'
};
const DEFAULTS = {
[FEATURES.LANGUAGE]: {
mode: MODES.TESTING,
name: 'Language Selection',
description: 'Allows changing the default interface language'
},
[FEATURES.LOGLEVELS]: {
mode: MODES.TESTING,
name: 'Logging Level Selection',
description: 'Allows changing of the log levels for various components'
}
};
if (process.env.NODE_ENV === 'test') {
Object
.keys(MODES)
.forEach((mode) => {
const key = `.${mode}`;
FEATURES[key] = key;
DEFAULTS[key] = {
mode: MODES[mode],
name: key,
description: key
};
});
}
export default DEFAULTS;
export {
FEATURES,
MODES
};

View File

@ -0,0 +1,67 @@
// Copyright 2015, 2016 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 defaults, { FEATURES, MODES } from './defaults';
const features = Object.values(FEATURES);
const modes = Object.values(MODES);
describe('ui/Features/Defaults', () => {
describe('feature codes', () => {
Object.keys(FEATURES).forEach((key) => {
describe(key, () => {
let value;
beforeEach(() => {
value = FEATURES[key];
});
it('exists as an default', () => {
expect(defaults[value]).to.be.ok;
});
it('has a single unique code', () => {
expect(features.filter((code) => code === value).length).to.equal(1);
});
});
});
});
describe('defaults', () => {
Object.keys(defaults).forEach((key) => {
describe(key, () => {
let value;
beforeEach(() => {
value = defaults[key];
});
it('exists as an exposed feature', () => {
expect(features.includes(key)).to.be.ok;
});
it('has a valid mode', () => {
expect(modes.includes(value.mode)).to.be.true;
});
it('has a name and description', () => {
expect(value.description).to.be.ok;
expect(value.name).to.be.ok;
});
});
});
});
});

View File

@ -0,0 +1,20 @@
/* Copyright 2015, 2016 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/>.
*/
.description {
opacity: 0.75;
}

View File

@ -0,0 +1,69 @@
// Copyright 2015, 2016 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 { Checkbox } from 'material-ui';
import { observer } from 'mobx-react';
import { List, ListItem } from 'material-ui/List';
import React, { Component } from 'react';
import defaults, { MODES } from './defaults';
import Store from './store';
import styles from './features.css';
@observer
export default class Features extends Component {
store = Store.get();
render () {
if (process.env.NODE_ENV === 'production') {
return null;
}
return (
<List>
{
Object
.keys(defaults)
.filter((key) => defaults[key].mode !== MODES.PRODUCTION)
.map(this.renderItem)
}
</List>
);
}
renderItem = (key) => {
const feature = defaults[key];
const onCheck = () => this.store.toggleActive(key);
return (
<ListItem
key={ `feature_${key}` }
leftCheckbox={
<Checkbox
checked={ this.store.active[key] }
onCheck={ onCheck }
/>
}
primaryText={ feature.name }
secondaryText={
<div className={ styles.description }>
{ feature.description }
</div>
}
/>
);
}
}

View File

@ -0,0 +1,89 @@
// Copyright 2015, 2016 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 defaults, { MODES } from './defaults';
import Features from './';
let component;
let instance;
function render (props = { visible: true }) {
component = shallow(
<Features { ...props } />
);
instance = component.instance();
return component;
}
describe('views/Settings/Features', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('visibility', () => {
let oldEnv;
beforeEach(() => {
oldEnv = process.env.NODE_ENV;
});
afterEach(() => {
process.env.NODE_ENV = oldEnv;
});
it('renders null when NODE_ENV === production', () => {
process.env.NODE_ENV = 'production';
render();
expect(component.get(0)).to.be.null;
});
it('renders component when NODE_ENV !== production', () => {
process.env.NODE_ENV = 'development';
render();
expect(component.get(0)).not.to.be.null;
});
});
describe('instance methods', () => {
describe('renderItem', () => {
const keys = Object.keys(defaults).filter((key) => defaults[key].mode !== MODES.PRODUCTION);
const key = keys[0];
let item;
beforeEach(() => {
item = instance.renderItem(key);
});
it('renders an item', () => {
expect(item).not.to.be.null;
});
it('displays the correct name', () => {
expect(item.props.primaryText).to.equal(defaults[key].name);
});
});
});
});

View File

@ -0,0 +1,25 @@
// Copyright 2015, 2016 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 { FEATURES } from './defaults';
import FeaturesStore from './store';
export default from './features';
export {
FEATURES,
FeaturesStore
};

View File

@ -0,0 +1,73 @@
// Copyright 2015, 2016 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 store from 'store';
import { LS_STORE_KEY } from './constants';
import defaults, { FEATURES, MODES } from './defaults';
const isProductionMode = process.env.NODE_ENV === 'production';
let instance = null;
export default class Store {
@observable active = {};
constructor () {
this.loadActiveFeatures();
}
@action setActiveFeatures = (features = {}, isProduction) => {
this.active = Object.assign({}, this.getDefaultActive(isProduction), features);
}
@action toggleActive = (featureKey) => {
this.active = Object.assign({}, this.active, { [featureKey]: !this.active[featureKey] });
this.saveActiveFeatures();
}
getDefaultActive (isProduction = isProductionMode) {
const modesTest = [MODES.PRODUCTION];
if (!isProduction) {
modesTest.push(MODES.TESTING);
}
return Object
.keys(FEATURES)
.reduce((visibility, feature) => {
visibility[feature] = modesTest.includes(defaults[feature].mode);
return visibility;
}, {});
}
loadActiveFeatures () {
this.setActiveFeatures(store.get(LS_STORE_KEY));
}
saveActiveFeatures () {
store.set(LS_STORE_KEY, this.active);
}
static get () {
if (!instance) {
instance = new Store();
}
return instance;
}
}

View File

@ -0,0 +1,96 @@
// Copyright 2015, 2016 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 lstore from 'store';
import { LS_STORE_KEY } from './constants';
import defaults, { MODES } from './defaults';
import Store from './store';
let store;
function createStore () {
store = new Store();
return store;
}
describe('ui/Features/Store', () => {
beforeEach(() => {
lstore.set(LS_STORE_KEY, { 'testingFromStorage': true });
createStore();
});
it('loads with values from localStorage', () => {
expect(store.active.testingFromStorage).to.be.true;
});
describe('@action', () => {
describe('setActiveFeatures', () => {
it('sets the active features', () => {
store.setActiveFeatures({ 'testing': true });
expect(store.active.testing).to.be.true;
});
it('overrides the defaults', () => {
store.setActiveFeatures({ '.PRODUCTION': false });
expect(store.active['.PRODUCTION']).to.be.false;
});
});
describe('toggleActive', () => {
it('changes the state of a feature', () => {
expect(store.active['.PRODUCTION']).to.be.true;
store.toggleActive('.PRODUCTION');
expect(store.active['.PRODUCTION']).to.be.false;
});
it('saves the updated state to localStorage', () => {
store.toggleActive('.PRODUCTION');
expect(lstore.get(LS_STORE_KEY)).to.deep.equal(store.active);
});
});
});
describe('operations', () => {
describe('getDefaultActive', () => {
it('returns features where mode === TESTING|PRODUCTION (non-production)', () => {
const visibility = store.getDefaultActive(false);
expect(
Object
.keys(visibility)
.filter((key) => visibility[key])
.filter((key) => ![MODES.TESTING, MODES.PRODUCTION].includes(defaults[key].mode))
.length
).to.equal(0);
});
it('returns features where mode === PRODUCTION (production)', () => {
const visibility = store.getDefaultActive(true);
expect(
Object
.keys(visibility)
.filter((key) => visibility[key])
.filter((key) => ![MODES.PRODUCTION].includes(defaults[key].mode))
.length
).to.equal(0);
});
});
});
});

View File

@ -14,20 +14,23 @@
// 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/>.
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { LocaleStore } from '~/i18n';
import { FeaturesStore, FEATURES } from '../Features';
import Select from '../Form/Select'; import Select from '../Form/Select';
import { LocaleStore } from '../../i18n';
@observer @observer
export default class LanguageSelector extends Component { export default class LanguageSelector extends Component {
features = FeaturesStore.get();
store = LocaleStore.get(); store = LocaleStore.get();
render () { render () {
if (!this.store.isDevelopment) { if (!this.features.active[FEATURES.LANGUAGE]) {
return null; return null;
} }
@ -70,6 +73,6 @@ export default class LanguageSelector extends Component {
} }
onChange = (event, index, locale) => { onChange = (event, index, locale) => {
this.store.setLocale(locale); this.store.setLocale(locale || event.target.value);
} }
} }

View File

@ -0,0 +1,69 @@
// Copyright 2015, 2016 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 { LocaleStore } from '~/i18n';
import LanguageSelector from './';
let component;
function render (props = {}) {
component = shallow(
<LanguageSelector { ...props } />
);
return component;
}
describe('LanguageSelector', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('Select', () => {
let select;
let localeStore;
beforeEach(() => {
localeStore = LocaleStore.get();
sinon.stub(localeStore, 'setLocale');
render();
select = component.find('Select');
});
afterEach(() => {
localeStore.setLocale.restore();
});
it('renders the Select', () => {
expect(select).to.have.length(1);
});
it('has locale items', () => {
expect(select.find('MenuItem').length > 0).to.be.true;
});
it('calls localeStore.setLocale when changed', () => {
select.simulate('change', { target: { value: 'de' } });
expect(localeStore.setLocale).to.have.been.calledWith('de');
});
});
});

View File

@ -31,6 +31,7 @@ import CopyToClipboard from './CopyToClipboard';
import CurrencySymbol from './CurrencySymbol'; import CurrencySymbol from './CurrencySymbol';
import Editor from './Editor'; import Editor from './Editor';
import Errors from './Errors'; import Errors from './Errors';
import Features, { FEATURES, FeaturesStore } from './Features';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import GasPriceEditor from './GasPriceEditor'; import GasPriceEditor from './GasPriceEditor';
import GasPriceSelector from './GasPriceSelector'; import GasPriceSelector from './GasPriceSelector';
@ -74,6 +75,9 @@ export {
CurrencySymbol, CurrencySymbol,
Editor, Editor,
Errors, Errors,
FEATURES,
Features,
FeaturesStore,
Form, Form,
FormWrap, FormWrap,
GasPriceEditor, GasPriceEditor,

View File

@ -16,17 +16,18 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import store from 'store'; import store from 'store';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; import { CancelIcon, FingerprintIcon } from '~/ui/Icons';
import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui';
import { Embedded as Signer } from '../Signer'; import { Embedded as Signer } from '../Signer';
import DappsStore from '~/views/Dapps/dappsStore'; import DappsStore from '~/views/Dapps/dappsStore';
import imagesEthcoreBlock from '!url-loader!../../../assets/images/parity-logo-white-no-text.svg';
import styles from './parityBar.css'; import styles from './parityBar.css';
const LS_STORE_KEY = '_parity::parityBar'; const LS_STORE_KEY = '_parity::parityBar';
@ -112,10 +113,6 @@ class ParityBar extends Component {
render () { render () {
const { moving, opened, position } = this.state; const { moving, opened, position } = this.state;
const content = opened
? this.renderExpanded()
: this.renderBar();
const containerClassNames = opened const containerClassNames = opened
? [ styles.overlay ] ? [ styles.overlay ]
: [ styles.bar ]; : [ styles.bar ];
@ -124,11 +121,12 @@ class ParityBar extends Component {
containerClassNames.push(styles.moving); containerClassNames.push(styles.moving);
} }
const parityBgClassName = opened const parityBgClassNames = [
? styles.expanded opened
: styles.corner; ? styles.expanded
: styles.corner,
const parityBgClassNames = [ parityBgClassName, styles.parityBg ]; styles.parityBg
];
if (moving) { if (moving) {
parityBgClassNames.push(styles.moving); parityBgClassNames.push(styles.moving);
@ -169,7 +167,11 @@ class ParityBar extends Component {
ref='container' ref='container'
style={ parityBgStyle } style={ parityBgStyle }
> >
{ content } {
opened
? this.renderExpanded()
: this.renderBar()
}
</ParityBackground> </ParityBackground>
</div> </div>
); );
@ -182,34 +184,38 @@ class ParityBar extends Component {
return null; return null;
} }
const parityIcon = (
<img
src={ imagesEthcoreBlock }
className={ styles.parityIcon }
/>
);
const parityButton = (
<Button
className={ styles.parityButton }
icon={ parityIcon }
label={ this.renderLabel('Parity') }
/>
);
return ( return (
<div <div
className={ styles.cornercolor } className={ styles.cornercolor }
ref={ this.onRef } ref={ this.onRef }
> >
{ this.renderLink(parityButton) } {
this.renderLink(
<Button
className={ styles.parityButton }
icon={
<img
className={ styles.parityIcon }
src={ imagesEthcoreBlock }
/>
}
label={
this.renderLabel(
<FormattedMessage
id='parityBar.label.parity'
defaultMessage='Parity'
/>
)
}
/>
)
}
<Button <Button
className={ styles.button } className={ styles.button }
icon={ <FingerprintIcon /> } icon={ <FingerprintIcon /> }
label={ this.renderSignerLabel() } label={ this.renderSignerLabel() }
onClick={ this.toggleDisplay } onClick={ this.toggleDisplay }
/> />
{ this.renderDrag() } { this.renderDrag() }
</div> </div>
); );
@ -307,7 +313,13 @@ class ParityBar extends Component {
); );
} }
return this.renderLabel('Signer', bubble); return this.renderLabel(
<FormattedMessage
id='parityBar.label.signer'
defaultMessage='Signer'
/>,
bubble
);
} }
getHorizontal (x) { getHorizontal (x) {

View File

@ -0,0 +1,157 @@
// Copyright 2015, 2016 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 ParityBar from './';
let component;
let instance;
let store;
function createRedux (state = {}) {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => Object.assign({ signer: { pending: [] } }, state)
};
return store;
}
function render (props = {}, state = {}) {
component = shallow(
<ParityBar { ...props } />,
{
context: {
store: createRedux(state)
}
}
).find('ParityBar').shallow({ context: { api: {} } });
instance = component.instance();
return component;
}
describe('views/ParityBar', () => {
beforeEach(() => {
render({ dapp: true });
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('includes the ParityBackground', () => {
expect(component.find('Connect(ParityBackground)')).to.have.length(1);
});
describe('renderBar', () => {
let bar;
beforeEach(() => {
bar = shallow(instance.renderBar());
});
it('renders nothing when not overlaying a dapp', () => {
render({ dapp: false });
expect(instance.renderBar()).to.be.null;
});
it('renders when overlaying a dapp', () => {
expect(bar.find('div')).not.to.have.length(0);
});
it('renders the Parity button', () => {
const label = shallow(bar.find('Button').first().props().label);
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.parity');
});
it('renders the Signer button', () => {
const label = shallow(bar.find('Button').last().props().label);
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
});
});
describe('renderExpanded', () => {
let expanded;
beforeEach(() => {
expanded = shallow(instance.renderExpanded());
});
it('includes the Signer', () => {
expect(expanded.find('Connect(Embedded)')).to.have.length(1);
});
});
describe('renderLabel', () => {
it('renders the label name', () => {
expect(shallow(instance.renderLabel('testing', null)).text()).to.equal('testing');
});
it('renders name and bubble', () => {
expect(shallow(instance.renderLabel('testing', '(bubble)')).text()).to.equal('testing(bubble)');
});
});
describe('renderSignerLabel', () => {
let label;
beforeEach(() => {
label = shallow(instance.renderSignerLabel());
});
it('renders the signer label', () => {
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
});
it('does not render a badge when no pending requests', () => {
expect(label.find('Badge')).to.have.length(0);
});
it('renders a badge when pending requests', () => {
render({}, { signer: { pending: ['123', '456'] } });
expect(shallow(instance.renderSignerLabel()).find('Badge').props().value).to.equal(2);
});
});
describe('opened state', () => {
beforeEach(() => {
sinon.spy(instance, 'renderBar');
sinon.spy(instance, 'renderExpanded');
});
afterEach(() => {
instance.renderBar.restore();
instance.renderExpanded.restore();
});
it('renders the bar on with opened === false', () => {
expect(component.find('Link[to="/apps"]')).to.have.length(1);
});
it('renders expanded with opened === true', () => {
expect(instance.renderExpanded).not.to.have.been.called;
instance.setState({ opened: true });
expect(instance.renderExpanded).to.have.been.called;
});
});
});

View File

@ -14,67 +14,28 @@
// 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/>.
import { MenuItem } from 'material-ui';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { MenuItem } from 'material-ui';
import LogLevel from 'loglevel';
import { LOG_KEYS } from '~/config';
import { Select, Container, LanguageSelector } from '~/ui'; import { Select, Container, LanguageSelector } from '~/ui';
import Features, { FeaturesStore, FEATURES } from '~/ui/Features';
import Store, { LOGLEVEL_OPTIONS } from './store';
import layout from '../layout.css'; import layout from '../layout.css';
@observer
export default class Parity extends Component { export default class Parity extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
}; };
state = { store = new Store(this.context.api);
loglevels: {}, features = FeaturesStore.get();
mode: 'active',
selectValues: []
};
componentWillMount () { componentWillMount () {
this.loadMode(); return this.store.loadMode();
this.loadLogLevels();
this.setSelectValues();
}
loadLogLevels () {
if (process.env.NODE_ENV === 'production') {
return null;
}
const nextState = { ...this.state.logLevels };
Object.keys(LOG_KEYS).map((logKey) => {
const log = LOG_KEYS[logKey];
const logger = LogLevel.getLogger(log.key);
const level = logger.getLevel();
nextState[logKey] = { level, log };
});
this.setState({ logLevels: nextState });
}
setSelectValues () {
if (process.env.NODE_ENV === 'production') {
return null;
}
const selectValues = Object.keys(LogLevel.levels).map((levelName) => {
const value = LogLevel.levels[levelName];
return {
name: levelName,
value
};
});
this.setState({ selectValues });
} }
render () { render () {
@ -94,18 +55,30 @@ export default class Parity extends Component {
</div> </div>
</div> </div>
<div className={ layout.details }> <div className={ layout.details }>
<LanguageSelector />
{ this.renderModes() } { this.renderModes() }
<Features />
<LanguageSelector />
</div> </div>
</div> </div>
{ this.renderLogsConfig() } { this.renderLogsConfig() }
</Container> </Container>
); );
} }
renderItem (mode, label) {
return (
<MenuItem
key={ mode }
label={ label }
value={ mode }
>
{ label }
</MenuItem>
);
}
renderLogsConfig () { renderLogsConfig () {
if (process.env.NODE_ENV === 'production') { if (!this.features.active[FEATURES.LOGLEVELS]) {
return null; return null;
} }
@ -127,129 +100,89 @@ export default class Parity extends Component {
} }
renderLogsLevels () { renderLogsLevels () {
if (process.env.NODE_ENV === 'production') { const { logLevels } = this.store;
return null;
}
const { logLevels, selectValues } = this.state; return Object
.keys(logLevels)
.map((key) => {
const { level, log } = logLevels[key];
const { path, desc } = log;
return Object.keys(logLevels).map((logKey) => { const onChange = (_, index) => {
const { level, log } = logLevels[logKey]; this.store.updateLoggerLevel(path, Object.values(LOGLEVEL_OPTIONS)[index].value);
const { key, desc } = log; };
const onChange = (_, index) => { return (
const nextLevel = Object.values(selectValues)[index].value; <div key={ key }>
<p>{ desc }</p>
LogLevel.getLogger(key).setLevel(nextLevel); <Select
this.loadLogLevels(); onChange={ onChange }
}; value={ level }
values={ LOGLEVEL_OPTIONS }
return ( />
<div key={ logKey }> </div>
<p>{ desc }</p> );
<Select });
onChange={ onChange }
value={ level }
values={ selectValues }
/>
</div>
);
});
} }
renderModes () { renderModes () {
const { mode } = this.state; const { mode } = this.store;
const renderItem = (mode, label) => {
return (
<MenuItem
key={ mode }
value={ mode }
label={ label }
>
{ label }
</MenuItem>
);
};
return ( return (
<Select <Select
label={ id='parityModeSelect'
<FormattedMessage
id='settings.parity.modes.label'
defaultMessage='mode of operation'
/>
}
hint={ hint={
<FormattedMessage <FormattedMessage
id='settings.parity.modes.hint' id='settings.parity.modes.hint'
defaultMessage='the syning mode for the Parity node' defaultMessage='the syning mode for the Parity node'
/> />
} }
value={ mode } label={
<FormattedMessage
id='settings.parity.modes.label'
defaultMessage='mode of operation'
/>
}
onChange={ this.onChangeMode } onChange={ this.onChangeMode }
value={ mode }
> >
{ {
renderItem('active', this.renderItem('active', (
<FormattedMessage <FormattedMessage
id='settings.parity.modes.mode_active' id='settings.parity.modes.mode_active'
defaultMessage='Parity continuously syncs the chain' defaultMessage='Parity continuously syncs the chain'
/> />
) ))
} }
{ {
renderItem('passive', this.renderItem('passive', (
<FormattedMessage <FormattedMessage
id='settings.parity.modes.mode_passive' id='settings.parity.modes.mode_passive'
defaultMessage='Parity syncs initially, then sleeps and wakes regularly to resync' defaultMessage='Parity syncs initially, then sleeps and wakes regularly to resync'
/> />
) ))
} }
{ {
renderItem('dark', this.renderItem('dark', (
<FormattedMessage <FormattedMessage
id='settings.parity.modes.mode_dark' id='settings.parity.modes.mode_dark'
defaultMessage='Parity syncs only when the RPC is active' defaultMessage='Parity syncs only when the RPC is active'
/> />
) ))
} }
{ {
renderItem('offline', this.renderItem('offline', (
<FormattedMessage <FormattedMessage
id='settings.parity.modes.mode_offline' id='settings.parity.modes.mode_offline'
defaultMessage="Parity doesn't sync" defaultMessage="Parity doesn't sync"
/> />
) ))
} }
</Select> </Select>
); );
} }
onChangeMode = (event, index, mode) => { onChangeMode = (event, index, mode) => {
const { api } = this.context; this.store.changeMode(mode || event.target.value);
api.parity
.setMode(mode)
.then((result) => {
if (result) {
this.setState({ mode });
}
})
.catch((error) => {
console.warn('onChangeMode', error);
});
}
loadMode () {
const { api } = this.context;
api.parity
.mode()
.then((mode) => {
this.setState({ mode });
})
.catch((error) => {
console.warn('loadMode', error);
});
} }
} }

View File

@ -0,0 +1,98 @@
// Copyright 2015, 2016 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 } from './parity.test.js';
import Parity from './';
let component;
let instance;
function render (props = {}) {
component = shallow(
<Parity { ...props } />,
{ context: { api: createApi() } }
);
instance = component.instance();
return component;
}
describe('views/Settings/Parity', () => {
beforeEach(() => {
render();
sinon.spy(instance.store, 'loadMode');
});
afterEach(() => {
instance.store.loadMode.restore();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('componentWillMount', () => {
beforeEach(() => {
return instance.componentWillMount();
});
it('loads the mode in the store', () => {
expect(instance.store.loadMode).to.have.been.called;
});
});
describe('components', () => {
it('renders a Container component', () => {
expect(component.find('Container')).to.have.length(1);
});
it('renders a LanguageSelector component', () => {
expect(component.find('LanguageSelector')).to.have.length(1);
});
it('renders a Features component', () => {
expect(component.find('LanguageSelector')).to.have.length(1);
});
});
describe('Parity features', () => {
describe('mode selector', () => {
let select;
beforeEach(() => {
select = component.find('Select[id="parityModeSelect"]');
sinon.spy(instance.store, 'changeMode');
});
afterEach(() => {
instance.store.changeMode.restore();
});
it('renders a mode selector', () => {
expect(select).to.have.length(1);
});
it('changes the mode on the store when changed', () => {
select.simulate('change', { target: { value: 'dark' } });
expect(instance.store.changeMode).to.have.been.calledWith('dark');
});
});
});
});

View File

@ -0,0 +1,30 @@
// Copyright 2015, 2016 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';
function createApi () {
return {
parity: {
mode: sinon.stub().resolves('passive'),
setMode: sinon.stub().resolves(true)
}
};
}
export {
createApi
};

View File

@ -0,0 +1,105 @@
// Copyright 2015, 2016 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 LogLevel from 'loglevel';
import { action, observable } from 'mobx';
import { LOG_KEYS } from '~/config';
const DEFAULT_MODE = 'active';
const LOGLEVEL_OPTIONS = Object
.keys(LogLevel.levels)
.map((name) => {
return {
name,
value: LogLevel.levels[name]
};
});
export default class Store {
@observable logLevels = {};
@observable mode = DEFAULT_MODE;
constructor (api) {
this._api = api;
this.loadLogLevels();
}
@action setLogLevels = (logLevels) => {
this.logLevels = logLevels;
}
@action setLogLevelsSelect = (logLevelsSelect) => {
this.logLevelsSelect = logLevelsSelect;
}
@action setMode = (mode) => {
this.mode = mode;
}
changeMode (mode) {
return this._api.parity
.setMode(mode)
.then((result) => {
if (result) {
this.setMode(mode);
}
})
.catch((error) => {
console.warn('changeMode', error);
});
}
loadLogLevels () {
this.setLogLevels(
Object
.keys(LOG_KEYS)
.reduce((state, logKey) => {
const log = LOG_KEYS[logKey];
const logger = LogLevel.getLogger(log.key);
const level = logger.getLevel();
state[logKey] = {
level,
log
};
return state;
}, this.logLevels)
);
}
updateLoggerLevel (path, level) {
LogLevel.getLogger(path).setLevel(level);
this.loadLogLevels();
}
loadMode () {
return this._api.parity
.mode()
.then((mode) => {
this.setMode(mode);
})
.catch((error) => {
console.warn('loadMode', error);
});
}
}
export {
LOGLEVEL_OPTIONS
};

View File

@ -0,0 +1,84 @@
// Copyright 2015, 2016 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 { createApi } from './parity.test.js';
import Store from './store';
let api;
let store;
function createStore () {
api = createApi();
store = new Store(api);
return store;
}
describe('views/Settings/Parity/Store', () => {
beforeEach(() => {
createStore();
sinon.spy(store, 'setMode');
});
afterEach(() => {
store.setMode.restore();
});
it('defaults to mode === active', () => {
expect(store.mode).to.equal('active');
});
describe('@action', () => {
describe('setMode', () => {
it('sets the mode', () => {
store.setMode('offline');
expect(store.mode).to.equal('offline');
});
});
});
describe('operations', () => {
describe('changeMode', () => {
beforeEach(() => {
return store.changeMode('offline');
});
it('calls parity.setMode', () => {
expect(api.parity.setMode).to.have.been.calledWith('offline');
});
it('sets the mode as provided', () => {
expect(store.setMode).to.have.been.calledWith('offline');
});
});
describe('loadMode', () => {
beforeEach(() => {
return store.loadMode();
});
it('calls parity.mode', () => {
expect(api.parity.mode).to.have.been.called;
});
it('sets the mode as retrieved', () => {
expect(store.setMode).to.have.been.calledWith('passive');
});
});
});
});

View File

@ -117,8 +117,13 @@ module.exports = {
test: /\.(woff(2)|ttf|eot|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.(woff(2)|ttf|eot|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [ 'file-loader?name=fonts/[name][hash:10].[ext]' ] use: [ 'file-loader?name=fonts/[name][hash:10].[ext]' ]
}, },
{
test: /parity-logo-white-no-text\.svg/,
use: [ 'url-loader' ]
},
{ {
test: /\.svg(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.svg(\?v=[0-9]\.[0-9]\.[0-9])?$/,
exclude: [ /parity-logo-white-no-text\.svg/ ],
use: [ 'file-loader?name=assets/[name].[hash:10].[ext]' ] use: [ 'file-loader?name=assets/[name].[hash:10].[ext]' ]
} }
], ],