AlgorithmEditor.jsx 21.7 KB
Newer Older
Jaden's avatar
Jaden committed
1
// @flow
2
import * as React from 'react';
Jaden's avatar
Jaden committed
3 4 5
import {
	Row,
	Col,
6
	Button,
7 8 9 10
	Form, FormGroup,
	Label, Input,
	Card, CardHeader, CardBody,
	TabContent, TabPane, Nav, NavItem, NavLink,
11
	Alert,
12
	UncontrolledDropdown, DropdownToggle, DropdownMenu,
13
	InputGroup, InputGroupAddon,
Jaden's avatar
Jaden committed
14 15
} from 'reactstrap';

16 17
import { connect } from 'react-redux';

18 19
import cn from 'classnames';

20
import { changeObjFieldName, generateNewKey, copyObj } from '@helpers';
21
import { algorithmValidator, BUILTIN_TYPES, ANALYZER_RESULT_TYPES, getValidAlgorithmObj as getValidObj } from '@helpers/beat';
22 23 24
import type { AlgorithmValidatorObject, BeatObject } from '@helpers/beat';

import * as Selectors from '@store/selectors.js';
Jaden's avatar
Jaden committed
25

26
import * as Actions from '@store/actions.js';
27 28 29 30
import CacheInput from '../CacheInput.jsx';
import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import TypedField from '../TypedField.jsx';
31
import TemplateButton from '../EntityTemplateGenerationButton.jsx';
32
import InfoTooltip from '../InfoTooltip.jsx';
33
import ParameterCreate from '../ParameterCreate.jsx';
34

Jaden's avatar
Jaden committed
35
type Props = {
Jaden's avatar
Jaden committed
36 37 38 39
	data: BeatObject,
	algorithms: BeatObject[],
	libraries: BeatObject[],
	dataformats: BeatObject[],
40
	saveFunc: (BeatObject) => any,
41
	updateFunc: (BeatObject) => any,
Jaden's avatar
Jaden committed
42 43
};

44
type State = {
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
45
	// which tab the user is viewing (endpoints, params, etc.)
46
	activeTab: string,
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
47
	// algorithm-specific validity checking (alerts for common errors & warnings)
48
	validity: AlgorithmValidatorObject,
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
49 50
	// parameters can restrict their values to individual choices
	// this string represents a choice that is being edited
51
	choiceCache: string,
Jaden's avatar
Jaden committed
52 53
};

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
54 55
// represents an input or output value
// inputs/outputs are just an object with a "type" field
56 57 58 59
type IOEntry = {|
	type: string,
|};

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
60
// represents a collection of inputs/outputs
61 62 63 64
type IOObject = {
	[string]: IOEntry,
};

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
65
// represents a group/endpoint, optionally with outputs
66 67 68 69 70 71
type Group = {
	name: string,
	inputs: IOObject,
	outputs?: IOObject,
};

72
export class AlgorithmEditor extends React.Component<Props, State> {
73
	constructor(props: Props) {
74
		//console.log(`Creating AlgDetail`);
75 76 77 78 79
		super(props);
	}

	state = {
		activeTab: '0',
Jaden's avatar
Jaden committed
80
		validity: algorithmValidator(this.props.data, this.props.algorithms),
81
		choiceCache: '',
82 83
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
84
	// if the data changes behind the scenes, update the editor with these changes
85 86 87 88 89 90
	componentDidUpdate = (prevProps: Props) => {
		if(this.props !== prevProps){
			this.setState({
				validity: this.getValidity(this.props.data, this.props.algorithms),
			});
		}
91 92
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
93
	// get the result types (analyzer result types are restricted to a subset of all available types)
94
	getResultDfs = (): string[] => ANALYZER_RESULT_TYPES.concat(this.props.dataformats.filter(d => d.name.startsWith('plot')).map(d => d.name))
95

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
96
	// returns a validity object based on the given algorithm cache and list of all algorithms
97
	getValidity = (cache: BeatObject = this.props.data, algs: BeatObject[] = this.props.algorithms): AlgorithmValidatorObject => {
98 99
		return algorithmValidator(cache, algs.filter(o => o !== this.props.data));
	}
Jaden's avatar
Jaden committed
100

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
101 102
	// much like getValidity but just returns a bool
	// doesnt worry about warnings, just errors
103
	isValid = (cache: BeatObject): boolean => {
104
		const isValid = Object.entries(this.getValidity(cache))
Jaden's avatar
Jaden committed
105
		.filter(([name, bool]) => name !== (this.isAnalyzer() ? 'endpoint0OutputExists' : 'result0Exists'))
106
		.reduce((bool, [name, b]): boolean => (!!b) && bool, true)
Jaden's avatar
Jaden committed
107 108 109 110 111
		;

		return isValid;
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
112
	// change the tab
113
	tabTo = (tab: string) => { if(this.state.activeTab !== tab) this.setState({ activeTab: tab }); }
114

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
115 116
	// is the algorithm an analyzer?
	// theres no flag set, so instead check for the existence of a 'splittable' field which only normal algorithms have
117
	isAnalyzer = (): boolean => { return !Object.keys(this.props.data.contents).includes('splittable');  }
Jaden's avatar
Jaden committed
118

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
119 120
	// helper to change a value in the "contents" subobject of an algorithm
	// (this is where the vast majority of change happens)
121
	changeContentsVal = (field: string, val: any) => {
Jaden's avatar
Jaden committed
122
		const newCache = {
123
			...this.props.data,
Jaden's avatar
Jaden committed
124
			contents: {
125
				...this.props.data.contents,
Jaden's avatar
Jaden committed
126
				[field]: val
127
			}
Jaden's avatar
Jaden committed
128 129
		};

130
		this.props.updateFunc(newCache);
131 132
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
133
	// updates an old group object to a new group object
134
	updateGroup = (oldGroup: Group, newGroup: Group) => {
135
		const gIdx = this.props.data.contents.groups.findIndex(g => JSON.stringify(g) === JSON.stringify(oldGroup));
136
		const newGroups = copyObj(this.props.data.contents.groups);
137 138 139 140
		newGroups[gIdx] = newGroup;
		this.changeContentsVal('groups', newGroups);
	}

141 142 143 144 145 146
	// 4 different functions:
	// creates a parameter (provide name & value)
	// updates a parameter (provide name & value)
	// renames a parameter (provide name & value & the old name)
	// deletes a parameter (provide null as name, value is optional, the name)
	updateParameter = (name: null | string, value: any, oldName: ?string) => {
147
		const params = copyObj(this.props.data.contents.parameters);
148 149 150 151 152
		if(oldName)
			delete params[oldName];
		if(name !== null)
			params[name] = value;
		this.changeContentsVal('parameters', params);
153 154
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
155
	// updates a result given the result name and the value obj
156
	updateResult = (name: string, res: any) => {
157
		const rs = this.props.data.contents.results;
158 159 160 161
		rs[name] = res;
		this.changeContentsVal('results', rs);
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
162
	// toggles the algorithm to an analyzer algorithm
163
	changeToAnalyzer = () => {
164
		const contents = copyObj(this.props.data.contents);
165 166 167 168 169
		contents.groups = contents.groups.map(g => {
			delete g['outputs'];
			return g;
		});
		contents.results = {};
170
		delete contents['splittable'];
Jaden's avatar
Jaden committed
171 172

		const newCache = {
173
			...this.props.data,
Jaden's avatar
Jaden committed
174 175 176
			contents
		};

177
		this.props.updateFunc(newCache);
178 179
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
180
	// toggles the algorithm to a normal algorithm
181
	changeToNormal = () => {
182
		const contents = copyObj(this.props.data.contents);
183 184
		delete contents.results;
		if(contents.groups.length > 0)
185
			contents.groups[0].outputs = {'output': { type: '' }};
186
		contents.splittable = false;
Jaden's avatar
Jaden committed
187 188

		const newCache = {
189
			...this.props.data,
Jaden's avatar
Jaden committed
190 191 192
			contents
		};

193
		this.props.updateFunc(newCache);
194 195
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
196 197
	// generates a func from a given group & type that takes an input/output name
	// and deletes it from the group
198 199 200
	deleteIO = (group: Group, type: 'inputs' | 'outputs') => (name: string) => {
		if(!group[type])
			return;
201 202 203 204
		delete group[type][name];
		this.updateGroup(group, { ...group });
	}

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
205
	// rendergs the endpoints tab
206 207 208
	renderEndpoints = () => (
		<TabPane tabId='0'>
			{
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
209
				// loop through all groups in the alg
210
				this.props.data.contents.groups.map((group: Group, i, groups: Group[]) => {
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
211 212
					// func to update an input/output
					// lets you change the name and/or the type
213
					const ioUpdate = (subObj: 'inputs' | 'outputs') => (oldName: string, newName: string, newObj: IOEntry) => {
214 215 216 217 218 219 220 221 222 223 224 225 226 227
						const changedObj = changeObjFieldName(group[subObj], oldName, newName);
						changedObj[newName] = newObj;
						this.updateGroup(group,
							{
								...group,
								[subObj]: changedObj
							}
						);
					};

					const inputsUpdate = ioUpdate('inputs');
					const outputsUpdate = ioUpdate('outputs');

					return (
228
						<Row key={i} className='mb-2' id={`endpoint${ i }`}>
229 230
							<Col sm='12'>
								<Card>
231 232 233 234 235
									{ groups.length > 1 &&
											<CardHeader>
												Group { i }
											</CardHeader>
									}
236
									<CardBody>
237
										{ Object.keys(group.inputs).length === 0 && i !== 0 &&
238
												<Alert color='warning'>
239
													In general, an endpoint group should have at least 1 input.
240 241 242 243 244
												</Alert>
										}
										<Row>
											<Col sm={group.outputs ? '6' : '12'}>
												{
245
													(Object.entries(group.inputs): [string, any][])
246
													.map(([name, ioObj], i, gEntries) =>
247
														<TypedField
248
															className='algInput'
249 250
															key={i}
															name={name}
251 252 253 254 255 256 257 258 259 260
															type={ioObj.type}
															types={this.props.dataformats.map(d => d.name)}
															existingFields={gEntries.map(([n, v]) => n)}
															nameUpdateFunc={str => inputsUpdate(name, str, ioObj)}
															typeUpdateFunc={str => inputsUpdate(
																name,
																name,
																{ type: str }
															)}
															deleteFunc={() => this.deleteIO(group, 'inputs')(name)}
261 262 263
														/>
													)
												}
264 265
												<Button outline block
													id='newInputBtn'
266 267 268 269 270 271 272 273 274 275
													onClick={(e) => {
														const newKey = generateNewKey('input', Object.keys(group.inputs));
														this.updateGroup(
															group,
															{
																...group,
																inputs: {
																	...group.inputs,
																	[newKey]: { type: '' }
																}
276
															}
277 278
														);
													}}
279 280 281 282 283 284 285
												>
													New Input
												</Button>
											</Col>
											{ group.outputs && (
												<Col sm='6'>
													{
286
														(Object.entries(group.outputs): [string, any][])
287
														.map(([name, ioObj], i, gEntries) =>
288
															<TypedField
289
																className='algOutput'
290 291
																key={i}
																name={name}
292 293 294 295 296 297 298 299 300 301
																type={ioObj.type}
																types={this.props.dataformats.map(d => d.name)}
																existingFields={gEntries.map(([n, v]) => n)}
																nameUpdateFunc={str => outputsUpdate(name, str, ioObj)}
																typeUpdateFunc={str => outputsUpdate(
																	name,
																	name,
																	{ type: str }
																)}
																deleteFunc={() => this.deleteIO(group, 'outputs')(name)}
302 303 304 305
															/>
														)
													}
													<Button outline block
306
														id='newOutputBtn'
307 308 309 310 311 312 313 314 315 316
														onClick={(e) => {
															const newKey = generateNewKey('output', Object.keys(group.outputs));
															this.updateGroup(
																group,
																{
																	...group,
																	outputs: {
																		...group.outputs,
																		[newKey]: { type: '' }
																	}
317
																}
318 319
															);
														}}
320 321 322 323 324 325 326
													>
														New Output
													</Button>
												</Col>
											)}
										</Row>
									</CardBody>
327 328 329 330 331 332 333 334 335 336 337
									{ i !== 0 &&
										<Button block color='danger' outline
											onClick={() => this.changeContentsVal(
												'groups',
												groups
												.filter(g => g.name !== group.name)
											)}
										>
											Delete Group
										</Button>
									}
338 339 340 341 342 343
								</Card>
							</Col>
						</Row>
					);
				})
			}
344
			<Button block
345 346
				id='newGroupBtn'
				onClick={(e) => {
347
					const newGroupName = generateNewKey('group', this.props.data.contents.groups.map(g => g.name));
348
					const newGroup = {
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
349
						name: newGroupName,
350 351
						inputs: {},
					};
352
					if(!this.isAnalyzer() && this.props.data.contents.groups.length === 0)
353 354 355 356
						newGroup.outputs = {};

					this.changeContentsVal(
						'groups',
357
						[... this.props.data.contents.groups, newGroup ]
358 359
					);
				}}
360 361 362 363 364 365
			>
				New Group
			</Button>
		</TabPane>
	);

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
366
	// renders the parameters tab
367 368 369 370
	renderParameters = () => {
		return (
			<TabPane tabId='1'>
				{
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
371
					// loop through all the parameters
372
					(Object.entries(this.props.data.contents.parameters): [string, any][]).map(([name, param], i, params) => (
373 374
						<Row key={i} className='mb-2'>
							<Col sm='12'>
375
								<ParameterCreate
376
									name={name}
377 378 379 380
									param={param}
									params={params}
									updateParameter={this.updateParameter}
								/>
381 382 383
							</Col>
						</Row>
					))
384
				}
385
				<Button outline block
386
					id='newParameterBtn'
387
					onClick={() => {
388
						const newKey = generateNewKey('parameter', Object.keys(this.props.data.contents.parameters || {}));
389 390
						this.changeContentsVal('parameters',
							{
391
								...(this.props.data.contents.parameters || {}),
392 393 394 395 396
								[newKey]: {
									type: '',
									default: '',
									description: '',
								}
397
							}
398 399
						);
					}}
400 401 402 403 404 405
				>
					New Parameter
				</Button>
			</TabPane>
		);
	}
406

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
407
	// renders the libraries tab
408 409 410 411
	renderLibraries = () => (
		<TabPane tabId='2'>
			<Row>
				<Col sm='12'>
412
					{
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
413
						// loop through all the used libs
414
						(Object.entries(this.props.data.contents.uses): [string, any][])
415 416 417 418 419 420 421 422
						.map(([name, lib], i, lEntries) => (
							<TypedField
								key={i}
								name={name}
								type={lib}
								types={this.props.libraries.map(l => l.name)}
								existingFields={lEntries.map(([n, v]) => n)}
								nameUpdateFunc={str => {
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
423
									// update the alias
424 425
									this.changeContentsVal('uses',
										changeObjFieldName(
426
											this.props.data.contents.uses,
427 428 429 430 431 432
											name,
											str
										)
									);
								}}
								typeUpdateFunc={str => {
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
433
									// update the chosen library for the alias
434
									const libs = {
435
										...this.props.data.contents.uses,
436 437 438 439 440
										[name]: str
									};
									this.changeContentsVal('uses', libs);
								}}
								deleteFunc={() => {
441
									const libs = { ...this.props.data.contents.uses };
442 443 444 445 446
									delete libs[name];
									this.changeContentsVal('uses', libs);
								}}
							/>
						))
447 448 449 450
					}
				</Col>
			</Row>
			<Button outline block
451
				id='newLibraryBtn'
452
				onClick={() => {
453
					const newKey = generateNewKey('library', Object.keys(this.props.data.contents.uses));
454 455
					this.changeContentsVal('uses',
						{
456
							...this.props.data.contents.uses,
457 458 459 460
							[newKey]: ''
						}
					);
				}}
461 462 463 464 465 466
			>
				New Library
			</Button>
		</TabPane>
	);

Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
467
	// renders the results tab (only if the alg is an analyzer)
468 469 470
	renderResults = () => (
		<TabPane tabId='3'>
			{
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
471
				// loop through results
472
				(Object.entries(this.props.data.contents.results): [string, any][])
473
				.map(([name, result], i, rEntries) => (
474 475 476 477 478 479 480 481 482
					<TypedField
						key={i}
						name={name}
						type={result.type}
						types={this.getResultDfs()}
						existingFields={rEntries.map(([n, v]) => n)}
						nameUpdateFunc={str => this.changeContentsVal(
							'results',
							changeObjFieldName(
483
								this.props.data.contents.results,
484
								name,
485
								str
486 487 488 489 490 491 492 493 494 495
							)
						)}
						typeUpdateFunc={str => this.updateResult(name,
							{
								...result,
								type: str
							}
						)}
						deleteFunc={() => {
							const newRes = {
496
								...this.props.data.contents.results
497 498 499 500
							};
							delete newRes[name];
							this.changeContentsVal(
								'results',
501
								newRes
502 503 504
							);
						}}
					>
505
						<Col>
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
506
							{/* display by default checkbox */}
507 508 509
							<Label check>
								<Input
									type='checkbox'
510 511 512
									value={result.display ? true : false}
									onChange={(e) => this.changeContentsVal(
										'results',
513
										{ ...this.props.data.contents.results,
514 515 516 517 518 519 520 521 522
											[name] : {
												...result,
												display: e.target.checked
											}
										})
									}
								/>
								{' '}
								<span>Display By Default</span>
523
							</Label>
524
						</Col>
525
					</TypedField>
526 527 528
				))
			}
			<Button outline block
529
				id='newResultBtn'
530
				onClick={() => {
531
					const newKey = generateNewKey('result', Object.keys(this.props.data.contents.results));
532 533
					this.changeContentsVal(
						'results',
534
						{...this.props.data.contents.results,
535 536 537 538
							[newKey]: {
								display: false,
								type: ''
							}
539
						}
540 541
					);
				}}
542 543 544 545 546 547 548
			>
				New Result
			</Button>
		</TabPane>
	);

	render = () => (
549
		<div>
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
550
			{/* header content (save button & template gen button) */}
551 552 553 554 555
			<div className='d-flex'>
				<Button
					className='mx-auto'
					outline
					color='secondary'
556
					onClick={() => this.props.saveFunc(this.props.data)}
557
				>
558
					Save Changes (Changes are <ValidSchemaBadge entity='algorithm' obj={this.props.data} />)
559
				</Button>
560
				<TemplateButton
561
					data={this.props.data}
562 563
					entity={'algorithm'}
				/>
564
			</div>
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
565
			{/* simple universal stuff - description, what type of alg, if its splittable */}
566
			<Form>
567
				<React.Fragment>
568 569 570 571 572 573 574 575
				<FormGroup tag='fieldset'>
					<FormGroup>
						<Label for='algDesc'>Short Description</Label>
						<Input
							type='text'
							name='desc'
							id='algDesc'
							placeholder='Algorithm description...'
576
							value={this.props.data.contents.description}
577
							onChange={(e) => this.changeContentsVal('description', e.target.value)}
578 579 580 581 582
						/>
					</FormGroup>
					<FormGroup check>
						<Label check>
							<Input
583
								id='algAnalyzer'
584 585 586 587
								type='checkbox'
								checked={this.isAnalyzer()}
								onChange={e => e.target.checked ? this.changeToAnalyzer() : this.changeToNormal()}
							/>{' '}
588 589 590 591 592 593
							<InfoTooltip
								id='analyzerTooltip'
								info={`Checking this option makes this algorithm an "analyzer". Analyzers are made to analyze the experiment's performance (and other characteristics). Experiments must have 1 and only analyzer at the end of the experiment's toolchain. These types of algorithms produce evalution data for the user, such as plots, scores, and statistics.`}
							>
								Analyser
							</InfoTooltip>
594 595
						</Label>
					</FormGroup>
596 597 598 599
					{ !this.isAnalyzer() &&
							<FormGroup check>
								<Label check>
									<Input
600
										id='algSplittable'
601
										type='checkbox'
602
										checked={this.props.data.contents.splittable ? true : false}
603 604
										onChange={e => this.changeContentsVal('splittable', e.target.checked)}
									/>{' '}
605 606 607 608 609 610
									<InfoTooltip
										id='splittableTooltip'
										info={`A non-analyzer algorithm may be decided to be "splittable". Being splittable means that multiple instances of the algorithm may be ran in parallel on different bits of input. Non-splittable algorithms usually have some sort of aggregation/cross-input activity. Most algorithms will be splittable, but it is much easier to debug an algorithm with it being non-splittable.`}
									>
										Splittable
									</InfoTooltip>
611 612 613
								</Label>
							</FormGroup>
					}
614
				</FormGroup>
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
615
				{/* most errors/warnings from the current AlgorithmValidator object are displayed here */}
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
				{
					(!this.isAnalyzer() || this.getValidity().result0Exists) ||
						<Alert color='danger'>
							You need at least 1 result from an Analyzer.
						</Alert>
				}
				{
					this.getValidity().endpoint0Exists ||
						<Alert color='danger'>
							You need at least 1 endpoint group.
						</Alert>
				}
				{
					(this.isAnalyzer() || this.getValidity().endpoint0OutputExists) ||
						<Alert color='danger'>
							You must have at least 1 output
							in the first endpoint group.
						</Alert>
				}
				{
					this.getValidity().endpoint0InputExists ||
						<Alert color='danger'>
							You need at least 1 input in the first endpoint group.
						</Alert>
				}
Jaden DIEFENBAUGH's avatar
Jaden DIEFENBAUGH committed
641
				{/* all the tabs: endpoints, params, libs, results */}
642 643 644 645 646 647
				<Nav tabs className='mt-3 mb-3'>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '0' })}
							onClick={() => this.tabTo('0')}
						>
648 649 650 651 652 653
							<InfoTooltip
								id='endpointTooltip'
								info={`Endpoints are groups of inputs & outputs. An algorithm can have multiple endpoints (and must have at least 1), but each endpoint must have at least 1 input. If the algorithm is an analyzer, no groups can have outputs. If it is not an analyzer, only the first endpoint can have outputs, and at least 1 output is required.`}
							>
								Endpoints
							</InfoTooltip>
654 655 656 657 658 659 660
						</NavLink>
					</NavItem>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '1' })}
							onClick={() => this.tabTo('1')}
						>
661 662 663 664 665 666
							<InfoTooltip
								id='parametersTooltip'
								info={`Parameters are completely optional, allowing for users to change the functionality of the block when configuring an experiment. Parameters lend themselves to building reusable and flexible blocks. For example, a parameter could allow configuring the number of components in PCA, or choosing kernels/kernel parameters for an SVM.`}
							>
								Parameters
							</InfoTooltip>
667 668 669 670 671 672 673
						</NavLink>
					</NavItem>
					<NavItem>
						<NavLink
							className={cn({ active: this.state.activeTab === '2' })}
							onClick={() => this.tabTo('2')}
						>
674 675 676 677 678 679
							<InfoTooltip
								id='libraryTooltip'
								info={`Libraries provide DRYness to code development for objects in the BEAT platform. Libraries are groups of functions under a given namespace. You may use libraries by adding it to your algorithm and referencing the namespace in your algorithm by the nickname you provide here.`}
							>
								Libraries
							</InfoTooltip>
680 681 682
						</NavLink>
					</NavItem>
					{ this.isAnalyzer() &&
683 684
							<NavItem>
								<NavLink
685 686
									className={cn({ active: this.state.activeTab === '3' })}
									onClick={() => this.tabTo('3')}
687
								>
688 689 690 691 692 693
									<InfoTooltip
										id='resultsTooltip'
										info={`Results are only for analyzer algorithms. They are similar to outputs in normal algorithms, but provide evaluation information on the whole experiment instead of output data to be processed by another block. Examples are plot data (for example, data for an ROC curve), scores, and statistics.`}
									>
										Results
									</InfoTooltip>
694 695
								</NavLink>
							</NavItem>
696 697 698 699 700 701
					}
				</Nav>
				<TabContent className='mb-3' activeTab={this.state.activeTab}>
					{ this.renderEndpoints() }
					{ this.renderParameters() }
					{ this.renderLibraries() }
702
					{ this.props.data.contents.results && this.renderResults() }
703
				</TabContent>
704
			</React.Fragment>
705 706 707
			</Form>
		</div>
	);
708 709
}

710
const mapStateToProps = (state, ownProps) => {
711
	const algs = Selectors.algorithmGet(state);
712
	const obj = {
713
		algorithms: algs,
714 715
		libraries: Selectors.libraryGet(state),
		dataformats: Selectors.dataformatGet(state),
716
		data: algs[ownProps.index] || getValidObj()
717 718 719 720
	};
	return obj;
};

721 722 723 724 725 726 727 728 729
const mapDispatchToProps = (dispatch, ownProps) => ({
	// replace the obj in the Redux store with the new object
	updateFunc: (obj) => {
		console.log(`dispatching for ${ obj.name }`);
		dispatch(Actions[`algorithmUpdate`](obj.name, obj));
	},
});

export default connect(mapStateToProps, mapDispatchToProps)(AlgorithmEditor);