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

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
// 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 { 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 { LocaleStore } from '../../i18n';
@observer
export default class LanguageSelector extends Component {
features = FeaturesStore.get();
store = LocaleStore.get();
render () {
if (!this.store.isDevelopment) {
if (!this.features.active[FEATURES.LANGUAGE]) {
return null;
}
@@ -70,6 +73,6 @@ export default class LanguageSelector extends Component {
}
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 Editor from './Editor';
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 GasPriceEditor from './GasPriceEditor';
import GasPriceSelector from './GasPriceSelector';
@@ -74,6 +75,9 @@ export {
CurrencySymbol,
Editor,
Errors,
FEATURES,
Features,
FeaturesStore,
Form,
FormWrap,
GasPriceEditor,