Commit 420ce36d authored by Flavio TARSETTI's avatar Flavio TARSETTI

Merge branch 'refactor' into 'master'

Refactor State Management Milestone

Closes #111

See merge request !7
parents 8c752385 1377ced9
Pipeline #22658 passed with stages
in 45 minutes and 9 seconds
This diff is collapsed.
......@@ -23,8 +23,8 @@
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-eslint": "^8.2.6",
"babel-loader": "^7.1.5",
"babel-plugin-dynamic-import-webpack": "^1.0.2",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
......@@ -34,20 +34,20 @@
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"chai": "^4.1.2",
"chai-enzyme": "^1.0.0-beta.0",
"cross-env": "^5.1.6",
"css-loader": "^0.28.11",
"chai-enzyme": "^1.0.0-beta.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^4.19.1",
"eslint-plugin-compat": "^2.4.0",
"eslint-plugin-flowtype": "^2.49.3",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.9.1",
"flow-bin": "^0.74.0",
"eslint": "^5.2.0",
"eslint-plugin-compat": "^2.5.1",
"eslint-plugin-flowtype": "^2.50.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-react": "^7.10.0",
"flow-bin": "^0.77.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3",
"karma": "^2.0.2",
"karma": "^2.0.4",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.1.0",
......@@ -56,46 +56,48 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.0",
"mocha": "^5.2.0",
"postcss": "^6.0.22",
"postcss-cli": "^5.0.0",
"postcss": "^6.0.23",
"postcss-cli": "^5.0.1",
"postcss-cssnext": "^3.1.0",
"postcss-loader": "^2.1.5",
"postcss-loader": "^2.1.6",
"postcss-smart-import": "^0.7.6",
"react-hot-loader": "^4.3.1",
"react-hot-loader": "^4.3.3",
"react-popper": "^0.10.4",
"react-test-renderer": "^16.4.0",
"react-test-renderer": "^16.4.1",
"redux-devtools": "^3.4.1",
"rimraf": "^2.6.2",
"sinon": "^6.0.0",
"sinon": "^6.1.4",
"style-loader": "^0.21.0",
"stylelint": "^9.2.1",
"stylelint": "^9.3.0",
"stylelint-config-standard": "^18.2.0",
"svg-inline-loader": "^0.8.0",
"tern-jsx": "^1.0.3",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "^4.12.0",
"webpack-cli": "^3.0.3",
"webpack-dev-server": "^3.1.4",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5",
"webpack-visualizer-plugin": "^0.1.11",
"worker-loader": "^2.0.0"
},
"dependencies": {
"ajv": "^6.5.1",
"bootstrap": "^4.1.1",
"ajv": "^6.5.2",
"bootstrap": "^4.1.3",
"classnames": "^2.2.6",
"d3": "^5.4.0",
"d3": "^5.5.0",
"fast-copy": "^1.2.2",
"fast-levenshtein": "^2.0.6",
"fuse.js": "^3.2.1",
"prop-types": "^15.6.1",
"react": "^16.4.0",
"prop-types": "^15.6.2",
"react": "^16.4.1",
"react-contextmenu": "^2.9.2",
"react-dom": "^16.4.0",
"react-dom": "^16.4.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-transition-group": "^2.3.1",
"reactstrap": "^6.1.0",
"react-transition-group": "^2.4.0",
"reactstrap": "^6.3.0",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^3.0.1"
"reselect": "^3.0.1",
"throttle-debounce": "^2.0.0"
}
}
......@@ -87,12 +87,14 @@ class CacheInput extends React.Component<Props, State>{
this.updateValidity(this.state.cache);
}
// if the parent's model changes, reset input
componentWillReceiveProps (nextProps: any) {
this.setState({
cache: (nextProps.value),
});
this.clearTimer();
// if the parent's model (the props) changes, reset input
componentDidUpdate = (prevProps: Props) => {
if(this.props.value !== prevProps.value){
this.setState({
cache: (this.props.value),
});
this.clearTimer();
}
}
// gets the validity info of an input string given a curr val
......
......@@ -49,6 +49,8 @@ type Props = {
history: any,
// gets the object based on the current URL
getEntityObject: () => BeatObject,
// gets the object's index # in the Redux array based on the current URL
getEntityIndex: () => number,
// updates the current object
updateFunc: (BeatObject) => any,
// the current BEAT entity being show
......@@ -90,6 +92,8 @@ export class EntityDetail extends React.Component<Props, State> {
render () {
const name = this.props.match.params.name;
const obj = this.props.getEntityObject();
const index = this.props.getEntityIndex();
return (
<Container>
<Row className='mb-1'>
......@@ -103,7 +107,7 @@ export class EntityDetail extends React.Component<Props, State> {
<pre style={{display: 'inline'}}>
{ name }
</pre>
<ValidSchemaBadge entity={this.props.entity} obj={this.props.getEntityObject()} />
<ValidSchemaBadge entity={this.props.entity} obj={obj} />
</h4>
{/* path line */}
<InputGroup>
......@@ -142,62 +146,62 @@ export class EntityDetail extends React.Component<Props, State> {
{
this.props.entity === 'algorithm' &&
<AlgorithmEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'dataformat' &&
<DataformatEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'library' &&
<LibraryEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'database' &&
<DatabaseEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'experiment' &&
<ExperimentEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'toolchain' &&
<ToolchainEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'plotter' &&
<PlotterEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
{
this.props.entity === 'plotterparameter' &&
<PlotterparameterEditorContainer
data={this.props.getEntityObject()}
saveFunc={this.saveChanges}
index={index}
/>
}
</TabPane>
<TabPane tabId='1'>
<pre>{ JSON.stringify(this.props.getEntityObject(), null, 4) }</pre>
<pre>{ JSON.stringify(obj, null, 4) }</pre>
</TabPane>
</TabContent>
</Col>
......@@ -216,6 +220,7 @@ const mapStateToProps = (state, ownProps) => {
// uses a selector based off the entity and finds the obj given the name
// if the obj doesnt exist (huge edge case) just return a default obj to not break everything
getEntityObject: (): BeatObject => Selectors[`${ entity }Get`](state).find(o => o.name === name) || getDefaultEntityObject(),
getEntityIndex: (): number => Selectors[`${ entity }Get`](state).findIndex(o => o.name === name),
entity,
prefix: Selectors.settingsGet(state).prefix,
};
......
......@@ -11,7 +11,7 @@ import {
generateAlgorithmTemplate,
generateLibraryTemplate,
} from '@helpers/api';
import { jsonClone } from '@helpers';
import { copyObj } from '@helpers';
import type { BeatEntity, BeatObject } from '@helpers/beat';
import { pluralize } from '@helpers/beat';
......@@ -58,12 +58,12 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop
if(!this.props.data.contents.parameters)
throw new Error(`Bad alg object, no params field: ${ this.props.data.contents }`);
const hasParameters = Object.keys(this.props.data.contents.parameters).length > 0;
uses = jsonClone(this.props.data.contents.uses);
uses = copyObj(this.props.data.contents.uses);
res = generateAlgorithmTemplate(this.props.data.name, hasParameters, uses);
break;
case('library'):
// find the used libraries
uses = jsonClone(this.props.data.contents.uses);
uses = copyObj(this.props.data.contents.uses);
res = generateLibraryTemplate(this.props.data.name, uses);
break;
default:
......
......@@ -85,11 +85,13 @@ export class NewEntityModal extends React.Component<Props, State> {
numSegs: nameSegmentsForEntity(this.props.entity),
}
componentWillReceiveProps = (newProps: Props) => {
this.setState({
nameSegs: getStartNameSegs(newProps.copyObj, newProps.data.map(d => d.name), newProps.entity, newProps.nameOrVersion),
numSegs: nameSegmentsForEntity(newProps.entity),
});
componentDidUpdate = (prevProps: Props) => {
if(this.props !== prevProps){
this.setState({
nameSegs: getStartNameSegs(this.props.copyObj, this.props.data.map(d => d.name), this.props.entity, this.props.nameOrVersion),
numSegs: nameSegmentsForEntity(this.props.entity),
});
}
}
titleStr = () => `New ${ this.props.entity }: "${ this.getNameString() }"`;
......
......@@ -54,8 +54,10 @@ class SearchBar extends React.PureComponent<Props, State> {
this.updateFuseInstance();
}
componentWillReceiveProps = (nextProps: Props) => {
this.updateFuseInstance(nextProps.data);
componentDidUpdate = (prevProps: Props) => {
if(this.props.data !== prevProps.data){
this.updateFuseInstance(this.props.data);
}
}
updateFuseInstance = (data: BeatObject[] = this.props.data) => {
......
......@@ -31,21 +31,19 @@ import CacheInput from '../CacheInput.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import TypedField from '../TypedField.jsx';
import * as Actions from '@store/actions.js';
import { BUILTIN_TYPES, getValidDataformatObj as getValidObj } from '@helpers/beat';
import type {
BeatObject,
} from '@helpers/beat.js';
import { changeObjFieldName, generateNewKey, jsonClone } from '@helpers';
import { changeObjFieldName, generateNewKey, copyObj } from '@helpers';
type Props = {
data: BeatObject,
dataformats: BeatObject[],
saveFunc: (BeatObject) => any,
};
type State = {
cache: any,
updateFunc: (BeatObject) => any,
};
const isObj = (obj): boolean => !Array.isArray(obj) && typeof obj === 'object' && obj !== null;
......@@ -223,36 +221,24 @@ const RecursiveObj = ({ obj, dfs, updateFunc }: {obj: any, dfs: string[], update
</div>
);
export class DataformatEditor extends React.Component<Props, State> {
export class DataformatEditor extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
state = {
cache: getValidObj(this.props.data),
}
componentWillReceiveProps (nextProps: Props) {
this.setState({
cache: getValidObj(nextProps.data),
});
}
setContents = (newContents: any) => {
this.setState({
cache: {
...this.state.cache,
contents: {
'#description': this.state.cache.contents['#description'],
...newContents,
}
this.props.updateFunc({
...this.props.data,
contents: {
'#description': this.props.data.contents['#description'],
...newContents,
}
});
}
allDfs = () => this.props.dataformats.map(d => d.name).concat(BUILTIN_TYPES);
filteredContents = (obj: any = this.state.cache.contents) => Object.entries(obj).filter(([n, v]) => n !== '#description').reduce((o, [n, v]) => ({...o, [n]: v}), {});
filteredContents = (obj: any = this.props.data.contents) => Object.entries(obj).filter(([n, v]) => n !== '#description').reduce((o, [n, v]) => ({...o, [n]: v}), {});
render = () => (
<div>
......@@ -261,9 +247,9 @@ export class DataformatEditor extends React.Component<Props, State> {
className='mx-auto'
outline
color='secondary'
onClick={() => this.props.saveFunc(this.state.cache)}
onClick={() => this.props.saveFunc(this.props.data)}
>
Save Changes (Changes are <ValidSchemaBadge entity='dataformat' obj={this.state.cache} />)
Save Changes (Changes are <ValidSchemaBadge entity='dataformat' obj={this.props.data} />)
</Button>
</div>
<Form onSubmit={(e) => e.preventDefault()}>
......@@ -274,8 +260,8 @@ export class DataformatEditor extends React.Component<Props, State> {
type='text'
name='description'
placeholder='Dataformat description...'
value={this.state.cache.contents['#description']}
onChange={e => this.setContents({ ...this.state.cache.contents, '#description': e.target.value})}
value={this.props.data.contents['#description']}
onChange={e => this.setContents({ ...this.props.data.contents, '#description': e.target.value})}
/>
</FormGroup>
</FormGroup>
......@@ -287,10 +273,20 @@ export class DataformatEditor extends React.Component<Props, State> {
}
const mapStateToProps = (state, ownProps) => {
const dfs = Selectors.dataformatGet(state);
const obj = {
dataformats: Selectors.dataformatGet(state),
dataformats: dfs,
data: dfs[ownProps.index] || getValidObj()
};
return obj;
};
export default connect(mapStateToProps)(DataformatEditor);
const mapDispatchToProps = (dispatch, ownProps) => ({
// replace the obj in the Redux store with the new object
updateFunc: (obj) => {
console.log(`dispatching for ${ obj.name }`);
dispatch(Actions[`dataformatUpdate`](obj.name, obj));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DataformatEditor);
......@@ -7,6 +7,7 @@ import { spies } from '@test';
import * as Selectors from '@store/selectors.js';
import { ExperimentEditor as C } from '.';
import { getValidExperimentObj as getValidObj } from '@helpers/beat';
import testAlgs from '@test/test_algs.json';
import testDbs from '@test/test_dbs.json';
......@@ -40,7 +41,10 @@ describe('<ExperimentEditor />', () => {
].concat(testExps);
exps.forEach(function(exp){
if(exp.name !== 'user/user/single/1/single_add')
return;
const saveFunc = () => {};
const updateFunc = () => {};
const tc = testTcs.find(tc => exp.name.includes(tc.name));
it(`${ exp.name }`, () => {
......@@ -54,11 +58,12 @@ describe('<ExperimentEditor />', () => {
toolchain={tc}
saveFunc={saveFunc}
environments={envs}
updateFunc={updateFunc}
/>
);
expect(wrapper).to.have.props(
['data', 'experiments', 'normalBlocks', 'analyzerBlocks', 'datasets', 'toolchain', 'saveFunc', 'environments']
['data', 'experiments', 'normalBlocks', 'analyzerBlocks', 'datasets', 'toolchain', 'saveFunc', 'environments', 'updateFunc']
);
});
});
......@@ -68,10 +73,14 @@ describe('<ExperimentEditor />', () => {
it(`user/user/single/1/single_add`, () => {
const expName = 'user/user/single/1/single_add';
const saveFunc = sinon.spy();
const _updateFunc = (obj) => {
wrapper.setProps && wrapper.setProps({ data: obj });
};
const updateFunc = sinon.spy(_updateFunc);
const tc = testTcs.find(tc => expName.includes(tc.name));
wrapper = mount(
<C
data={{name: expName, contents: {}}}
data={getValidObj({name: expName, contents: {}}, tc, [normalBlocks, ...analyzerBlocks])}
experiments={[]}
normalBlocks={normalBlocks}
analyzerBlocks={analyzerBlocks}
......@@ -79,17 +88,19 @@ describe('<ExperimentEditor />', () => {
toolchain={tc}
saveFunc={saveFunc}
environments={envs}
updateFunc={updateFunc}
/>
);
//console.log('doing name');
expect(wrapper.state('cache')).to.have.property('name', expName);
expect(wrapper.props().data).to.have.property('name', expName);
//console.log('finished name change, doing dataset');
wrapper.find('div.dataset0 select').simulate('change', { target: { value: 'protocol/set (simple/1)'}});
expect(wrapper.state('cache').contents).to.have.deep.property('datasets', {
expect(updateFunc.callCount).to.equal(1);
expect(wrapper.props().data.contents).to.have.deep.property('datasets', {
'set': {
'set': 'set',
'protocol': 'protocol',
......@@ -102,7 +113,8 @@ describe('<ExperimentEditor />', () => {
wrapper.find('svg #block_echo').simulate('click');
expect(wrapper.find({ name: 'echo', set: 'blocks'}).find('.tcBlockBackground').prop('className')).to.include('highlighted');
wrapper.find('div.block0 div.algorithm select').at(0).simulate('change', { target: { value: 'user/integers_add/1'}});
expect(wrapper.state('cache').contents).to.have.deep.property('blocks', {
expect(updateFunc.callCount).to.equal(2);
expect(wrapper.props().data.contents).to.have.deep.property('blocks', {
'echo': {
'inputs': {
'in_data': 'in'
......@@ -120,7 +132,8 @@ describe('<ExperimentEditor />', () => {
wrapper.find('svg #block_analysis').simulate('click');
expect(wrapper.find({ name: 'analysis', set: 'analyzers'}).find('.tcBlockBackground').prop('className')).to.include('highlighted');
wrapper.find('div.block0 div.algorithm select').at(0).simulate('change', { target: { value: 'user/integers_echo_analyzer/1'}});
expect(wrapper.state('cache').contents).to.have.deep.property('analyzers', {
expect(updateFunc.callCount).to.equal(3);
expect(wrapper.props().data.contents).to.have.deep.property('analyzers', {
'analysis': {
'inputs': {
'in_data': 'in'
......@@ -133,8 +146,10 @@ describe('<ExperimentEditor />', () => {
//console.log('finished analyzer, doing env');
wrapper.find('.globals select.env').simulate('change', { target: { value: 'Scientific Python 2.7 (1.0.0)'}});
expect(updateFunc.callCount).to.equal(4);
wrapper.find('.globals select.queue').simulate('change', { target: { value: 'Default'}});
expect(wrapper.state('cache').contents).to.have.deep.property('globals', {
expect(updateFunc.callCount).to.equal(5);
expect(wrapper.props().data.contents).to.have.deep.property('globals', {
'queue': 'Default',
'environment': {
'version': '1.0.0',
......@@ -147,7 +162,7 @@ describe('<ExperimentEditor />', () => {
//console.log('finished env, doing cache check');
expect(wrapper.state('cache')).to.be.deep.equal({
expect(wrapper.props().data).to.be.deep.equal({
'name': 'user/user/single/1/single_add',
'contents': {
description: '',
......
......@@ -25,10 +25,11 @@ import { connect } from 'react-redux';
import { getValidLibraryObj as getValidObj } from '@helpers/beat';
import type { BeatObject } from '@helpers/beat';
import { changeObjFieldName, jsonClone } from '@helpers';
import { changeObjFieldName, copyObj } from '@helpers';
import * as Selectors from '@store/selectors.js';
import * as Actions from '@store/actions.js';
import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
import CacheInput from '../CacheInput.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
......@@ -39,153 +40,149 @@ type Props = {
data: BeatObject,
libraries: BeatObject[],
saveFunc: (BeatObject) => any,
updateFunc: (BeatObject) => any,
};
type State = {
cache: any,
};
export class LibraryEditor extends React.Component<Props, State> {
export class LibraryEditor extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
state = {
cache: getValidObj(this.props.data),
}
componentWillReceiveProps (nextProps: Props) {
this.setState({
cache: getValidObj(nextProps.data),
});
}
setContents = (newContents: any) => {
this.setState({
cache: {
...this.state.cache,
contents: {
'description': this.state.cache.contents['description'],
...newContents,
}
}
this.props.updateFunc({
...this.props.data,
contents: newContents
});
}
updateLibraries = (libraries: {}) => {
this.setContents({...this.state.cache.contents, uses: libraries});
this.setContents({...this.props.data.contents, uses: libraries});
}
render = () => (
<div>
<div className='d-flex'>
<Button
className='mx-auto'
outline
color='secondary'
onClick={() => this.props.saveFunc(this.state.cache)}
>
Save Changes (Changes are <ValidSchemaBadge entity='library' obj={this.state.cache} />)
</Button>
<TemplateButton
data={this.state.cache}
entity={'library'}
/>
</div>
<Form onSubmit={(e) => e.preventDefault()}>
<FormGroup tag='fieldset'>
<legend>Library Settings</legend>
<FormGroup>
<Label for='description'>Short Description</Label>
<Input
type='text'
name='description'
placeholder='Library description...'
value={this.state.cache.contents['description']}
onChange={e => this.setContents({ ...this.state.cache.contents, 'description': e.target.value})}
/>
</FormGroup>
<FormGroup>
<Label>Language</Label>
<Input
id='libLanguage'
type='select'
className='custom-select'
value={this.state.cache.contents.language}
onChange={() => {}}
>
<option value='python'>Python</option>
<option disabled value='cxx'>C++</option>
<option disabled value='matlab'>Matlab</option>
<option disabled value='r'>R</option>
</Input>
</FormGroup>
</FormGroup>
<FormGroup id='usedLibs'>
<h4>Libraries Used</h4>
{
(Object.entries(this.state.cache.contents.uses): [string, any][]).map(([name, lib], i) => (
<TypedField
key={i}
name={name}
type={lib}
types={this.props.libraries.filter(l => l.name !== this.state.cache.name).map(l => l.name)}
existingFields={Object.keys(this.state.cache.contents.uses)}
nameUpdateFunc={str => {
const newLibs = changeObjFieldName(this.state.cache.contents.uses, name, str);
this.updateLibraries(newLibs);
}}
typeUpdateFunc={str => {
const newLibs = {
...this.state.cache.contents.uses,
[name]: str
};
this.updateLibraries(newLibs);
}}
deleteFunc={() => {
const newLibs = { ...this.state.cache.contents.uses };
delete newLibs[name];
this.updateLibraries(newLibs);
}}
/>
))
}
render = () => {
const data = this.props.data;