ExperimentEditor.jsx 44.9 KB
Newer Older
1
2
3
4
5
6
// @flow
import * as React from 'react';
import {
	Container,
	Row,
	Col,
7
	Button, ButtonGroup,
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	Form,
	FormGroup,
	Label,
	Input,
	InputGroup,
	FormText,
	Collapse,
	Card,
	CardHeader,
	CardBody,
	TabContent, TabPane, Nav, NavItem, NavLink, CardTitle, CardText,
	FormFeedback,
	Alert,
	InputGroupAddon,
22
	Badge,
23
24
25
26
} from 'reactstrap';

import { connect } from 'react-redux';

27
28
import cn from 'classnames';

29
30
import lev from 'fast-levenshtein';

31
import './ExperimentEditor.css';
32
import { getValidExperimentObj as getValidObj, expGetDefaultParameterValue as getDefaultParameterValue } from '@helpers/beat';
33
import type { BeatObject, ParameterValue, BeatEnvironment } from '@helpers/beat';
34
import { changeObjFieldName, sortObject, copyObj } from '@helpers';
35
36

import * as Selectors from '@store/selectors.js';
37
import * as Actions from '@store/actions.js';
38

39
40
41
42
import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
import CacheInput from '../CacheInput.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import TypedField from '../TypedField.jsx';
43
import InfoTooltip from '../InfoTooltip.jsx';
44
import ParameterConsume from '../ParameterConsume.jsx';
45

46
import GraphicalEditor from '../toolchain/GraphicalEditor.jsx';
47
import type { BlockSet } from '../toolchain/types.js';
48
import { connectionToId } from '../toolchain/ToolchainConnection.jsx';
49

50
51
52
type Props = {
	data: BeatObject,
	experiments: BeatObject[],
53
54
	normalBlocks: BeatObject[],
	analyzerBlocks: BeatObject[],
55
56
	datasets: any[],
	toolchain: BeatObject,
57
	environments: { [string]: BeatEnvironment },
58
	saveFunc: (BeatObject) => any,
59
	updateFunc: (BeatObject) => any,
60
61
62
};

type State = {
63
64
65
66
67
68
69
70
	/* LockMap: map of which blocks are used in the type inference
	 * This is a per-block mapping of which blocks are used for type inference.
	 * There are more references (including some commented-out buttons & such) to
	 * block-level control of the lock map. Currently this is managed behind-the-scenes,
	 * where once a dataset/block has a valid dataset/algorithm assigned to it,
	 * its lock map value is true. The user can only influence this by disabling
	 * the entire lock map via "disable type inference" button.
	 */
71
	lockMap: LockMap,
72
	// info for the modal for editing blocks
73
	activeBlockInfo: {
74
75
76
		set: ?BlockSet,
		name: ?string,
	},
77
78
	// is type inference disabled globally?
	disableTypeInference: boolean,
79
80
};

81
82
83
84
type LockMap = {
	[string]: boolean,
};

85
// parses a string resulting from the "datasetFieldToString" function into the original database, protocol, and set
86
const rxStringToDataset = /^(\S+)\/(\S+)\s\((\S+\/\d+)\)/;
87
// parses a string resulting from the "datasetProtocolFieldToString" function into the original database and protocol
88
const rxStringToDatasetProtocol = /^(\S+)\s\((\S+\/\d+)\)/;
89
// given a dataset obj (with database, protocol, and set fields), return a user-readable string representing this dataset
90
const datasetFieldToString = (dataset: any) => `${ dataset.protocol }/${ dataset.set } (${ dataset.database })`;
91
// given a dataset obj (with database, protocol, and set fields), return a user-readable string representing this dataset's protocol
92
const datasetProtocolFieldToString = (dataset: any) => `${ dataset.protocol } (${ dataset.database })`;
93
// given an algorithm, returns maps of the inputs and outputs of the algorithm (the i/o name mapped to its type)
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const algIOs = (alg: any) => {
	const inputs = alg.contents.groups.map(g => {
		if(!g.inputs)
			return [];
		return Object.entries(g.inputs)
		.map(([iName, tObj]) => ({[iName]: tObj.type}))
		.reduce((o, iObj) => ({...o, ...iObj}), {})
		;
	})
	.reduce((o, is) => ({...o, ...is}), {});

	const outputs = alg.contents.groups.map(g => {
		if(!g.outputs)
			return [];
		return Object.entries(g.outputs)
		.map(([iName, tObj]) => ({[iName]: tObj.type}))
		.reduce((o, iObj) => ({...o, ...iObj}), {})
		;
	})
	.reduce((o, is) => ({...o, ...is}), {});

115
	return [
116
117
		inputs,
		outputs,
118
	];
119
};
120

121
122
// given a block and the available algorithms,
// remap the chosen algorithm type data onto the block i/o names
123
124
125
const getBlockIOTypes = (block: any, algs: any[]) => {
	const alg = algs.find(alg => alg.name === block.algorithm);
	const [ais, aos] = alg ? algIOs(alg) : [{}, {}];
126
127
	const bis = Object.entries(block.inputs).reduce((o, [name, bi]) => ({...o, [bi]: ais[name] || ''}), {});
	const bos = Object.entries(block.outputs || {}).reduce((o, [name, bo]) => ({...o, [bo]: aos[name] || ''}), {});
128
129
130
131

	return [bis, bos];
};

132
// generate a new lock map for the given toolchain
133
const genLockMap = (toolchain: any): LockMap => [...toolchain.contents.datasets, ...toolchain.contents.blocks, ...toolchain.contents.analyzers]
134
135
136
.map(b => b.name)
.reduce((o, name) => ({...o, [name]: false}), {});

137
// checks if the given object is valid for the current entity given the i/o types and inferred types map
138
139
140
141
142
143
const isValidEntity = (
	entityName: string,
	blockTcEntity: {inputs?: [], outputs?: []} = {},
	possibleObj: {inputs: any, outputs: any} = {inputs:{},outputs:{}},
	inferredTypes = {}
): boolean => {
144
145
146
	const hasInputs = Array.isArray(blockTcEntity.inputs);
	const hasOutputs = Array.isArray(blockTcEntity.outputs);
	if(!hasInputs && Object.keys(possibleObj.inputs).length > 0)
147
		return false;
148
	if(!hasOutputs && Object.keys(possibleObj.outputs).length > 0)
149
		return false;
150
	if(hasInputs && blockTcEntity.inputs.length !== Object.keys(possibleObj.inputs).length)
151
		return false;
152
	if(hasInputs && hasOutputs &&
153
154
		blockTcEntity.outputs.length !== Object.keys(possibleObj.outputs).length
	)
155
		return false;
156
157

	if(inferredTypes.hasOwnProperty(entityName)){
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
		if(inferredTypes[entityName][0] !== undefined){
			const remainingEntityInputTypes = Object.values(possibleObj.inputs);
			const remainingInferredInputTypes = Object.values(inferredTypes[entityName][0]).filter(t => t !== '');
			while(remainingInferredInputTypes.length > 0){
				const it = remainingInferredInputTypes.pop();
				if(!remainingEntityInputTypes.includes(it))
					return false;
				remainingEntityInputTypes.splice(remainingEntityInputTypes.indexOf(it), 1);
			}
		}

		if(inferredTypes[entityName][1] !== undefined){
			const remainingEntityOutputTypes = Object.values(possibleObj.outputs);
			const remainingInferredOutputTypes = Object.values(inferredTypes[entityName][1]).filter(t => t !== '');
			while(remainingInferredOutputTypes.length > 0){
				const it = remainingInferredOutputTypes.pop();
				if(!remainingEntityOutputTypes.includes(it))
					return false;
				remainingEntityOutputTypes.splice(remainingEntityOutputTypes.indexOf(it), 1);
			}
		}
	}

	return true;
};

184
185
186
187
188
189
190
191
const dbSetIsValidForTcDataset = (dbSet, tcDs) => {
	if(Object.keys(dbSet.outputs).length < tcDs.outputs.length)
		return false;
	if(!tcDs.outputs.reduce((b, output) => b && dbSet.outputs.hasOwnProperty(output), true))
		return false;
	return true;
};

192
193
194
195
196
197
198
199
200
201
202
// maps key strings to value strings via levenshtein distances
// must have the same # of keys as vals
const levMapStrings = (keyArr: string[], valArr: string[]): { [string]: string } => {
	if(keyArr.length !== valArr.length){
		throw new Error(`Cannot lev map arrays of different lengths!`);
	}

	const keys = [...keyArr];
	const vals = [...valArr];
	const map = {};

203
204
205
	//console.log(keys);
	//console.log(vals);

206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
	while(keys.length > 0){
		const scores = {};
		const matches = {};
		for(const k of keys){
			scores[k] = 10000;
			matches[k] = 'notfound';
			for(const v of vals){
				const score = lev.get(k, v);
				if(score < scores[k]){
					scores[k] = score;
					matches[k] = v;
				}
			}
		}

		let chosenKey = keys[0];
		let bestScore = 10000;
		for(const k of keys){
			if(scores[k] < bestScore){
				chosenKey = k;
				bestScore = scores[k];
			}
		}

		map[chosenKey] = matches[chosenKey];
		keys.splice(keys.indexOf(chosenKey), 1);
		vals.splice(vals.indexOf(matches[chosenKey]), 1);
233
		//console.log(map);
234
235
236
237
238
239
240
	}

	const orderedMap = {};
	for(const k of keyArr){
		orderedMap[k] = map[k];
	}

241
	//console.log(orderedMap);
242
243
244
	return orderedMap;
};

245
246
247
248
249
250
type EnvironmentConfigProps = {
	envInfo: {
		name: string,
		version: string,
	},
	queue: string,
251
	availableEnvs: { [string]: BeatEnvironment },
252
253
254
255
256
	updateEnvInfo: (name: string, version: string) => any,
	updateQueue: (queue: string) => any,
	disabled?: boolean,
};

257
258
const formatEnv = (name, version) => `${ name } (${ version })`;
const EnvironmentConfig = ({ envInfo, queue, availableEnvs, updateEnvInfo, updateQueue, disabled = false }: EnvironmentConfigProps) => {
259
	const eObjs = Object.values(availableEnvs).map(e => ({[formatEnv(e.name, e.version)]: Object.keys(e.queues)}));
260
261
262
263
264
265
266
267
	const envQueues = Object.assign({}, ...eObjs);
	const currEnv = formatEnv(envInfo.name, envInfo.version);
	const queues = envQueues[currEnv];
	return (
		<FormGroup row>
			<Col>
				<Label>
					<InfoTooltip
268
						id='envTooltip'
269
270
271
272
273
274
275
						info={`When executing an experiment in a docker-ized execution system (on BEAT web or in beat.cmdline using the "--docker" flag), there are certain Python environments available to run the experiment in. To choose a non-default environment, select the environment (and queue) here.`}
					>
						Environment Name
					</InfoTooltip>
				</Label>
				<Input
					type='select'
276
					className='env custom-select'
277
278
279
280
281
282
283
284
285
286
287
288
					value={currEnv}
					onChange={e => {
						// deconstruct selected env
						const rx = /^(.*) \((.*)\)$/;
						const res = rx.exec(e.target.value);
						if(!res)
							return;
						const [n, v] = res.slice(1, 3);
						updateEnvInfo(n, v);
					}}
					placeholder='Environment'
					disabled={disabled}
289
				>
290
291
292
293
294
295
296
297
					<option value={''}>Environment...</option>
					{
						Object.keys(envQueues).map((name, i) =>
							<option key={i} value={name}>{ name }</option>
						)
					}
				</Input>
			</Col>
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
			{ Array.isArray(queues) && queues.length > 0 &&
					<Col>
						<Label>
							<InfoTooltip
								id='queueTooltip'
								info={`The "queue" refers to the job queue to put the experiment in once a user decides to run it. This setting currently is only used on the BEAT web platform, and has no effect if ran through beat.cmdline.`}
							>
								Queue
							</InfoTooltip>
						</Label>
						<Input
							type='select'
							className='queue custom-select'
							value={queue}
							onChange={e => updateQueue(e.target.value)}
							placeholder='Queue'
							disabled={disabled}
						>
							{
								(queues).map((queue, i) =>
									<option key={i} value={queue}>{ queue }</option>
								)
							}
						</Input>
					</Col>
			}
324
325
326
		</FormGroup>
	);
};
327

328
329
330
331
332
333
export class ExperimentEditor extends React.Component<Props, State> {
	constructor(props: Props) {
		super(props);
	}

	state = {
334
		lockMap: genLockMap(this.props.toolchain),
335
		activeBlockInfo: {
336
337
338
			name: undefined,
			set: undefined,
		},
339
		disableTypeInference: false,
340
341
342
	}

	setContents = (newContents: any) => {
343
344
345
346
347
		this.props.updateFunc({
			...this.props.data,
			contents: {
				'description': this.props.data.contents['description'],
				...newContents,
348
349
350
351
			}
		});
	}

352
353
354
355
	// handles left clicking on a block
	handleBlockClick = (blockName: string, set: BlockSet) => {
		const newMBI = {
			set,
356
			name: blockName
357
358
		};
		this.setState({
359
			activeBlockInfo: newMBI,
360
		});
361
362
363
364
365
366
367
368

		setTimeout(() => {
			const elForm = document.querySelector(`.${ set === 'datasets' ? 'dataset' : 'block' }_${ blockName }`);
			if(!elForm){
				return;
			}
			elForm.scrollIntoView(true);
		}, 50);
369
370
	}

371
	setLockMap = (name: string, value: boolean) => {
372
		this.setState((prevState, props) => ({
373
			lockMap: {
374
				...prevState.lockMap,
375
376
				[name]: value,
			},
377
		}));
378
379
	}

380
	// gets parameter data for all the blocks with selected algorithms
381
382
	getParameterObjs = (): any[] => {
		const algs = [...this.props.normalBlocks, ...this.props.analyzerBlocks];
383
		return Object.entries({...this.props.data.contents.blocks, ...this.props.data.contents.analyzers})
384
		.map(([bName, block]) => algs.find(a => a.name === block.algorithm))
385
		.filter(a => a !== null && a !== undefined && a.contents.parameters !== undefined && Object.keys(a.contents.parameters).length > 0)
386
387
388
389
		.map(a => [a.name, a.contents.parameters])
		;
	}

390
391
392
393
	// tries to infer the types of inputs & outputs given the structure of the toolchain,
	// the algorithms currently chosen for blocks,
	// and the database/protocol/set currently chosen for datasets.
	// Does forward (from datasets to analyzers) and backward (from analyzers to datasets).
394
	getConnectionInferredTypes = (): any => {
395
		const isLocked = (name) => !this.state.disableTypeInference && this.state.lockMap[name];
396

397
		const dsTypes = Object.entries(this.props.data.contents.datasets)
398
399
400
401
402
403
404
405
		.reduce((o, [name, ds]) => {
			const d = this.props.datasets.find(pds => datasetFieldToString(pds) === datasetFieldToString(ds));
			const outputs = d ? d.outputs : {};
			return {
				...o,
				[name]: [{}, outputs]
			};
		}, {});
406

407
		const nTypes = Object.entries(this.props.data.contents.blocks)
408
		.filter(([name, block]) => isLocked(name))
409
410
411
412
413
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.normalBlocks),
		}), {});

414
		const aTypes = Object.entries(this.props.data.contents.analyzers)
415
		.filter(([name, block]) => isLocked(name))
416
417
418
419
420
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.analyzerBlocks),
		}), {});

421
422
423
424
425
426
427
		const missingDatasets = [...this.props.toolchain.contents.datasets]
		.filter(ds => !dsTypes.hasOwnProperty(ds.name))
		.reduce((o, ds) => ({
			...o,
			[ds.name]: [{}, ds.outputs.reduce((o, out) => ({...o, [out]: ''}), {})]
		}), {});

428
		const missingBlocks = Object.entries({
429
430
			...this.props.data.contents.blocks,
			...this.props.data.contents.analyzers
431
432
		})
		.filter(([name, block]) => !nTypes.hasOwnProperty(name) && !aTypes.hasOwnProperty(name))
433
434
		.reduce((o, [name, block]) => ({...o, [name]: getBlockIOTypes(block, [...this.props.normalBlocks, ...this.props.analyzerBlocks])}), {});

435
		const inferredCache = Object.keys({...missingDatasets, ...missingBlocks}).reduce((o, name) => ({...o, [name]: []}), {});
436
437

		const getSetTypes = (entityName: string) => {
438
			if(dsTypes.hasOwnProperty(entityName))
439
				return dsTypes[entityName];
440
			if(nTypes.hasOwnProperty(entityName))
441
				return nTypes[entityName];
442
			if(aTypes.hasOwnProperty(entityName))
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
				return aTypes[entityName];
			return [{}, {}];
		};

		// false === forwards, inferring input types
		// true === backwards, inferring output types
		const calcInferredType = (entityName: string, forwardsOrBackwards: boolean): [any, any][] => {
			//forwards
			if(!forwardsOrBackwards) {
				const connectionsTo = this.props.toolchain.contents.connections.filter(c => c.to.split('.')[0] === entityName);
				const types = connectionsTo.map(c => {
					const [bName, oName] = c.from.split('.');
					const iName = c.to.split('.')[1];
					const fromTypes = getSetTypes(bName)[1];
					return [iName, fromTypes[oName] || ''];
				})
459
				.reduce((o, [iName, type]) => ({...o, [iName]: o.hasOwnProperty(iName) ? (o[iName] === '' ? type : o[iName]) : type}), {});
460
461
462
463
464
465
466
467
468
				inferredCache[entityName] = [types, inferredCache[entityName][1]];
			} else {
				const connectionsFrom = this.props.toolchain.contents.connections.filter(c => c.from.split('.')[0] === entityName);
				const types = connectionsFrom.map(c => {
					const [bName, iName] = c.to.split('.');
					const oName = c.from.split('.')[1];
					const toTypes = getSetTypes(bName)[0];
					return [oName, toTypes[iName] || ''];
				})
469
				.reduce((o, [oName, type]) => ({...o, [oName]: o.hasOwnProperty(oName) ? (o[oName] === '' ? type : o[oName]) : type}), {});
470
471
472
473
474
475
476
				inferredCache[entityName] = [inferredCache[entityName][0], types];
			}

			return inferredCache[entityName];
		};


477
478
		Object.keys({...missingBlocks, ...missingDatasets})
		.forEach(name => {
479
480
481
482
483
484
485
			calcInferredType(name, false);
			calcInferredType(name, true);
		});

		return inferredCache;
	};

486
487
488
	// finds any issues/errors/misconfiguration throughout the experiment. things it checks:
	// - whether a block doesnt have a dataset/algorithm assigned yet
	// - whether a block with an algorithm doesnt have an input/output assigned yet
489
	// - whether a connection of a block has different types at the output and input
490
491
492
493
494
	getErrorMap = (): {
		dataSourceMissing: string[],
		algorithmMissing: string[],
		ioMissing: string[],
	} => {
495
		const dataSourceMissing = Object.entries(this.props.data.contents.datasets)
496
497
498
499
500
		.filter(([name, ds]) => ds.set === '')
		.map(([name, b]) => name)
		;

		const algorithmMissing = [
501
502
			...Object.entries(this.props.data.contents.blocks),
			...Object.entries(this.props.data.contents.analyzers),
503
504
505
506
507
		].filter(([name, b]) => b.algorithm === '')
		.map(([name, b]) => name)
		;

		const ioMissing = [
508
509
			...Object.entries(this.props.data.contents.blocks),
			...Object.entries(this.props.data.contents.analyzers),
510
511
512
		].filter(([name, b]) => {
			if(b.algorithm === '')
				return false;
513
514
			const inputs = Object.values(b.inputs);
			if(inputs.includes('') || Array.from(new Set(inputs)).length !== inputs.length)
515
				return true;
516
517
518
519
520
			if(b.hasOwnProperty('outputs')){
				const outputs = Object.values(b.outputs);
				if(outputs.includes('') || Array.from(new Set(outputs)).length !== outputs.length)
					return true;
			}
521
522
523
524
525
			return false;
		})
		.map(([name, b]) => name)
		;

526
		const dsTypes = Object.entries(this.props.data.contents.datasets)
527
528
529
530
531
532
533
534
		.reduce((o, [name, ds]) => {
			const d = this.props.datasets.find(pds => datasetFieldToString(pds) === datasetFieldToString(ds));
			const outputs = d ? d.outputs : {};
			return {
				...o,
				[name]: [{}, outputs]
			};
		}, {});
535

536
		const nTypes = Object.entries(this.props.data.contents.blocks)
537
538
539
540
541
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.normalBlocks),
		}), {});

542
		const aTypes = Object.entries(this.props.data.contents.analyzers)
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.analyzerBlocks),
		}), {});
		// returns the connection ids instead of block names
		const incompatibleConnectionTypes = this.props.toolchain.contents.hasOwnProperty('connections') ?
			this.props.toolchain.contents.connections.map(c => {
				const [fromBlock, fromOutput] = c.from.split('.');
				const [toBlock, toInput] = c.to.split('.');
				const fromTypes = dsTypes[fromBlock] || nTypes[fromBlock];
				if(!fromTypes)
					return false;
				const fromType = fromTypes[1][fromOutput];
				const toTypes = dsTypes[toBlock] || nTypes[toBlock];
				if(!toTypes)
					return false;
				const toType = toTypes[0][toInput];
				if(!fromType || !toType)
					return false;
				if(fromType === toType)
					return false;
				return connectionToId(c);
			}).filter(id => id) :
			[]
		;

569
570
571
572
		return {
			dataSourceMissing,
			algorithmMissing,
			ioMissing,
573
			incompatibleConnectionTypes,
574
575
576
		};
	};

577
	renderDatasets = () => (
578
		<FormGroup className='datasets'>
579
			<FormGroup className='d-flex'>
580
581
582
583
584
585
586
587
				<h3 className='mr-2'>
					<InfoTooltip
						id='datasetsTooltip'
						info={`Assign sets from database protocols to your dataset blocks. Or, assign all your dataset blocks at once using a compatible database protocol.`}
					>
						Datasets
					</InfoTooltip>
				</h3>
588
589
590
				<Input
					type='select'
					className='custom-select'
591
					value={JSON.stringify(sortObject(this.props.data.contents.datasets))}
592
593
594
					onChange={e => {
						const str = e.target.value;

595
						const newDs = JSON.parse(str);
596
						//console.log(newDs);
597
						this.setContents({...this.props.data.contents, datasets: newDs});
598
						/*
599
						for(const dataset in this.props.data.contents.datasets){
600
601
							this.setLockMap(dataset, true);
						}
602
						*/
603
604
					}}
				>
605
					<option value=''>Protocol...</option>
606
					{
607
608
609
610
611
612
613
614
615
616
617
618
						Object.entries(
							this.props.datasets
							.reduce((o, ds) => {
								const key = datasetProtocolFieldToString(ds);
								return {
									...o,
									[key]: [
										...(o[key] || []),
										ds
									]
								};
							}, {})
619
						)
620
						.map(([dbProtStr, sets]) => {
621
							const enoughSets = sets.length == Object.keys(this.props.data.contents.datasets).length;
622
623
624
625
626
627
628
629
630
631
632
							if(!enoughSets)
								return [dbProtStr, false];

							// least to greatest outputs len
							const tcDss = [...this.props.toolchain.contents.datasets]
							.sort((a, b) => a.outputs.length - b.outputs.length);

							const newDataset = {};
							// start at greatest go to least
							while(tcDss.length > 0){
								const tcDs = tcDss.pop();
633
634
								let setsIdx = sets.findIndex(set => set.set === tcDs.name && dbSetIsValidForTcDataset(set, tcDs));
								if(setsIdx === -1){
635
636
637
638
639
640
641
642
643
644
645
									const validSets = sets.filter(set => dbSetIsValidForTcDataset(set, tcDs));
									let bestScore = 10000;
									let bestMatch = 'notfound';
									for(const s of validSets){
										const score = lev.get(tcDs.name, s.set);
										if(score < bestScore){
											bestScore = score;
											bestMatch = s;
										}
									}
									setsIdx = sets.findIndex(set => set == bestMatch);
646
								}
647
648
649
650
651
652
653
654
655
656
657
658
659
660
								if(setsIdx === -1)
									return [dbProtStr, false];

								const dbSet = sets.splice(setsIdx, 1)[0];
								newDataset[tcDs.name] = {
									set: dbSet.set,
									protocol: dbSet.protocol,
									database: dbSet.database,
								};
							}
							return [dbProtStr, newDataset];
						})
						.filter(([dbProtStr, dss]) => dss !== false)
						.map(([dbProtStr, dss], i) => <option key={i} value={JSON.stringify(sortObject(dss))}>{dbProtStr}</option>)
661
662
663
					}
				</Input>
			</FormGroup>
664
			{
665
				(Object.entries(this.props.data.contents.datasets): [string, any][])
666
667
				.sort(([n1, ds1], [n2, ds2]) => n1 > n2 ? 1 : -1)
				.map(([name, dataset], i, dEntries) => {
668
669
					const inferredTypes = this.getConnectionInferredTypes();
					const tcDataset = this.props.toolchain.contents.datasets.find(d => d.name === name);
670
					const dsString = datasetFieldToString(dataset);
671
					return (
672
						<FormGroup row key={i} className={`dataset${ i } dataset_${ name }`}>
673
							<Label sm={4}>
674
								{ name }
675
676
677
678
679
680
681
								{' '}
								<Badge
									title={`This dataset provides the "${ name }" synchronization channel.`}
									color='info'
								>
									{ name }
								</Badge>
682
								{/*
683
684
685
686
687
								{' '}
								<Button
									color={this.state.lockMap[name] ? 'primary' : 'secondary'}
									title={`When unlocked, connected blocks will not restrict their available algorithms to those compatible with this dataset's output types. When locked, connected blocks will take into account this dataset's output types.`}
									onClick={() => {
688
										this.setLockMap(name, !this.state.lockMap[name] && dataset.set !== '');
689
690
691
692
									}}
								>
									{ this.state.lockMap[name] ? 'Locked' : 'Unlocked' }
								</Button>
693
								*/}
694
695
696
697
698
							</Label>
							<Col sm={8}>
								<Input
									type='select'
									className='custom-select'
699
700
									valid={dsString !== datasetFieldToString({database:'',protocol:'',set:''})}
									value={dsString}
701
702
703
704
705
									onChange={e => {
										const str = e.target.value;
										const res = rxStringToDataset.exec(str) || [];
										const ds = {
											set: res[2] || '',
706
707
											protocol: res[1] || '',
											database: res[3] || '',
708
709
										};

710
711
										const newDs = {...this.props.data.contents.datasets, [name]: ds};
										this.setContents({...this.props.data.contents, datasets: newDs});
712
										//this.setLockMap(name, true);
713
714
									}}
								>
715
									<option value=''>Dataset...</option>
716
717
									{
										this.props.datasets
718
										.filter(ds => dbSetIsValidForTcDataset(ds, tcDataset))
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
										.filter(ds => isValidEntity(
											name,
											tcDataset,
											{outputs: ds.outputs, inputs: {}},
											inferredTypes
										))
										.map(ds => datasetFieldToString(ds))
										.map((str, i) =>
											<option key={i} value={str}>{ str }</option>
										)
									}
								</Input>
							</Col>
						</FormGroup>
					);
				})
735
736
737
738
			}
		</FormGroup>
	);

739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
	// update a block's data (and the globals object, if it changed)
	updateBlock = (blockName: string, isAnalyzer: boolean, newBlock: any, globals: any = this.props.data.contents.globals) => {
		if(isAnalyzer)
			delete newBlock.outputs;

		const bKey = isAnalyzer ? 'analyzers' : 'blocks';
		const newBlocks = copyObj(this.props.data.contents[bKey]);
		newBlocks[blockName] = newBlock;
		this.setContents({
			...this.props.data.contents,
			[bKey]: newBlocks,
			globals,
		});
	}

754
	// renders the currently selected block to edit, if any
755
	renderBlock = (blockName: string, block: any, isAnalyzer: boolean, key: number) => {
756
		// gets the block info from the tc
757
		const tcBlock = isAnalyzer ? this.props.toolchain.contents.analyzers.find(b => b.name === blockName) : this.props.toolchain.contents.blocks.find(b => b.name === blockName);
758
		// gets the possible algorithms
759
		const algorithms = isAnalyzer ? this.props.analyzerBlocks : this.props.normalBlocks;
760

761
		// finds the current algorithm, if any
762
		const alg = algorithms.find(nb => nb.name === block.algorithm) || {contents: {groups: []}};
763
		// gets the input & output info of the current alg
764
765
		const [ inputs, outputs ] = algIOs(alg);

766
		// calculates the inferred types throughout the experiment
767
768
		const inferredTypes = this.getConnectionInferredTypes();

769
770
		// filters through the algs based on shape (input/output count) and type inferrence
		// to get the list of possible algs for this block
771
772
		const possibleAlgorithms = algorithms.filter(pa => {
			const [iAlg, oAlg] = algIOs(pa);
773
			return isValidEntity(blockName, tcBlock, {inputs: iAlg, outputs: oAlg}, inferredTypes);
774
		});
775

776
		// func to update the exp's block info
777
778
		const updateBlock = (newBlock: any, globals: any = this.props.data.contents.globals) =>
			this.updateBlock(blockName, isAnalyzer, newBlock, globals);
779

780
		// get info that could be from the block or from the global settings
781
782
		const envInfo = block.environment || this.props.data.contents.globals.environment;
		const queue = block.queue || this.props.data.contents.globals.queue;
783
784
		const envDisabled = !block.hasOwnProperty('environment');

785
786
787
788
789
790
791
792
793
794
795
		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;
		};

796
		return (
797
			<FormGroup key={key} className={`block${ key } block_${ blockName }`}>
798
				<h4>
799
800
801
802
803
804
					<InfoTooltip
						id='blockTooltip'
						info={`Choose a compatible algorithm for this block to run when execution reaches this place in the toolchain. When you choose an algorithm, inputs & outputs in the algorithm are automatically mapped to the inputs & outputs of the block - but make sure to check this mapping and manually fix incorrect assignments.`}
					>
						Block <pre className='preInline'>{ blockName }</pre>
					</InfoTooltip>
805
					{' '}
806
807
808
809
					<Badge
						title={`Block is synchronized to the "${ tcBlock.synchronized_channel }" channel`}
						color='info'
					>{ tcBlock.synchronized_channel }</Badge>
810
				</h4>
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
				<FormGroup row>
					<Col sm='2'>
						<Label>Execution Info</Label>
						<Button
							color={envDisabled ? 'secondary' : 'warning'}
							onClick={() => {
								if(envDisabled)
									updateBlock({
										...block,
										environment: {
											name: envInfo.name,
											version: envInfo.version,
										},
										queue: queue,
									});
								else {
827
									const newBlock = copyObj(block);
828
829
830
831
832
833
834
835
836
837
838
839
840
841
									delete newBlock.environment;
									delete newBlock.queue;
									updateBlock(newBlock);
								}
							}}
							title={`Toggle whether or not the block is using the global default value for execution information, or is using a custom setting for this specific block.`}
						>
							{ envDisabled ? 'Using global defaults' : 'Using block value' }
						</Button>
					</Col>
					<Col>
						<EnvironmentConfig
							envInfo={envInfo}
							queue={queue}
842
							availableEnvs={this.props.environments}
843
							disabled={envDisabled}
844
							updateEnvInfo={(name, version) => {
845
								const env = Object.values(this.props.environments).find(e => e.name === name && e.version === version);
846
847
848
849
850
851
852
853
854
855
856
857
								if(!env)
									return;
								const queue = Object.keys(env.queues)[0];
								updateBlock({
									...block,
									environment: {
										name,
										version,
									},
									queue,
								});
							}}
858
859
860
861
862
863
864
							updateQueue={queue => updateBlock({
								...block,
								queue,
							})}
						/>
					</Col>
				</FormGroup>
865
				<FormGroup className='algorithm'>
866
867
868
					<Label>
						Algorithm
					</Label>
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
					<FormGroup row>
						<Col>
							<Input
								type='select'
								className='custom-select'
								valid={block.algorithm !== ''}
								value={block.algorithm}
								onChange={e => {
									// user selected a different algorithm
									const str = e.target.value;
									// find the alg or use a dummy algorithm instead
									const alg = algorithms.find(nb => nb.name === str) || {name: '', contents: {groups: []}};
									const [ inputs, outputs ] = algIOs(alg);
									// try to map the IOs from the algorithm to the block via names
									const findCloseIOMatch = (s: string, arr: string[]): string => {
										if(arr.length === 1)
											return arr[0];
										const findRes = arr.find(str => str.includes(s));
										if(findRes !== undefined)
											return findRes;
										return '';
									};

									// create the new block obj with the new alg
									const thisBlock = {
										...block,
										algorithm: str,
									};
									// assign new inputs
									if(tcBlock.inputs)
										thisBlock.inputs = levMapStrings(Object.keys(inputs), tcBlock.inputs);
									// assign new outputs
									if(tcBlock.outputs)
										thisBlock.outputs = levMapStrings(Object.keys(outputs), tcBlock.outputs);

									// setup the parameters (erase & create stuff)
905
906
									const globals = getClearedGlobals();

907
908
909
910
911
912
913
914
									// 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] = {
											...Object.entries(alg.contents.parameters || {})
											.map(([pName, param]) => ({[pName]: getDefaultParameterValue(param)}))
											.reduce((o, p) => ({...o, ...p}), {}),
											...this.props.data.contents.globals[alg.name],
										};
915

916
									updateBlock(thisBlock, globals);
917
									//this.setLockMap(blockName, true);
918
919
920
921
922
923
924
925
926
927
928
929
								}}
							>
								<option value=''>Algorithm...</option>
								{
									possibleAlgorithms
									.map(nb => nb.name)
									.map((str, i) =>
										<option key={i} value={str}>{ str }</option>
									)
								}
							</Input>
						</Col>
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
						<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>
990
					</FormGroup>
991
				</FormGroup>
992
				{ Object.entries(alg.contents.parameters || {}).length > 0 && <h5>Parameters</h5> }
993
				{
994
					Object.entries(alg.contents.parameters || {}).map(([pName, paramObj], i) => {
995
						const isTethered = !block.parameters.hasOwnProperty(pName);
996
						const globalParams = this.props.data.contents.globals[alg.name];
997
						const currVal = block.parameters[pName] || globalParams[pName];
998
999
1000

						const deleteParam = () => {
							const thisBlock = {...block,
For faster browsing, not all history is shown. View entire blame