diff --git a/conda/js/src/components/experiment/ExperimentEditor.jsx b/conda/js/src/components/experiment/ExperimentEditor.jsx index d6c0a1d970e7fb159b0c0646a7b61e46c873bac0..f53a51baf91a774d718e35463bc5b9a099f36a7e 100644 --- a/conda/js/src/components/experiment/ExperimentEditor.jsx +++ b/conda/js/src/components/experiment/ExperimentEditor.jsx @@ -4,7 +4,7 @@ import { Container, Row, Col, - Button, + Button, ButtonGroup, Form, FormGroup, Label, @@ -765,6 +765,17 @@ export class ExperimentEditor extends React.Component<Props, State> { const queue = block.queue || this.props.data.contents.globals.queue; const envDisabled = !block.hasOwnProperty('environment'); + const getClearedGlobals = () => { + const globals = copyObj(this.props.data.contents.globals); + // if the old algorithm had parameters and no other block is using that alg, + // delete the global param defaults for the old alg + if(globals.hasOwnProperty(block.algorithm) && + [...this.props.toolchain.contents.analyzers, ...this.props.toolchain.contents.blocks] + .filter(b => b.algorithm === block.algorithm).length <= 1) + delete globals[block.algorithm]; + return globals; + }; + return ( <FormGroup key={key} className={`block${ key } block_${ blockName }`}> <h4> @@ -874,7 +885,8 @@ export class ExperimentEditor extends React.Component<Props, State> { thisBlock.outputs = levMapStrings(Object.keys(outputs), tcBlock.outputs); // setup the parameters (erase & create stuff) - const globals = copyObj(this.props.data.contents.globals); + const globals = getClearedGlobals(); + // if the new alg has parameters, gen the global defaults for the params if(alg.contents.parameters && Object.keys(alg.contents.parameters).length > 0) globals[alg.name] = { @@ -884,13 +896,6 @@ export class ExperimentEditor extends React.Component<Props, State> { ...this.props.data.contents.globals[alg.name], }; - // if the old algorithm had parameters and no other block is using that alg, - // delete the global param defaults for the old alg - if(globals.hasOwnProperty(block.algorithm) && - [...this.props.toolchain.contents.analyzers, ...this.props.toolchain.contents.blocks] - .filter(b => b.algorithm === block.algorithm).length <= 1) - delete globals[block.algorithm]; - updateBlock(thisBlock, globals); this.setLockMap(blockName, true); }} @@ -905,44 +910,66 @@ export class ExperimentEditor extends React.Component<Props, State> { } </Input> </Col> - { !isAnalyzer && - <Col sm='auto'> - <Button - color='primary' - disabled={block.algorithm === ''} - title={`Copies the algorithm & IO mappings to all unconfigured blocks with the same inputs & outputs`} - onClick={() => { - // copy the algorithm and input/output mappings to blocks - // that are the same except for the block name and dont have an algorithm assigned already - const targetBlocks = this.props.toolchain.contents.blocks - // not the block being copied from - .filter(b => b.name !== blockName) - // same IO - .filter(b => JSON.stringify(b.inputs) === JSON.stringify(tcBlock.inputs)) - .filter(b => JSON.stringify(b.outputs) === JSON.stringify(tcBlock.outputs)) - .map(b => b.name) - ; - //console.log(targetBlocks); - - // because the target blocks have the exact same inputs/outputs as the current block - // we dont need to re-compute the block object that we will copy! - // instead, just reuse the current block object and assign it to all - // the target blocks. - // also, because all these blocks are the same algorithm as the current block - // we dont need to mess with the globals. - - const newBlocks = copyObj(this.props.data.contents.blocks); - targetBlocks.forEach(bName => { newBlocks[bName] = copyObj(block); }); - this.setContents({ - ...this.props.data.contents, - blocks: newBlocks, - }); - }} - > - Copy to similar blocks - </Button> - </Col> - } + <Col sm='auto'> + { !isAnalyzer && + <React.Fragment> + <Button + color='primary' + disabled={block.algorithm === ''} + title={`Copies the algorithm & IO mappings to all unconfigured blocks with the same inputs & outputs`} + onClick={() => { + // copy the algorithm and input/output mappings to blocks + // that are the same except for the block name and dont have an algorithm assigned already + const targetBlocks = this.props.toolchain.contents.blocks + // not the block being copied from + .filter(b => b.name !== blockName) + // same IO + .filter(b => JSON.stringify(b.inputs) === JSON.stringify(tcBlock.inputs)) + .filter(b => JSON.stringify(b.outputs) === JSON.stringify(tcBlock.outputs)) + .map(b => b.name) + ; + //console.log(targetBlocks); + + // because the target blocks have the exact same inputs/outputs as the current block + // we dont need to re-compute the block object that we will copy! + // instead, just reuse the current block object and assign it to all + // the target blocks. + // also, because all these blocks are the same algorithm as the current block + // we dont need to mess with the globals. + + const newBlocks = copyObj(this.props.data.contents.blocks); + targetBlocks.forEach(bName => { newBlocks[bName] = copyObj(block); }); + this.setContents({ + ...this.props.data.contents, + blocks: newBlocks, + }); + }} + > + Copy to similar blocks + </Button> + {' '} + </React.Fragment> + } + <Button + color='secondary' + title={`Clears the block data, resetting the selected block.`} + onClick={() => { + const newBlock = copyObj(block); + const newGlobals = getClearedGlobals(); + newBlock.algorithm = ''; + if(newBlock.inputs) + newBlock.inputs = {}; + if(newBlock.outputs) + newBlock.outputs = {}; + if(newBlock.parameters) + newBlock.parameters = {}; + + updateBlock(newBlock, newGlobals); + }} + > + Reset + </Button> + </Col> </FormGroup> </FormGroup> { Object.entries(alg.contents.parameters || {}).length > 0 && <h5>Parameters</h5> } diff --git a/conda/js/src/components/experiment/ExperimentEditor.spec.jsx b/conda/js/src/components/experiment/ExperimentEditor.spec.jsx index 1c236bccf14c4e98b127bd2b5083c172af464b9e..88baef9d26e7a2a22ddfcd11426a4a81ffa76bcd 100644 --- a/conda/js/src/components/experiment/ExperimentEditor.spec.jsx +++ b/conda/js/src/components/experiment/ExperimentEditor.spec.jsx @@ -511,4 +511,105 @@ describe('<ExperimentEditor />', () => { }); }); }); + + describe('Regression Tests', () => { + it(`clears the 'echo' & 'analysis' blocks from 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 exp = testExps.find(exp => expName === exp.name); + const tc = testTcs.find(tc => expName.includes(tc.name)); + wrapper = mount( + <C + data={getValidObj(exp, tc, [...normalBlocks, ...analyzerBlocks])} + experiments={[]} + normalBlocks={normalBlocks} + analyzerBlocks={analyzerBlocks} + datasets={datasets} + toolchain={tc} + saveFunc={saveFunc} + environments={envs} + updateFunc={updateFunc} + /> + ); + + expect(wrapper.props().data).to.have.property('name', expName); + expect(wrapper.props().data.contents).to.have.deep.property('blocks', { + 'echo': { + 'inputs': { + 'in_data': 'in' + }, + 'algorithm': 'user/integers_add/1', + 'outputs': { + 'out_data': 'out' + }, + parameters: {} + } + }); + + wrapper.find('svg #block_echo').simulate('click'); + wrapper.update(); + wrapper.find('.algorithm button.btn-secondary').simulate('click'); + expect(updateFunc.callCount).to.equal(1); + expect(wrapper.props().data.contents).to.have.deep.property('blocks', + { echo: { + inputs: {}, + algorithm: '', + outputs: {}, + parameters: {} + }} + ); + + wrapper.find('svg #block_analysis').simulate('click'); + wrapper.update(); + wrapper.find('.algorithm button.btn-secondary').simulate('click'); + expect(updateFunc.callCount).to.equal(2); + expect(wrapper.props().data.contents).to.have.deep.property('analyzers', + { analysis: { + inputs: {}, + algorithm: '', + parameters: {} + }} + ); + + expect(wrapper.props().data).to.be.deep.equal({ + 'name': 'user/user/single/1/single_add', + 'contents': { + description: '', + 'analyzers': { + 'analysis': { + inputs: {}, + algorithm: '', + parameters: {} + } + }, + 'datasets': { + 'set': { + 'set': 'set', + 'protocol': 'protocol', + 'database': 'simple/1' + } + }, + 'blocks': { + 'echo': { + algorithm: '', + inputs: {}, + outputs: {}, + parameters: {} + } + }, + 'globals': { + 'queue': 'queue', + 'environment': { + 'version': '1.2.0', + 'name': 'Python 2.7' + }, + } + } + }); + }); + }); });