ExperimentEditor.jsx 41.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// @flow
import * as React from 'react';
import {
	Container,
	Row,
	Col,
	Button,
	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, jsonClone } 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
144
const isValidEntity = (
	entityName: string,
	blockTcEntity: {inputs?: [], outputs?: []} = {},
	possibleObj: {inputs: any, outputs: any} = {inputs:{},outputs:{}},
	inferredTypes = {}
): boolean => {
	if(!blockTcEntity.hasOwnProperty('inputs') && Object.keys(possibleObj.inputs).length > 0)
145
		return false;
146
	if(!blockTcEntity.hasOwnProperty('outputs') && Object.keys(possibleObj.outputs).length > 0)
147
		return false;
148
	if(blockTcEntity.hasOwnProperty('inputs') && blockTcEntity.inputs.length !== Object.keys(possibleObj.inputs).length)
149
		return false;
150
151
152
153
	if(blockTcEntity.hasOwnProperty('inputs') &&
		blockTcEntity.hasOwnProperty('outputs') &&
		blockTcEntity.outputs.length !== Object.keys(possibleObj.outputs).length
	)
154
		return false;
155
156

	if(inferredTypes.hasOwnProperty(entityName)){
157
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
		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;
};

183
184
185
186
187
188
189
190
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;
};

191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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
233
234
235
236
237
238
// 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 = {};

	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);
	}

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

	return orderedMap;
};

239
240
241
242
243
244
type EnvironmentConfigProps = {
	envInfo: {
		name: string,
		version: string,
	},
	queue: string,
245
	availableEnvs: { [string]: BeatEnvironment },
246
247
248
249
250
	updateEnvInfo: (name: string, version: string) => any,
	updateQueue: (queue: string) => any,
	disabled?: boolean,
};

251
252
const formatEnv = (name, version) => `${ name } (${ version })`;
const EnvironmentConfig = ({ envInfo, queue, availableEnvs, updateEnvInfo, updateQueue, disabled = false }: EnvironmentConfigProps) => {
253
	const eObjs = Object.values(availableEnvs).map(e => ({[formatEnv(e.name, e.version)]: Object.keys(e.queues)}));
254
255
256
257
258
259
260
261
	const envQueues = Object.assign({}, ...eObjs);
	const currEnv = formatEnv(envInfo.name, envInfo.version);
	const queues = envQueues[currEnv];
	return (
		<FormGroup row>
			<Col>
				<Label>
					<InfoTooltip
262
						id='envTooltip'
263
264
265
266
267
268
269
						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'
270
					className='env custom-select'
271
272
273
274
275
276
277
278
279
280
281
282
					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}
283
				>
284
285
286
287
288
289
290
291
					<option value={''}>Environment...</option>
					{
						Object.keys(envQueues).map((name, i) =>
							<option key={i} value={name}>{ name }</option>
						)
					}
				</Input>
			</Col>
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
			{ 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>
			}
318
319
320
		</FormGroup>
	);
};
321

322
323
324
325
326
327
export class ExperimentEditor extends React.Component<Props, State> {
	constructor(props: Props) {
		super(props);
	}

	state = {
328
		lockMap: genLockMap(this.props.toolchain),
329
		activeBlockInfo: {
330
331
332
			name: undefined,
			set: undefined,
		},
333
		disableTypeInference: false,
334
335
336
	}

	setContents = (newContents: any) => {
337
338
339
340
341
		this.props.updateFunc({
			...this.props.data,
			contents: {
				'description': this.props.data.contents['description'],
				...newContents,
342
343
344
345
			}
		});
	}

346
347
348
349
	// handles left clicking on a block
	handleBlockClick = (blockName: string, set: BlockSet) => {
		const newMBI = {
			set,
350
			name: blockName
351
352
		};
		this.setState({
353
			activeBlockInfo: newMBI,
354
		});
355
356
357
358
359
360
361
362

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

365
	setLockMap = (name: string, value: boolean) => {
366
		this.setState((prevState, props) => ({
367
			lockMap: {
368
				...prevState.lockMap,
369
370
				[name]: value,
			},
371
		}));
372
373
	}

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

384
385
386
387
	// 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).
388
	getConnectionInferredTypes = (): any => {
389
		const isLocked = (name) => !this.state.disableTypeInference && this.state.lockMap[name];
390

391
		const dsTypes = Object.entries(this.props.data.contents.datasets)
392
393
394
395
396
397
398
399
		.reduce((o, [name, ds]) => {
			const d = this.props.datasets.find(pds => datasetFieldToString(pds) === datasetFieldToString(ds));
			const outputs = d ? d.outputs : {};
			return {
				...o,
				[name]: [{}, outputs]
			};
		}, {});
400

401
		const nTypes = Object.entries(this.props.data.contents.blocks)
402
		.filter(([name, block]) => isLocked(name))
403
404
405
406
407
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.normalBlocks),
		}), {});

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

415
416
417
418
419
420
421
		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]: ''}), {})]
		}), {});

422
		const missingBlocks = Object.entries({
423
424
			...this.props.data.contents.blocks,
			...this.props.data.contents.analyzers
425
426
		})
		.filter(([name, block]) => !nTypes.hasOwnProperty(name) && !aTypes.hasOwnProperty(name))
427
428
		.reduce((o, [name, block]) => ({...o, [name]: getBlockIOTypes(block, [...this.props.normalBlocks, ...this.props.analyzerBlocks])}), {});

429
		const inferredCache = Object.keys({...missingDatasets, ...missingBlocks}).reduce((o, name) => ({...o, [name]: []}), {});
430
431

		const getSetTypes = (entityName: string) => {
432
			if(dsTypes.hasOwnProperty(entityName))
433
				return dsTypes[entityName];
434
			if(nTypes.hasOwnProperty(entityName))
435
				return nTypes[entityName];
436
			if(aTypes.hasOwnProperty(entityName))
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
				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] || ''];
				})
453
				.reduce((o, [iName, type]) => ({...o, [iName]: o.hasOwnProperty(iName) ? (o[iName] === '' ? type : o[iName]) : type}), {});
454
455
456
457
458
459
460
461
462
				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] || ''];
				})
463
				.reduce((o, [oName, type]) => ({...o, [oName]: o.hasOwnProperty(oName) ? (o[oName] === '' ? type : o[oName]) : type}), {});
464
465
466
467
468
469
470
				inferredCache[entityName] = [inferredCache[entityName][0], types];
			}

			return inferredCache[entityName];
		};


471
472
		Object.keys({...missingBlocks, ...missingDatasets})
		.forEach(name => {
473
474
475
476
477
478
479
			calcInferredType(name, false);
			calcInferredType(name, true);
		});

		return inferredCache;
	};

480
481
482
483
484
485
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
	// - TODO: whether a connection of a block has different types at the output and input
	getErrorMap = (): {
		dataSourceMissing: string[],
		algorithmMissing: string[],
		ioMissing: string[],
	} => {
489
		const dataSourceMissing = Object.entries(this.props.data.contents.datasets)
490
491
492
493
494
		.filter(([name, ds]) => ds.set === '')
		.map(([name, b]) => name)
		;

		const algorithmMissing = [
495
496
			...Object.entries(this.props.data.contents.blocks),
			...Object.entries(this.props.data.contents.analyzers),
497
498
499
500
501
		].filter(([name, b]) => b.algorithm === '')
		.map(([name, b]) => name)
		;

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

520
		const dsTypes = Object.entries(this.props.data.contents.datasets)
521
522
523
524
525
526
527
528
		.reduce((o, [name, ds]) => {
			const d = this.props.datasets.find(pds => datasetFieldToString(pds) === datasetFieldToString(ds));
			const outputs = d ? d.outputs : {};
			return {
				...o,
				[name]: [{}, outputs]
			};
		}, {});
529

530
		const nTypes = Object.entries(this.props.data.contents.blocks)
531
532
533
534
535
		.reduce((o, [name, block]) => ({
			...o,
			[name]: getBlockIOTypes(block, this.props.normalBlocks),
		}), {});

536
		const aTypes = Object.entries(this.props.data.contents.analyzers)
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
		.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) :
			[]
		;

563
564
565
566
		return {
			dataSourceMissing,
			algorithmMissing,
			ioMissing,
567
			incompatibleConnectionTypes,
568
569
570
		};
	};

571
	renderDatasets = () => (
572
		<FormGroup className='datasets'>
573
			<FormGroup className='d-flex'>
574
575
576
577
578
579
580
581
				<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>
582
583
584
				<Input
					type='select'
					className='custom-select'
585
					value={JSON.stringify(sortObject(this.props.data.contents.datasets))}
586
587
588
					onChange={e => {
						const str = e.target.value;

589
						//Object.keys(this.props.data.contents.datasets).map(dsName
590
						const newDs = JSON.parse(str);
591
						console.log(newDs);
592
593
						this.setContents({...this.props.data.contents, datasets: newDs});
						for(const dataset in this.props.data.contents.datasets){
594
595
							this.setLockMap(dataset, true);
						}
596
597
					}}
				>
598
					<option value=''>Protocol...</option>
599
					{
600
601
602
603
604
605
606
607
608
609
610
611
						Object.entries(
							this.props.datasets
							.reduce((o, ds) => {
								const key = datasetProtocolFieldToString(ds);
								return {
									...o,
									[key]: [
										...(o[key] || []),
										ds
									]
								};
							}, {})
612
						)
613
						.map(([dbProtStr, sets]) => {
614
							const enoughSets = sets.length == Object.keys(this.props.data.contents.datasets).length;
615
616
617
618
619
620
621
622
623
624
625
							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();
626
627
628
629
								let setsIdx = sets.findIndex(set => set.set === tcDs.name && dbSetIsValidForTcDataset(set, tcDs));
								if(setsIdx === -1){
									setsIdx = sets.findIndex(set => dbSetIsValidForTcDataset(set, tcDs));
								}
630
631
632
633
634
635
636
637
638
639
640
641
642
643
								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>)
644
645
646
					}
				</Input>
			</FormGroup>
647
			{
648
				(Object.entries(this.props.data.contents.datasets): [string, any][])
649
650
				.sort(([n1, ds1], [n2, ds2]) => n1 > n2 ? 1 : -1)
				.map(([name, dataset], i, dEntries) => {
651
652
					const inferredTypes = this.getConnectionInferredTypes();
					const tcDataset = this.props.toolchain.contents.datasets.find(d => d.name === name);
653
					const dsString = datasetFieldToString(dataset);
654
					return (
655
						<FormGroup row key={i} className={`dataset${ i } dataset_${ name }`}>
656
							<Label sm={4}>
657
								{ name }
658
659
660
661
662
663
664
								{' '}
								<Badge
									title={`This dataset provides the "${ name }" synchronization channel.`}
									color='info'
								>
									{ name }
								</Badge>
665
								{/*
666
667
668
669
670
								{' '}
								<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={() => {
671
										this.setLockMap(name, !this.state.lockMap[name] && dataset.set !== '');
672
673
674
675
									}}
								>
									{ this.state.lockMap[name] ? 'Locked' : 'Unlocked' }
								</Button>
676
								*/}
677
678
679
680
681
							</Label>
							<Col sm={8}>
								<Input
									type='select'
									className='custom-select'
682
683
									valid={dsString !== datasetFieldToString({database:'',protocol:'',set:''})}
									value={dsString}
684
685
686
687
688
									onChange={e => {
										const str = e.target.value;
										const res = rxStringToDataset.exec(str) || [];
										const ds = {
											set: res[2] || '',
689
690
											protocol: res[1] || '',
											database: res[3] || '',
691
692
										};

693
694
										const newDs = {...this.props.data.contents.datasets, [name]: ds};
										this.setContents({...this.props.data.contents, datasets: newDs});
695
										this.setLockMap(name, true);
696
697
									}}
								>
698
									<option value=''>Dataset...</option>
699
700
									{
										this.props.datasets
701
										.filter(ds => dbSetIsValidForTcDataset(ds, tcDataset))
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
										.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>
					);
				})
718
719
720
721
			}
		</FormGroup>
	);

722
723
724
	renderBlock = (blockName: string, block: any, isAnalyzer: boolean, key: number) => {
		const tcBlock = isAnalyzer ? this.props.toolchain.contents.analyzers.find(b => b.name === blockName) : this.props.toolchain.contents.blocks.find(b => b.name === blockName);
		const algorithms = isAnalyzer ? this.props.analyzerBlocks : this.props.normalBlocks;
725

726
		/*
727
728
		if(!tcBlock)
			return;
729
730
731
732
733
		*/

		const alg = algorithms.find(nb => nb.name === block.algorithm) || {contents: {groups: []}};
		const [ inputs, outputs ] = algIOs(alg);

734
735
		const inferredTypes = this.getConnectionInferredTypes();

736
		const arrCount = (arr: string[]): any => arr.reduce((o, e) => ({...o, [e]: o.hasOwnProperty(e) ? o[e] + 1 : 1}), {});
737
738
		const possibleAlgorithms = algorithms.filter(pa => {
			const [iAlg, oAlg] = algIOs(pa);
739
			return isValidEntity(blockName, tcBlock, {inputs: iAlg, outputs: oAlg}, inferredTypes);
740
		});
741

742
		const updateBlock = (newBlock, globals = this.props.data.contents.globals, newName = blockName) => {
743
744
745
746
			if(isAnalyzer)
				delete newBlock.outputs;

			const bKey = isAnalyzer ? 'analyzers' : 'blocks';
747
			const newBlocks = changeObjFieldName(this.props.data.contents[bKey], blockName, newName);
748
			newBlocks[newName] = newBlock;
749
			this.setContents({
750
				...this.props.data.contents,
751
752
753
				[bKey]: newBlocks,
				globals,
			});
754
		};
755

756
757
		const envInfo = block.environment || this.props.data.contents.globals.environment;
		const queue = block.queue || this.props.data.contents.globals.queue;
758
759
		const envDisabled = !block.hasOwnProperty('environment');

760
		return (
761
			<FormGroup key={key} className={`block${ key } block_${ blockName }`}>
762
				<h4>
763
764
765
766
767
768
					<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>
769
					{' '}
770
771
772
773
					<Badge
						title={`Block is synchronized to the "${ tcBlock.synchronized_channel }" channel`}
						color='info'
					>{ tcBlock.synchronized_channel }</Badge>
774
					{/*
775
					{' '}
776
777
					<Button
						color={this.state.lockMap[blockName] ? 'primary' : 'secondary'}
778
						title={`When unlocked, neighboring blocks will not restrict their available algorithms to those compatible with this block's algorithm's input & output types. When locked, neighboring blocks will take into account this block's algorithm's input & output types.`}
779
						onClick={() => {
780
							this.setLockMap(blockName, !this.state.lockMap[blockName] && block.algorithm !== '');
781
782
783
784
						}}
					>
						{ this.state.lockMap[blockName] ? 'Locked' : 'Unlocked' }
					</Button>
785
					*/}
786
				</h4>
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
				<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 {
									const newBlock = jsonClone(block);
									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}
818
							availableEnvs={this.props.environments}
819
							disabled={envDisabled}
820
							updateEnvInfo={(name, version) => {
821
								const env = Object.values(this.props.environments).find(e => e.name === name && e.version === version);
822
823
824
825
826
827
828
829
830
831
832
833
								if(!env)
									return;
								const queue = Object.keys(env.queues)[0];
								updateBlock({
									...block,
									environment: {
										name,
										version,
									},
									queue,
								});
							}}
834
835
836
837
838
839
840
							updateQueue={queue => updateBlock({
								...block,
								queue,
							})}
						/>
					</Col>
				</FormGroup>
841
				<FormGroup className='algorithm'>
842
843
844
					<Label>
						Algorithm
					</Label>
845
846
847
					<Input
						type='select'
						className='custom-select'
848
						valid={block.algorithm !== ''}
849
850
851
						value={block.algorithm}
						onChange={e => {
							const str = e.target.value;
852
							const alg = algorithms.find(nb => nb.name === str) || {name: '', contents: {groups: []}};
853
							const [ inputs, outputs ] = algIOs(alg);
854
855
856
857
858
859
860
861
862
							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 '';
							};

863
864
							const thisBlock = {
								...block,
865
866
								algorithm: str,
							};
867
868
869
870
							if(tcBlock.inputs)
								thisBlock.inputs = levMapStrings(Object.keys(inputs), tcBlock.inputs);
							if(tcBlock.outputs)
								thisBlock.outputs = levMapStrings(Object.keys(outputs), tcBlock.outputs);
871

872
							const globals = {
873
								...this.props.data.contents.globals,
874
875
876
877
878
879
							};
							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}), {}),
880
									...this.props.data.contents.globals[alg.name],
881
882
								};

883
							if(globals.hasOwnProperty(block.algorithm) &&
884
885
								[...this.props.toolchain.contents.analyzers, ...this.props.toolchain.contents.blocks]
								.filter(b => b.algorithm === block.algorithm).length <= 1)
886
887
888
								delete globals[block.algorithm];

							updateBlock(thisBlock, globals);
889
							this.setLockMap(blockName, true);
890
						}}
891
					>
892
						<option value=''>Algorithm...</option>
893
						{
894
895
							possibleAlgorithms
							.map(nb => nb.name)
896
897
898
899
900
901
							.map((str, i) =>
								<option key={i} value={str}>{ str }</option>
							)
						}
					</Input>
				</FormGroup>
902
				{ Object.entries(alg.contents.parameters || {}).length > 0 && <h5>Parameters</h5> }
903
				{
904
					Object.entries(alg.contents.parameters || {}).map(([pName, paramObj], i) => {
905
						const isTethered = !block.parameters.hasOwnProperty(pName);
906
						const globalParams = this.props.data.contents.globals[alg.name];
907
						const currVal = block.parameters[pName] || globalParams[pName];
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924

						const deleteParam = () => {
							const thisBlock = {...block,
							};
							delete block.parameters[pName];
							updateBlock(thisBlock);
						};
						const updateParam = (val: ParameterValue) => {
							const thisBlock = {...block,
								parameters: {...block.parameters,
									[pName]: val
								}
							};
							updateBlock(thisBlock);
						};

						return (
925
							<FormGroup key={i} className={`parameter${ i }`}>
926
927
928
929
930
931
932
933
934
935
936
937
938
939
								<Label>
									{ pName }
								</Label>
								<FormGroup row>
									<Col sm='auto'>
										<Button
											color={isTethered ? 'secondary' : 'warning'}
											onClick={() => {
												if(isTethered){
													updateParam(currVal);
												} else {
													deleteParam();
												}
											}}
940
											title={`Toggle whether or not the block is using the global default value for this parameter, or is using a custom value for this specific block.`}
941
942
943
944
945
										>
											{ isTethered ? 'Using global defaults' : 'Using block value' }
										</Button>
									</Col>
									<Col>
946
										<ParameterConsume
947
948
949
950
951
952
953
954
955
956
											parameter={paramObj}
											currVal={currVal}
											updateFunc={updateParam}
										/>
									</Col>
								</FormGroup>
							</FormGroup>
						);
					})
				}
957
958
				<h5>Inputs</h5>
				{
959
					Object.entries(inputs).map(([iName, type], i, inputs) => {
960
						const iLabel = `${ iName } (${ type })`;
961
						const value = block.inputs[iName] || '';
962
						const foundCount = Object.values(block.inputs).filter(s => s === value).length;
963
						return (
964
							<FormGroup row key={i} className={`input${ i }`}>
965
966
								<Label sm={6}>{ iLabel }</Label>
								<Col sm={6}>
967
968
969
									<Input
										type='select'
										className='custom-select'
970
										valid={value !== '' && foundCount === 1}
971
										value={value}
972
973
974
975
976
977
										onChange={e => {
											const str = e.target.value;
											const thisBlock = {
												...block,
												inputs: {...block.inputs, [iName]: str}
											};
978
979
980
981
											for(const key in block.inputs){
												if(block.inputs[key] === str)
													thisBlock.inputs[key] = value;
											}
982
											updateBlock(thisBlock);
983
984
										}}
									>
985
										<option value=''>Input...</option>
986
987
988
989
990
991
992
993
994
995
996
997
										{
											tcBlock.inputs
											.map((str, i) =>
												<option key={i} value={str}>{ str }</option>
											)
										}
									</Input>
								</Col>
							</FormGroup>
						);
					})
				}
998
				{ Object.keys(outputs).length > 0 && <h5>Outputs</h5> }
999
				{
1000
1001
					Object.entries(outputs).map(([oName, type], i) => {
						const oLabel = `${ oName } (${ type })`;
1002
						const value = block.outputs[oName] || '';
1003
						const foundCount = Object.values(block.outputs).filter(s => s === value).length;
1004
						return (
1005
							<FormGroup row key={i} className={`output${ i }`}>
1006
1007
								<Label sm={6}>{ oLabel }</Label>
								<Col sm={6}>
1008
1009
1010
									<Input
										type='select'
										className='custom-select'
1011
										valid={value !== '' && foundCount === 1}
1012
										value={value}
1013
1014
1015
1016
1017
1018
										onChange={e => {
											const str = e.target.value;
											const thisBlock = {
												...block,
												outputs: {...block.outputs, [oName]: str}
											};
1019
1020
1021
1022
											for(const key in block.outputs){
												if(block.outputs[key] === str)
													thisBlock.outputs[key] = value;
											}
1023
											updateBlock(thisBlock);
1024
1025
										}}
									>
1026
										<option value=''>Output...</option>
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
										{
											tcBlock.outputs
											.map((str, i) =>
												<option key={i} value={str}>{ str }</option>
											)
										}
									</Input>
								</Col>
							</FormGroup>
						);
					})
				}

			</FormGroup>
		);
	}

1044
	renderBlocks = () => this.state.activeBlockInfo.set === 'blocks' && (
1045
1046
		<FormGroup>
			{
1047
				(Object.entries(this.props.data.contents.blocks): [string, any][])
1048
				.filter(([n, b]) => n === this.state.activeBlockInfo.name)
1049
1050
				.sort(([n1, ds1], [n2, ds2]) => n1 > n2 ? 1 : -1)
				.map(([name, blk], i) =>
1051
					this.renderBlock(name, blk, false, i)
1052
1053
				)
			}
1054
1055
1056
		</FormGroup>
	);

1057
	renderAnalyzers = () => this.state.activeBlockInfo.set === 'analyzers' && (
1058
		<FormGroup>
1059
1060
			<h3>Analyzers</h3>
			{
1061
				(Object.entries(this.props.data.contents.analyzers): [string, any][])
1062
				.filter(([n, b]) => n === this.state.activeBlockInfo.name)
1063
1064
				.sort(([n1, ds1], [n2, ds2]) => n1 > n2 ? 1 : -1)
				.map(([name, blk], i) =>
1065
					this.renderBlock(name, blk, true, i)
1066
1067
1068
				)
			}
		</FormGroup>
1069
	)
1070

1071
1072
1073
	renderGlobals = () => {
		const paramObjs = this.getParameterObjs();
		return (
1074
			<FormGroup className='globals'>
1075
1076
1077
1078
1079
1080
1081
1082
				<h3>
					<InfoTooltip
						id='globalSettingsTooltip'
						info={`These settings are for the entire experiment, not for a specific block.`}
					>
						Global Settings
					</InfoTooltip>
				</h3>
1083
				<EnvironmentConfig
1084
1085
					envInfo={this.props.data.contents.globals.environment}
					queue={this.props.data.contents.globals.queue}
1086
					availableEnvs={this.props.environments}
1087
					updateEnvInfo={(name, version) => this.setContents({
1088
						...this.props.data.contents,
1089
						globals: {
1090
							...this.props.data.contents.globals,
1091
							environment: {
1092
								...this.props.data.contents.globals.environment,