Commit 134e1a10 authored by Flavio TARSETTI's avatar Flavio TARSETTI
Browse files

Merge branch 'tc-fixes' into 'master'

Toolchain fixes

Closes #120, #121, #144, #130, and #143

See merge request !22
parents 9d65e266 f8d4d571
Pipeline #24367 passed with stages
in 32 minutes and 12 seconds
{
"plugins": [
"syntax-object-rest-spread",
"transform-object-rest-spread",
"transform-class-properties",
"syntax-dynamic-import"
"@babel/plugin-syntax-flow",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties"
],
"presets": [
"react",
"@babel/preset-react",
[
"env",
"@babel/preset-env",
{
"modules": false,
"debug": true,
......@@ -19,12 +18,12 @@
]
}
}
]
],
"@babel/preset-flow"
],
"env": {
"test": {
"plugins": [
"istanbul"
]
}
},
......
This diff is collapsed.
......@@ -22,84 +22,82 @@
"author": "",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.3",
"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",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"@babel/core": "^7.1.0",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.2",
"chai": "^4.1.2",
"chai-enzyme": "^1.0.0-beta.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"deep-equal-in-any-order": "^1.0.10",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^5.2.0",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"eslint": "^5.6.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",
"eslint-plugin-flowtype": "^2.50.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-react": "^7.11.1",
"flow-bin": "^0.81.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3",
"karma": "^2.0.4",
"karma": "^3.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.0",
"karma-webpack": "^3.0.5",
"mocha": "^5.2.0",
"postcss": "^6.0.23",
"postcss-cli": "^5.0.1",
"postcss": "^7.0.2",
"postcss-cli": "^6.0.0",
"postcss-cssnext": "^3.1.0",
"postcss-loader": "^2.1.6",
"postcss-loader": "^3.0.0",
"postcss-smart-import": "^0.7.6",
"react-hot-loader": "^4.3.3",
"react-popper": "^0.10.4",
"react-test-renderer": "^16.4.1",
"react-hot-loader": "^4.3.8",
"react-popper": "^1.0.2",
"react-test-renderer": "^16.5.2",
"redux-devtools": "^3.4.1",
"rimraf": "^2.6.2",
"selenium-webdriver": "^4.0.0-alpha.1",
"sinon": "^6.1.4",
"style-loader": "^0.21.0",
"stylelint": "^9.3.0",
"sinon": "^6.3.4",
"style-loader": "^0.23.0",
"stylelint": "^9.5.0",
"stylelint-config-standard": "^18.2.0",
"svg-inline-loader": "^0.8.0",
"tern-jsx": "^1.0.3",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "^4.16.2",
"terser-webpack-plugin": "^1.1.0",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5",
"webpack-dev-server": "^3.1.8",
"webpack-visualizer-plugin": "^0.1.11",
"worker-loader": "^2.0.0"
},
"dependencies": {
"ajv": "^6.5.2",
"ajv": "^6.5.3",
"bootstrap": "^4.1.3",
"classnames": "^2.2.6",
"d3": "^5.5.0",
"d3": "^5.7.0",
"fast-copy": "^1.2.2",
"fast-levenshtein": "^2.0.6",
"fuse.js": "^3.2.1",
"prop-types": "^15.6.2",
"react": "^16.4.1",
"react-contextmenu": "^2.9.2",
"react-dom": "^16.4.1",
"react": "^16.5.2",
"react-contextmenu": "^2.9.3",
"react-dom": "^16.5.2",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-transition-group": "^2.4.0",
"reactstrap": "^6.3.0",
"reactstrap": "^6.4.0",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^3.0.1",
"throttle-debounce": "^2.0.0"
"throttle-debounce": "^2.0.1"
}
}
......@@ -24,7 +24,7 @@ import type {
Group,
LocationMapEntry,
LocationMap ,
} from './types.js';
} from '@helpers/toolchainTypes';
type Props = {
// the representation data of the toolchain (block locations & channel colors)
......
......@@ -19,7 +19,7 @@ import {
import type { FlattenedDatabaseEntry } from '@store/selectors';
import type { ConnectionType } from './types.js';
import type { ConnectionType } from '@helpers/toolchainTypes';
import type { Protocol, Set as ProtocolSet } from '../database/DatabaseEditor.jsx';
import type { BeatObject } from '@helpers/beat';
import { generateNewKey } from '@helpers';
......
......@@ -26,7 +26,7 @@ import {
} from 'reactstrap';
import { ContextMenuTrigger } from 'react-contextmenu';
import cn from 'classnames';
import type { BlockSet, BlockCoords } from './types.js';
import type { BlockSet, BlockCoords } from '@helpers/toolchainTypes';
export type Props = {
name: string,
......
......@@ -3,7 +3,7 @@ import * as React from 'react';
import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu';
import cn from 'classnames';
import type { BlockCoords, ConnectionType } from './types.js';
import type { BlockCoords, ConnectionType } from '@helpers/toolchainTypes';
type Props = {
fromLocMap: BlockCoords,
......@@ -84,7 +84,7 @@ class ToolchainConnection extends React.Component<Props> {
render = () => {
const conn = this.props.connection;
const connectionId = `#${ connectionToId(conn) }`;
const connectionId = `${ connectionToId(conn) }`;
const fromLoc = this.props.fromLocMap;
const toLoc = this.props.toLocMap;
//console.log(`connecting "${ connId }" from ${ fromInfo } to ${ toInfo }`);
......
......@@ -33,13 +33,14 @@ import { getRandomBrightColor, generateNewKey, copyObj } from '@helpers';
import { getValidToolchainObj as getValidObj } from '@helpers/beat';
import type { BeatObject } from '@helpers/beat';
import { fetchLayout, genModuleApiFuncs } from '@helpers/api';
import Validate, { VALIDATORS } from '@helpers/schema';
import * as Actions from '@store/actions.js';
import * as Selectors from '@store/selectors.js';
import type { FlattenedDatabaseEntry } from '@store/selectors';
import Block from './ToolchainBlock.jsx';
import type { BlockType, ConnectionType, BlockSet, Group } from './types.js';
import type { NormalBlock, AnalyzerBlock, BlockType, ConnectionType, BlockSet, Group, Contents } from '@helpers/toolchainTypes';
import { connectionToId } from './ToolchainConnection.jsx';
import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
import CacheInput from '../CacheInput.jsx';
......@@ -56,7 +57,13 @@ import RenameGroupModal from './RenameGroupModal.jsx';
type Props = {
// saved data for the current toolchain
data: BeatObject,
data: {
name: string,
contents: Contents,
extraContents: {
groups: Group[]
},
},
// all toolchains
toolchains: BeatObject[],
// all databases
......@@ -253,7 +260,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
const toBlockNamesAndChannels = connectionData.map(c => [c.to.split('.')[0], c.channel]);
const assignBlockChannels = (blocks: BlockType[]) => {
return blocks.map(b => {
if(b.synchronized_channel !== '')
if(this.props.data.contents.connections.find(c => c.to.startsWith(`${ b.name }.`)) && b.synchronized_channel !== '')
return b;
const tbnc = toBlockNamesAndChannels.find(([n, c]) => n === b.name);
if(!tbnc)
......@@ -405,7 +412,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
};
const rep = {...this.props.data.contents.representation};
rep.connections = Object.entries(rep.connections).map(([name, rep]) => {
const newRepConns = Object.entries(rep.connections).map(([name, rep]) => {
if(!name.includes(`.${ oldName }`))
return [name, rep];
......@@ -440,6 +447,10 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
to: updated[1],
};
}),
representation: {
...rep,
connections: newRepConns,
},
};
this.setContents(newContents);
......@@ -455,11 +466,15 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
if(newBlock === undefined){
newBlock = {};
if(set === 'blocks' || set === 'analyzers'){
newBlock.inputs = [];
newBlock.synchronized_channel = Object.keys(this.props.data.contents.representation.channel_colors)[0];
newBlock.inputs = [
'input',
];
newBlock.synchronized_channel = '';
}
if(set === 'blocks' || set === 'datasets'){
newBlock.outputs = [];
newBlock.outputs = [
'output',
];
}
}
newBlock.name = blockName;
......@@ -639,12 +654,27 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
.reduce((cs, [name, rep]) => ({...cs, [name]: rep}), {})
;
const newConns = this.props.data.contents.connections.filter(c => {
if(c.from === cn.from && c.to === cn.to)
return false;
return true;
});
// if the given block has no conns to it, the syncd channel should become ''
const removeInvalidChannels = (b: NormalBlock | AnalyzerBlock): NormalBlock | AnalyzerBlock => {
if(newConns.find(c => c.to.split('.')[0] == b.name))
return b;
return {
...b,
synchronized_channel: '',
};
};
const newContents = {
connections: this.props.data.contents.connections.filter(c => {
if(c.from === cn.from && c.to === cn.to)
return false;
return true;
}),
connections: newConns,
blocks: this.props.data.contents.blocks.map(b => removeInvalidChannels(b)),
analyzers: this.props.data.contents.analyzers.map(b => removeInvalidChannels(b)),
representation: rep,
};
......@@ -1008,7 +1038,8 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
<Button
size='sm'
onClick={this.autoLayout}
title={'This will save the toolchain to your BEAT prefix.'}
title={'This will use Graphviz to generate a more visually understandable layout for your toolchain.'}
disabled={!!Validate(VALIDATORS['toolchain'], this.props.data)}
>
Layout
</Button>
......
......@@ -24,22 +24,25 @@ import {
InputGroupAddon,
Badge,
Modal, ModalBody, ModalHeader, ModalFooter,
UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,
UncontrolledDropdown, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,
} from 'reactstrap';
import CacheInput from '../CacheInput.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import { generateNewKey } from '@helpers';
import type { BlockType } from '@helpers/toolchainTypes';
type Props = {
// block data
data: any,
// func to close modal
toggle: () => any,
// is the modal active?
active: boolean,
// block data
data: BlockType,
// name of all blocks
blockNames: string[],
// possible channels the block can be sync'd to (an obj w keys as channel names & vals as colours)
possibleChannels: { [string]: string },
// func to close modal
toggle: () => any,
// change the block name
updateBlockName: (newName: string) => any,
// change the name of an input or output
......@@ -50,8 +53,6 @@ type Props = {
deleteBlockIO: (ioType: 'input' | 'output', ioName: string) => any,
// delete the block
deleteBlock: () => any,
// possible channels the block can be sync'd to (an obj w keys as channel names & vals as colours)
possibleChannels: any,
// update sync'd channel
updateBlockChannel: (channel: string) => any,
};
......@@ -85,6 +86,20 @@ class ToolchainModal extends React.Component<Props, State> {
// TODO: right now this is just an escape hatch for channel changing not propogating throughout the network. this needs to be fixed
const channelSelectDisabled = false; //Object.keys(this.props.possibleChannels).length === 1;
const currChannel: string = data.synchronized_channel || '';
const formatChannel = (channel: string) => {
return (
<span>
<span style={{backgroundColor: this.props.possibleChannels[channel] || '#000000'}}>
&nbsp;&nbsp;&nbsp;&nbsp;
</span>
&nbsp;
{ channel === '' ? '<none>' : channel }
</span>
);
};
return (
<Modal
isOpen={this.props.active}
......@@ -112,38 +127,49 @@ class ToolchainModal extends React.Component<Props, State> {
this.props.updateBlockName(e.target.value);
}}
validateFunc={(str) => {
return (
str === data.name ||
!this.props.blockNames.includes(str) ||
<span>This name is already taken by an existing block</span>
);
if(str === data.name){
return true;
}
if(this.props.blockNames.includes(str)){
return <span>This name is already taken by an existing block</span>;
}
if(str.includes('-')){
return <span>{`Cannot include the dash ("-") character`}</span>;
}
return true;
}}
/>
</FormGroup>
{ data.hasOwnProperty('synchronized_channel')
&&
<FormGroup>
<Label>Channel: {data.synchronized_channel}</Label>
<Input
type='select'
disabled={channelSelectDisabled}
value={data.synchronized_channel}
onChange={e => this.props.updateBlockChannel(e.target.value)}
title={channelSelectDisabled ? 'Cannot change the synchronized channel because there is not more than one to choose from' : undefined}
>
{ /* TODO: adding the current value in the list of possible values is part of the escape hatch for channel changes not propogating throughout the network. this needs to be fixed */ }
{
Array.from(new Set([...Object.keys(this.props.possibleChannels), data.synchronized_channel]))
.map((channel, i) =>
<option
key={i}
value={channel}
>
{ channel }
</option>
)
}
</Input>
<Label>
Synchronized channel:&nbsp;
<UncontrolledDropdown
id='channelSelector'
disabled={channelSelectDisabled}
style={{display: 'inline-block'}}
>
<DropdownToggle caret outline>
{ formatChannel(currChannel || '') }
</DropdownToggle>
<DropdownMenu>
{
Array.from(new Set([...Object.keys(this.props.possibleChannels), currChannel || '']))
.filter(c => c !== '')
.map((c, i) =>
<DropdownItem
id={`channel-${ c }`}
key={i}
onClick={e => this.props.updateBlockChannel(c)}
>
{ formatChannel(c) }
</DropdownItem>
)
}
</DropdownMenu>
</UncontrolledDropdown>
</Label>
</FormGroup>
}
<Row>
......
// @flow
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { spies } from '@test';
import C from './ToolchainModal.jsx';
// sleep for a bit, to wait for async things
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
describe('<ToolchainModal />', () => {
let wrapper;
afterEach(() => {
if(wrapper && wrapper.unmount)
wrapper.unmount();
});
const testBlockData = {
name: 'block1',
inputs: [
'input1',
],
outputs: [
'output1',
],
synchronized_channel: 'dataset1',
};
const testChannels = { 'dataset1': '#880000' };
const testBlockNames = ['block1'];
it('accepts the test data with the right props', () => {
wrapper = mount(
<C
active={true}
data={testBlockData}
blockNames={testBlockNames}
possibleChannels={testChannels}
toggle={() => {}}
updateBlockName={() => {}}
updateBlockChannel={() => {}}
updateBlockIOName={() => {}}
addBlockIO={() => {}}
deleteBlockIO={() => {}}
deleteBlock={() => {}}
/>
);
expect(wrapper).to.have.props([
'active',
'data',
'blockNames',
'possibleChannels',
'toggle',
'updateBlockName',
'updateBlockChannel',
'updateBlockIOName',
'addBlockIO',
'deleteBlockIO',
'deleteBlock',
]);
});
it('lets you change the name to another valid name', async () => {
const newBlockName = 'block2';
const _updateBlockName = (str: string) => {
expect(str).to.equal(newBlockName);
};
wrapper = mount(
<C
active={true}
data={testBlockData}
blockNames={testBlockNames}
possibleChannels={testChannels}
toggle={() => {}}
updateBlockName={_updateBlockName}
updateBlockChannel={() => {}}
updateBlockIOName={() => {}}
addBlockIO={() => {}}
deleteBlockIO={() => {}}
deleteBlock={() => {}}
/>
);
wrapper.find('input#tcModalInitFocus').prop('onChange')( { target: { value: newBlockName }});
wrapper.update();
await sleep(300);
wrapper.update();
expect(wrapper.exists('.input-group-append .text-danger')).to.equal(false);
});
it('doesnt let you change the name to a valid name with a dash ("-") in it', async () => {
const newBlockName = 'block-2';
const _updateBlockName = (str: string) => {
expect(str).to.equal(newBlockName);
};
wrapper = mount(
<C
active={true}
data={testBlockData}
blockNames={testBlockNames}
possibleChannels={testChannels}
toggle={() => {}}
updateBlockName={_updateBlockName}
updateBlockChannel={() => {}}
updateBlockIOName={() => {}}
addBlockIO={() => {}}
deleteBlockIO={() => {}}
deleteBlock={() => {}}
/>
);
wrapper.find('input#tcModalInitFocus').prop('onChange')( { target: { value: newBlockName }});
wrapper.update();
await sleep(300);
wrapper.update();
expect(wrapper.exists('.input-group-append .text-danger')).to.equal(true);
});
});