Rift.IO OSM R1 Initial Submission
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelProperties.js
1 /*
2 *
3 * Copyright 2016 RIFT.IO Inc
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18 /**
19 * Created by onvelocity on 1/18/16.
20 *
21 * This class generates the form fields used to edit the CONFD JSON model.
22 */
23 'use strict';
24
25 import _ from 'lodash'
26 import utils from '../libraries/utils'
27 import React from 'react'
28 import ClassNames from 'classnames'
29 import changeCase from 'change-case'
30 import toggle from '../libraries/ToggleElementHandler'
31 import Button from './Button'
32 import Property from '../libraries/model/DescriptorModelMetaProperty'
33 import ComposerAppActions from '../actions/ComposerAppActions'
34 import CatalogItemsActions from '../actions/CatalogItemsActions'
35 import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
36 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
37 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
38 import SelectionManager from '../libraries/SelectionManager'
39 import DeletionManager from '../libraries/DeletionManager'
40 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
41 import getEventPath from '../libraries/getEventPath'
42
43 import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
44 import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
45
46 import '../styles/EditDescriptorModelProperties.scss'
47
48 function getDescriptorMetaBasicForType(type) {
49 const basicPropertiesFilter = d => _.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
50 return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
51 }
52
53 function getDescriptorMetaAdvancedForType(type) {
54 const advPropertiesFilter = d => !_.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
55 return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
56 }
57
58 function getTitle(model = {}) {
59 if (typeof model['short-name'] === 'string' && model['short-name']) {
60 return model['short-name'];
61 }
62 if (typeof model.name === 'string' && model.name) {
63 return model.name;
64 }
65 if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
66 return model.uiState.displayName
67 }
68 if (typeof model.id === 'string') {
69 return model.id;
70 }
71 }
72
73 export default function EditDescriptorModelProperties(props) {
74
75 const container = props.container;
76
77 if (!(DescriptorModelFactory.isContainer(container))) {
78 return
79 }
80
81 function startEditing() {
82 DeletionManager.removeEventListeners();
83 }
84
85 function endEditing() {
86 DeletionManager.addEventListeners();
87 }
88
89 function onClickSelectItem(property, path, value, event) {
90 event.preventDefault();
91 const root = this.getRoot();
92 if (SelectionManager.select(value)) {
93 CatalogItemsActions.catalogItemMetaDataChanged(root.model);
94 }
95 }
96
97 function onFocusPropertyFormInputElement(property, path, value, event) {
98
99 event.preventDefault();
100 startEditing();
101
102 function removeIsFocusedClass(event) {
103 event.target.removeEventListener('blur', removeIsFocusedClass);
104 Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
105 }
106
107 removeIsFocusedClass(event);
108
109 const propertyWrapper = getEventPath(event).reduce((parent, element) => {
110 if (parent) {
111 return parent;
112 }
113 if (!element.classList) {
114 return false;
115 }
116 if (element.classList.contains('property')) {
117 return element;
118 }
119 }, false);
120
121 if (propertyWrapper) {
122 propertyWrapper.classList.add('-is-focused');
123 event.target.addEventListener('blur', removeIsFocusedClass);
124 }
125
126 }
127
128 function buildAddPropertyAction(container, property, path) {
129 function onClickAddProperty(property, path, event) {
130 event.preventDefault();
131 //SelectionManager.resume();
132 const create = Property.getContainerCreateMethod(property, this);
133 if (create) {
134 const model = null;
135 create(model, path, property);
136 } else {
137 const name = path.join('.');
138 const value = Property.createModelInstance(property);
139 utils.assignPathValue(this.model, name, value);
140 }
141 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
142 }
143 return (
144 <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
145 );
146 }
147
148 function buildRemovePropertyAction(container, property, path) {
149 function onClickRemoveProperty(property, path, event) {
150 event.preventDefault();
151 const name = path.join('.');
152 const removeMethod = Property.getContainerMethod(property, this, 'remove');
153 if (removeMethod) {
154 removeMethod(utils.resolvePath(this.model, name));
155 } else {
156 utils.removePathValue(this.model, name);
157 }
158 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
159 }
160 return (
161 <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
162 );
163 }
164
165 function onFormFieldValueChanged(event) {
166 if (DescriptorModelFactory.isContainer(this)) {
167 event.preventDefault();
168 const name = event.target.name;
169 const value = event.target.value;
170 utils.assignPathValue(this.model, name, value);
171 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
172 }
173 }
174
175 function buildField(container, property, path, value, fieldKey) {
176
177 const name = path.join('.');
178 const isEditable = true;
179 const isGuid = Property.isGuid(property);
180 const onChange = onFormFieldValueChanged.bind(container);
181 const isEnumeration = Property.isEnumeration(property);
182 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
183 const placeholder = changeCase.title(property.name);
184 const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
185 if (isEnumeration) {
186 const enumeration = Property.getEnumeration(property, value);
187 const options = enumeration.map((d, i) => {
188 // note yangforge generates values for enums but the system does not use them
189 // so we categorically ignore them
190 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
191 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
192 return <option key={fieldKey.toString() + ':' + i} value={d.name}>{d.name}</option>;
193 });
194 const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
195 if (!isValueSet || property.cardinality === '0..1') {
196 const noValueDisplayText = changeCase.title(property.name);
197 options.unshift(<option key={'(value-not-in-enum)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
198 }
199 return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={value} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} readOnly={!isEditable}>{options}</select>;
200 }
201
202 if (property['preserve-line-breaks']) {
203 return <textarea key={fieldKey.toString()} cols="5" id={fieldKey.toString()} name={name} value={value} placeholder={placeholder} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing} readOnly={!isEditable} />;
204 }
205
206 return <input key={fieldKey.toString()}
207 id={fieldKey.toString()}
208 type="text"
209 name={name}
210 value={value}
211 className={className}
212 placeholder={placeholder}
213 onChange={onChange}
214 onFocus={onFocus}
215 onBlur={endEditing}
216 onMouseDown={startEditing}
217 onMouseOver={startEditing}
218 onMouseOut={endEditing}
219 onMouseLeave={endEditing}
220 readOnly={!isEditable}
221 />;
222
223 }
224
225 function buildElement(container, property, valuePath, value) {
226 return property.properties.map((property, index) => {
227 let childValue;
228 const childPath = valuePath.slice();
229 if (typeof value === 'object') {
230 childValue = value[property.name];
231 }
232 childPath.push(property.name);
233
234 return build(container, property, childPath, childValue);
235
236 });
237 }
238
239 function buildChoice(container, property, path, value, key) {
240
241 function onFormFieldValueChanged(event) {
242 if (DescriptorModelFactory.isContainer(this)) {
243
244 event.preventDefault();
245
246 const name = event.target.name;
247 const value = event.target.value;
248
249 /*
250 Transient State is stored for convenience in the uiState field.
251 The choice yang type uses case elements to describe the "options".
252 A choice can only ever have one option selected which allows
253 the system to determine which type is selected by the name of
254 the element contained within the field.
255 */
256
257 //const stateExample = {
258 // uiState: {
259 // choice: {
260 // 'conf-config': {
261 // selected: 'rest',
262 // 'case': {
263 // rest: {},
264 // netconf: {},
265 // script: {}
266 // }
267 // }
268 // }
269 // }
270 //};
271
272 const statePath = ['uiState.choice'].concat(name);
273 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
274 // write state back to the model so the new state objects are captured
275 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
276
277 // write the current choice value into the state
278 const choiceObject = utils.resolvePath(this.model, [name, stateObject.selected].join('.'));
279 if (choiceObject) {
280 utils.assignPathValue(stateObject, ['case', stateObject.selected].join('.'), _.cloneDeep(choiceObject));
281 }
282
283 // remove the current choice value from the model
284 utils.removePathValue(this.model, [name, stateObject.selected].join('.'));
285
286 // get any state for the new selected choice
287 const newChoiceObject = utils.resolvePath(stateObject, ['case', value].join('.')) || {};
288
289 // assign new choice value to the model
290 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
291
292 // update the selected name
293 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
294
295 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
296 }
297 }
298
299 const caseByNameMap = {};
300
301 const onChange = onFormFieldValueChanged.bind(container);
302
303 const cases = property.properties.map(d => {
304 if (d.type === 'case') {
305 caseByNameMap[d.name] = d.properties[0];
306 return {optionName: d.name, optionTitle: d.description};
307 }
308 caseByNameMap[d.name] = d;
309 return {optionName: d.name};
310 });
311
312 const options = [{optionName: ''}].concat(cases).map((d, i) => {
313 return (
314 <option key={i} value={d.optionName} title={d.optionTitle}>
315 {d.optionName}
316 {i ? null : changeCase.title(property.name)}
317 </option>
318 );
319 });
320
321 const selectName = path.join('.');
322 const selectedOptionPath = ['uiState.choice', selectName, 'selected'].join('.');
323 const selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
324 const valueProperty = caseByNameMap[selectedOptionValue] || {properties: []};
325
326 const valueResponse = valueProperty.properties.map((d, i) => {
327 const childPath = path.concat(valueProperty.name, d.name);
328 const childValue = utils.resolvePath(container.model, childPath.join('.'));
329 return (
330 <div key={childPath.concat('info', i).join(':')}>
331 {build(container, d, childPath, childValue, props)}
332 </div>
333 );
334 });
335
336 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
337
338 return (
339 <div key={key} className="choice">
340 <select key={Date.now()} className={ClassNames({'-value-not-set': !selectedOptionValue})} name={selectName} value={selectedOptionValue} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing}>
341 {options}
342 </select>
343 {valueResponse}
344 </div>
345 );
346
347 }
348
349 function buildSimpleListItem(container, property, path, value, key, index) {
350 // todo need to abstract this better
351 const title = getTitle(value);
352 var req = require.context("../", true, /\.svg/);
353 return (
354 <div>
355 <a href="#select-list-item" key={Date.now()} className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
356 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
357 <span>{title}</span>
358 </a>
359 {buildRemovePropertyAction(container, property, path)}
360 </div>
361 );
362 }
363
364 function buildRemoveListItem(container, property, valuePath, fieldKey, index) {
365 const className = ClassNames(property.name + '-remove actions');
366 return (
367 <div key={fieldKey.concat(index).join(':')} className={className}>
368 <h3>
369 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
370 <span className="info">{index + 1}</span>
371 {buildRemovePropertyAction(container, property, valuePath)}
372 </h3>
373 </div>
374 );
375 }
376
377 function buildLeafListItem(container, property, valuePath, value, key, index) {
378 // look at the type to determine how to parse the value
379 return (
380 <div>
381 {buildRemoveListItem(container, property, valuePath, key, index)}
382 {buildField(container, property, valuePath, value, key)}
383 </div>
384
385 );
386 }
387
388 function build(container, property, path, value, props = {}) {
389
390 const fields = [];
391 const isLeaf = Property.isLeaf(property);
392 const isArray = Property.isArray(property);
393 const isObject = Property.isObject(property);
394 const isLeafList = Property.isLeafList(property);
395 const fieldKey = [container.id].concat(path);
396 const isRequired = Property.isRequired(property);
397 const title = changeCase.titleCase(property.name);
398 const columnCount = property.properties.length || 1;
399 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
400 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
401
402 if (!property.properties && isObject) {
403 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
404 property.properties = uiState.properties;
405 }
406
407 const hasProperties = _.isArray(property.properties) && property.properties.length;
408 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
409
410 // ensure value is not undefined for non-leaf property types
411 if (isObject) {
412 if (typeof value !== 'object') {
413 value = isArray ? [] : {};
414 }
415 }
416 const valueAsArray = _.isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
417
418 const isMetaField = property.name === 'meta';
419 const isCVNFD = property.name === 'constituent-vnfd';
420 const isSimpleListView = Property.isSimpleList(property);
421
422 valueAsArray.forEach((value, index) => {
423
424 let field;
425 const key = fieldKey.slice();
426 const valuePath = path.slice();
427
428 if (isArray) {
429 valuePath.push(index);
430 key.push(index);
431 }
432
433 if (isMetaField) {
434 if (typeof value === 'object') {
435 value = JSON.stringify(value, undefined, 12);
436 } else if (typeof value !== 'string') {
437 value = '{}';
438 }
439 }
440
441 if (isMissingDescriptorMeta) {
442 field = <span key={key.concat('warning').join(':')} className="warning">No Descriptor Meta for {property.name}</span>;
443 } else if (property.type === 'choice') {
444 field = buildChoice(container, property, valuePath, value, key.join(':'));
445 } else if (isSimpleListView) {
446 field = buildSimpleListItem(container, property, valuePath, value, key, index);
447 } else if (isLeafList) {
448 field = buildLeafListItem(container, property, valuePath, value, key, index);
449 } else if (hasProperties) {
450 field = buildElement(container, property, valuePath, value, key.join(':'))
451 } else {
452 field = buildField(container, property, valuePath, value, key.join(':'));
453 }
454
455 function onClickLeaf(property, path, value, event) {
456 if (event.isDefaultPrevented()) {
457 return;
458 }
459 event.preventDefault();
460 event.stopPropagation();
461 this.getRoot().uiState.focusedPropertyPath = path.join('.');
462 console.log('property selected', path.join('.'));
463 ComposerAppActions.propertySelected([path.join('.')]);
464 }
465
466 const clickHandler = isLeaf ? onClickLeaf : () => {};
467 const isContainerList = isArray && !(isSimpleListView || isLeafList);
468
469 fields.push(
470 <div key={fieldKey.concat(['property-content', index]).join(':')}
471 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
472 onClick={clickHandler.bind(container, property, valuePath, value)}>
473 {isContainerList ? buildRemoveListItem(container, property, valuePath, fieldKey, index) : null}
474 {field}
475 </div>
476 );
477
478 });
479
480 classNames['-is-leaf'] = isLeaf;
481 classNames['-is-array'] = isArray;
482 classNames['cols-' + columnCount] = isColumnar;
483
484 if (property.type === 'choice') {
485 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
486 }
487
488 let displayValue = typeof value === 'object' ? '' : value;
489 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
490
491 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
492
493 return (
494 <div key={fieldKey.join(':')} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
495 <h3 className="property-label">
496 <label htmlFor={fieldKey.join(':')}>
497 <span className={property.type + '-name name'}>{title}</span>
498 <small>
499 <span className={property.type + '-info info'}>{displayValueInfo}</span>
500 <span className={property.type + '-value value'}>{displayValue}</span>
501 </small>
502 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
503 </label>
504 </h3>
505 <span className={property.type + '-description description'}>{property.description}</span>
506 <val className="property-value">
507 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
508 {fields}
509 </val>
510 </div>
511 );
512
513 }
514
515 const containerType = container.uiState['qualified-type'] || container.uiState.type;
516 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
517
518 function buildBasicGroup() {
519 if (basicProperties.length === 0) {
520 return null;
521 }
522 return (
523 <div className="basic-properties-group">
524 <h2>Basic</h2>
525 <div>
526 {basicProperties.map(property => {
527 const path = [property.name];
528 const value = container.model[property.name];
529 return build(container, property, path, value);
530 })}
531 </div>
532 </div>
533 );
534 }
535
536 function buildAdvancedGroup() {
537 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
538 if (properties.length === 0) {
539 return null;
540 }
541 const hasBasicFields = basicProperties.length > 0;
542 const closeGroup = basicProperties.length > 0;
543 return (
544 <div className="advanced-properties-group">
545 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
546 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
547 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
548 </h1>
549 <div className="toggleable">
550 {properties.map(property => {
551 const path = [property.name];
552 const value = container.model[property.name];
553 return build(container, property, path, value, {toggle: true, width: props.width});
554 })}
555 </div>
556 <div className="toggle-bottom-spacer" style={{visibility: 'hidden', 'height': '50%', position: 'absolute'}}>We need this so when the user closes the panel it won't shift away and scare the bj out of them!</div>
557 </div>
558 );
559 }
560
561 function buildMoreLess(d, i) {
562 return (
563 <span key={'bread-crumb-part-' + i}>
564 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
565 <i> / </i>
566 </span>
567 );
568 }
569
570 const path = [];
571 if (container.parent) {
572 path.push(container.parent);
573 }
574 path.push(container);
575
576 return (
577 <div className="EditDescriptorModelProperties -is-tree-view">
578 <h1>{path.map(buildMoreLess)}</h1>
579 {buildBasicGroup()}
580 {buildAdvancedGroup()}
581 </div>
582 );
583
584 }