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:
@@ -14,67 +14,28 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// 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 { 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 Features, { FeaturesStore, FEATURES } from '~/ui/Features';
|
||||
|
||||
import Store, { LOGLEVEL_OPTIONS } from './store';
|
||||
import layout from '../layout.css';
|
||||
|
||||
@observer
|
||||
export default class Parity extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
loglevels: {},
|
||||
mode: 'active',
|
||||
selectValues: []
|
||||
};
|
||||
store = new Store(this.context.api);
|
||||
features = FeaturesStore.get();
|
||||
|
||||
componentWillMount () {
|
||||
this.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 });
|
||||
return this.store.loadMode();
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -94,18 +55,30 @@ export default class Parity extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={ layout.details }>
|
||||
<LanguageSelector />
|
||||
{ this.renderModes() }
|
||||
<Features />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ this.renderLogsConfig() }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
renderItem (mode, label) {
|
||||
return (
|
||||
<MenuItem
|
||||
key={ mode }
|
||||
label={ label }
|
||||
value={ mode }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderLogsConfig () {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (!this.features.active[FEATURES.LOGLEVELS]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -127,129 +100,89 @@ export default class Parity extends Component {
|
||||
}
|
||||
|
||||
renderLogsLevels () {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return null;
|
||||
}
|
||||
const { logLevels } = this.store;
|
||||
|
||||
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 { level, log } = logLevels[logKey];
|
||||
const { key, desc } = log;
|
||||
const onChange = (_, index) => {
|
||||
this.store.updateLoggerLevel(path, Object.values(LOGLEVEL_OPTIONS)[index].value);
|
||||
};
|
||||
|
||||
const onChange = (_, index) => {
|
||||
const nextLevel = Object.values(selectValues)[index].value;
|
||||
|
||||
LogLevel.getLogger(key).setLevel(nextLevel);
|
||||
this.loadLogLevels();
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={ logKey }>
|
||||
<p>{ desc }</p>
|
||||
<Select
|
||||
onChange={ onChange }
|
||||
value={ level }
|
||||
values={ selectValues }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div key={ key }>
|
||||
<p>{ desc }</p>
|
||||
<Select
|
||||
onChange={ onChange }
|
||||
value={ level }
|
||||
values={ LOGLEVEL_OPTIONS }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderModes () {
|
||||
const { mode } = this.state;
|
||||
|
||||
const renderItem = (mode, label) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={ mode }
|
||||
value={ mode }
|
||||
label={ label }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
const { mode } = this.store;
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.label'
|
||||
defaultMessage='mode of operation'
|
||||
/>
|
||||
}
|
||||
id='parityModeSelect'
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.hint'
|
||||
defaultMessage='the syning mode for the Parity node'
|
||||
/>
|
||||
}
|
||||
value={ mode }
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.label'
|
||||
defaultMessage='mode of operation'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onChangeMode }
|
||||
value={ mode }
|
||||
>
|
||||
{
|
||||
renderItem('active',
|
||||
this.renderItem('active', (
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.mode_active'
|
||||
defaultMessage='Parity continuously syncs the chain'
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
{
|
||||
renderItem('passive',
|
||||
this.renderItem('passive', (
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.mode_passive'
|
||||
defaultMessage='Parity syncs initially, then sleeps and wakes regularly to resync'
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
{
|
||||
renderItem('dark',
|
||||
this.renderItem('dark', (
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.mode_dark'
|
||||
defaultMessage='Parity syncs only when the RPC is active'
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
{
|
||||
renderItem('offline',
|
||||
this.renderItem('offline', (
|
||||
<FormattedMessage
|
||||
id='settings.parity.modes.mode_offline'
|
||||
defaultMessage="Parity doesn't sync"
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeMode = (event, index, mode) => {
|
||||
const { api } = this.context;
|
||||
|
||||
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);
|
||||
});
|
||||
this.store.changeMode(mode || event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
98
js/src/views/Settings/Parity/parity.spec.js
Normal file
98
js/src/views/Settings/Parity/parity.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
30
js/src/views/Settings/Parity/parity.test.js
Normal file
30
js/src/views/Settings/Parity/parity.test.js
Normal 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
|
||||
};
|
||||
105
js/src/views/Settings/Parity/store.js
Normal file
105
js/src/views/Settings/Parity/store.js
Normal 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
|
||||
};
|
||||
84
js/src/views/Settings/Parity/store.spec.js
Normal file
84
js/src/views/Settings/Parity/store.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user