NOTICKET: Merging OSM/master to OSM/projects
[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
24 import _includes from 'lodash/includes'
25 import _isArray from 'lodash/isArray'
26 import _cloneDeep from 'lodash/cloneDeep'
27 import _debounce from 'lodash/debounce';
28 import utils from '../libraries/utils'
29 import React from 'react'
30 import ClassNames from 'classnames'
31 import changeCase from 'change-case'
32 import toggle from '../libraries/ToggleElementHandler'
33 import Button from './Button'
34 import Property from '../libraries/model/DescriptorModelMetaProperty'
35 import ComposerAppActions from '../actions/ComposerAppActions'
36 import CatalogItemsActions from '../actions/CatalogItemsActions'
37 import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
38 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
39 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
40 import SelectionManager from '../libraries/SelectionManager'
41 import DeletionManager from '../libraries/DeletionManager'
42 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
43 import getEventPath from '../libraries/getEventPath'
44 import CatalogDataStore from '../stores/CatalogDataStore'
45
46 import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
47 import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
48
49 import '../styles/EditDescriptorModelProperties.scss'
50
51 const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
52
53 function getDescriptorMetaBasicForType(type) {
54 const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
55 return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
56 }
57
58 function getDescriptorMetaAdvancedForType(type) {
59 const advPropertiesFilter = d => !_includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
60 return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
61 }
62
63 function getTitle(model = {}) {
64 if (typeof model['short-name'] === 'string' && model['short-name']) {
65 return model['short-name'];
66 }
67 if (typeof model.name === 'string' && model.name) {
68 return model.name;
69 }
70 if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
71 return model.uiState.displayName
72 }
73 if (typeof model.id === 'string') {
74 return model.id;
75 }
76 }
77
78 export default function EditDescriptorModelProperties(props) {
79
80 const container = props.container;
81 const readonly = props.readonly;
82 const isEditable = !readonly; //true
83
84 if (!(DescriptorModelFactory.isContainer(container))) {
85 return
86 }
87
88 function startEditing() {
89 DeletionManager.removeEventListeners();
90 }
91
92 function endEditing() {
93 DeletionManager.addEventListeners();
94 }
95
96 function onClickSelectItem(property, path, value, event) {
97 event.preventDefault();
98 const root = this.getRoot();
99 if (SelectionManager.select(value)) {
100 CatalogItemsActions.catalogItemMetaDataChanged(root.model);
101 }
102 }
103
104 function onFocusPropertyFormInputElement(property, path, value, event) {
105
106 event.preventDefault();
107 startEditing();
108
109 function removeIsFocusedClass(event) {
110 event.target.removeEventListener('blur', removeIsFocusedClass);
111 Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
112 }
113
114 removeIsFocusedClass(event);
115
116 const propertyWrapper = getEventPath(event).reduce((parent, element) => {
117 if (parent) {
118 return parent;
119 }
120 if (!element.classList) {
121 return false;
122 }
123 if (element.classList.contains('property')) {
124 return element;
125 }
126 }, false);
127
128 if (propertyWrapper) {
129 propertyWrapper.classList.add('-is-focused');
130 event.target.addEventListener('blur', removeIsFocusedClass);
131 }
132
133 }
134
135 function buildAddPropertyAction(container, property, path) {
136 function onClickAddProperty(property, path, event) {
137 event.preventDefault();
138 //SelectionManager.resume();
139 const create = Property.getContainerCreateMethod(property, this);
140 if (create) {
141 const model = null;
142 create(model, path, property);
143 } else {
144 const name = path.join('.');
145 // get a unique name for the new list item based on the current list content
146 // some lists, based on the key, may not get a uniqueName generated here
147 const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(container.model[property.name], property);
148 const value = Property.createModelInstance(property, uniqueName);
149 utils.assignPathValue(this.model, name, value);
150 }
151 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
152 }
153 if(readonly) {
154 return null;
155 }
156 return (
157 <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
158 );
159 }
160
161 function buildRemovePropertyAction(container, property, path) {
162 function onClickRemoveProperty(property, path, event) {
163 event.preventDefault();
164 const name = path.join('.');
165 const removeMethod = Property.getContainerMethod(property, this, 'remove');
166 if (removeMethod) {
167 removeMethod(utils.resolvePath(this.model, name));
168 } else {
169 utils.removePathValue(this.model, name);
170 }
171 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
172 }
173 if(readonly) {
174 return null;
175 }
176 return (
177 <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
178 );
179 }
180
181 function buildField(container, property, path, value, fieldKey) {
182 let cds = CatalogDataStore;
183 let catalogs = cds.getTransientCatalogs();
184
185 const pathToProperty = path.join('.');
186 const isEditable = true;
187 const isGuid = Property.isGuid(property);
188 const isBoolean = Property.isBoolean(property);
189 const isEnumeration = Property.isEnumeration(property);
190 const isLeafRef = Property.isLeafRef(property);
191 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
192 const placeholder = changeCase.title(property.name);
193 const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
194 const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : (isNaN(value) ? undefined : value);
195
196 // process the named field value change
197 function processFieldValueChange(name, value) {
198 console.debug('processed change for -- ' + name + ' -- with value -- ' + value);
199 // this = the container being edited
200 if (DescriptorModelFactory.isContainer(this)) {
201 utils.assignPathValue(this.model, name, value);
202 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
203 }
204 }
205
206 // change handler used for onChange event
207 const changeHandler = (handleValueChange, event) => {
208 event.preventDefault();
209 console.debug(event.target.value);
210 handleValueChange(event.target.value);
211 };
212 // create an onChange event handler for a text field for the specified field path (debounced to accumulate chars)
213 const onTextChange = changeHandler.bind(null, _debounce(
214 processFieldValueChange.bind(container, pathToProperty), 2000, {maxWait: 5000})); // max wait for short-name
215 // create an onChange event handler for a select field for the specified field path
216 const onSelectChange = changeHandler.bind(null, processFieldValueChange.bind(container, pathToProperty));
217
218 if (isEnumeration) {
219 const enumeration = Property.getEnumeration(property, value);
220 const options = enumeration.map((d, i) => {
221 // note yangforge generates values for enums but the system does not use them
222 // so we categorically ignore them
223 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
224 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
225 return <option key={':' + i} value={d.name}>{d.name}</option>;
226 });
227 const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
228 if (!isValueSet || property.cardinality === '0..1') {
229 const noValueDisplayText = changeCase.title(property.name);
230 options.unshift(<option key={'(value-not-in-enum)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
231 }
232 return (
233 <select
234 key={fieldKey}
235 id={fieldKey}
236 className={ClassNames({'-value-not-set': !isValueSet})}
237 defaultValue={value}
238 title={pathToProperty}
239 onChange={onSelectChange}
240 onFocus={onFocus}
241 onBlur={endEditing}
242 onMouseDown={startEditing}
243 onMouseOver={startEditing}
244 disabled={!isEditable}>
245 {options}
246 </select>
247 );
248 }
249
250 if (isLeafRef) {
251 let fullPathString = container.key + ':' + path.join(':');
252 let containerRef = container;
253 while (containerRef.parent) {
254 fullPathString = containerRef.parent.key + ':' + fullPathString;
255 containerRef = containerRef.parent;
256 }
257 const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
258
259 const options = leafRefPathValues && leafRefPathValues.map((d, i) => {
260 return <option key={':' + i} value={d.value}>{d.value}</option>;
261 });
262 const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
263 if (!isValueSet || property.cardinality === '0..1') {
264 const noValueDisplayText = changeCase.title(property.name);
265 options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
266 }
267 return (
268 <select
269 key={fieldKey}
270 id={fieldKey}
271 className={ClassNames({'-value-not-set': !isValueSet})}
272 defaultValue={value}
273 title={pathToProperty}
274 onChange={onSelectChange}
275 onFocus={onFocus}
276 onBlur={endEditing}
277 onMouseDown={startEditing}
278 onMouseOver={startEditing}
279 disabled={!isEditable}>
280 {options}
281 </select>
282 );
283 }
284
285 if (isBoolean) {
286 const options = [
287 <option key={'true'} value="TRUE">TRUE</option>,
288 <option key={'false'} value="FALSE">FALSE</option>
289 ]
290
291 // if (!isValueSet) {
292 const noValueDisplayText = changeCase.title(property.name);
293 options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}></option>);
294 // }
295 let val = value;
296 if(typeof(val) == 'number') {
297 val = value ? "TRUE" : "FALSE"
298 }
299 const isValueSet = (val != '' && val)
300 return (
301 <select
302 key={fieldKey}
303 id={fieldKey}
304 className={ClassNames({'-value-not-set': !isValueSet})}
305 defaultValue={val && val.toUpperCase()}
306 title={pathToProperty}
307 onChange={onSelectChange}
308 onFocus={onFocus}
309 onBlur={endEditing}
310 onMouseDown={startEditing}
311 onMouseOver={startEditing}
312 disabled={!isEditable}>
313 {options}
314 </select>
315 );
316 }
317
318 if (Property.isLeafEmpty(property)) {
319 // A null value indicates the leaf exists (as opposed to undefined).
320 // We stick in a string when the user actually sets it to simplify things
321 // but the correct thing happens when we serialize to user data
322 let isEmptyLeafPresent = (value === EMPTY_LEAF_PRESENT || value === null);
323 let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
324 const options = [
325 <option key={'true'} value={EMPTY_LEAF_PRESENT}>Enabled</option>,
326 <option key={'false'} value="">Not Enabled</option>
327 ]
328
329 return (
330 <select
331 key={fieldKey}
332 id={fieldKey}
333 className={ClassNames({'-value-not-set': !isEmptyLeafPresent})}
334 defaultValue={present}
335 title={pathToProperty}
336 onChange={onSelectChange}
337 onFocus={onFocus}
338 onBlur={endEditing}
339 onMouseDown={startEditing}
340 onMouseOver={startEditing}
341 disabled={!isEditable}>
342 {options}
343 </select>
344 );
345 }
346
347 if (property['preserve-line-breaks']) {
348 return (
349 <textarea
350 key={fieldKey}
351 cols="5"
352 id={fieldKey}
353 defaultValue={value}
354 placeholder={placeholder}
355 onChange={onTextChange}
356 onFocus={onFocus}
357 onBlur={endEditing}
358 onMouseDown={startEditing}
359 onMouseOver={startEditing}
360 onMouseOut={endEditing}
361 onMouseLeave={endEditing}
362 disabled={!isEditable} />
363 );
364 }
365
366 return (
367 <input
368 key={fieldKey}
369 id={fieldKey}
370 type="text"
371 defaultValue={fieldValue}
372 className={className}
373 placeholder={placeholder}
374 onChange={onTextChange}
375 onFocus={onFocus}
376 onBlur={endEditing}
377 onMouseDown={startEditing}
378 onMouseOver={startEditing}
379 onMouseOut={endEditing}
380 onMouseLeave={endEditing}
381 disabled={!isEditable}
382 />
383 );
384
385 }
386
387 function buildElement(container, property, valuePath, value) {
388 return property.properties.map((property, index) => {
389 let childValue;
390 const childPath = valuePath.slice();
391 if (typeof value === 'object') {
392 childValue = value[property.name];
393 }
394 if(property.type != 'choice'){
395 childPath.push(property.name);
396 }
397 return build(container, property, childPath, childValue);
398
399 });
400 }
401
402 function buildChoice(container, property, path, value, key) {
403
404 function processChoiceChange(name, value) {
405 if (DescriptorModelFactory.isContainer(this)) {
406
407 /*
408 Transient State is stored for convenience in the uiState field.
409 The choice yang type uses case elements to describe the "options".
410 A choice can only ever have one option selected which allows
411 the system to determine which type is selected by the name of
412 the element contained within the field.
413 */
414 /*
415 const stateExample = {
416 uiState: {
417 choice: {
418 'conf-config': {
419 selected: 'rest',
420 'case': {
421 rest: {},
422 netconf: {},
423 script: {}
424 }
425 }
426 }
427 }
428 };
429 */
430 const statePath = ['uiState.choice'].concat(name);
431 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
432 const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
433 // write state back to the model so the new state objects are captured
434 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
435
436 // write the current choice value into the state
437 let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
438 let isTopCase = false;
439 if (choiceObject) {
440 isTopCase = true;
441 choiceObject = utils.resolvePath(this.model, [selected].join('.'));
442 }
443 utils.assignPathValue(stateObject, [selected].join('.'), _cloneDeep(choiceObject));
444
445 if(selected) {
446 if(this.model.uiState.choice.hasOwnProperty(name)) {
447 delete this.model[selected];
448 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
449 } else {
450 // remove the current choice value from the model
451 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
452 }
453 }
454
455 // get any state for the new selected choice
456 const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
457
458 // assign new choice value to the model
459 if (isTopCase) {
460 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
461 } else {
462 utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
463 }
464
465 // update the selected name
466 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
467
468 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
469 }
470 }
471
472 const pathToChoice = path.join('.');
473 const caseByNameMap = {};
474
475 const choiceChangeHandler = processChoiceChange.bind(container, pathToChoice);
476 const onChange = ((handleChoiceChange, event) => {
477 event.preventDefault();
478 handleChoiceChange(event.target.value);
479 }).bind(null, choiceChangeHandler);
480
481
482 const cases = property.properties.map(d => {
483 if (d.type === 'case') {
484 //Previous it was assumed that a case choice would have only one property. Now we pass on either the only item or the
485 caseByNameMap[d.name] = d.properties && (d.properties.length == 1 ? d.properties[0] : d.properties);
486 return {
487 optionName: d.name,
488 optionTitle: d.description,
489 //represents case name and case element name
490 optionValue: [d.name, d.properties[0].name].join('.')
491 };
492 }
493 caseByNameMap[d.name] = d;
494 return {optionName: d.name};
495 });
496
497 const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
498 return (
499 <option key={i} value={d.optionValue} title={d.optionTitle}>
500 {d.optionName}
501 {i ? null : changeCase.title(property.name)}
502 </option>
503 );
504 });
505
506 let selectedOptionPath = ['uiState.choice', pathToChoice, 'selected'].join('.');
507 //Currently selected choice/case statement on UI model
508 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
509 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
510 if(!selectedOptionValue) {
511 //get field properties for choice on container model
512 let fieldProperties = utils.resolvePath(container.model, pathToChoice);
513 if(fieldProperties) {
514 //Check each case statement in model and see if it is present in container model.
515 cases.map(function(c){
516 if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
517 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
518 }
519 });
520 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
521 } else {
522 property.properties.map(function(p) {
523 let pname = p.properties[0].name;
524 if(container.model.hasOwnProperty(pname)) {
525 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
526 }
527 })
528 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
529 }
530 }
531 //If selectedOptionValue is present, take first item in string which represents the case name.
532 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
533 const isLeaf = Property.isLeaf(valueProperty);
534 const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
535 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
536 //Some magic that prevents errors for arising
537 const valueResponse = valueProperty.properties && valueProperty.properties.length ? valueProperty.properties.map(valuePropertyFn) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) :
538 valueProperty.map && valueProperty.map(valuePropertyFn);
539 function valuePropertyFn(d, i) {
540 const childPath = path.concat(valueProperty.name, d.name);
541 const childValue = utils.resolvePath(container.model, childPath.join('.'));
542 return (
543 <div key={childPath.concat('info', i).join(':')}>
544 {build(container, d, childPath, childValue, props)}
545 </div>
546 );
547 }
548 // end magic
549 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
550
551 return (
552 <div key={key} className="choice">
553 <select
554 key={Date.now()}
555 className={ClassNames({'-value-not-set': !selectedOptionValue})}
556 defaultValue={selectedOptionValue}
557 onChange={onChange}
558 onFocus={onFocus}
559 onBlur={endEditing}
560 onMouseDown={startEditing}
561 onMouseOver={startEditing}
562 onMouseOut={endEditing}
563 onMouseLeave={endEditing}
564 disabled={!isEditable}
565 >
566 {options}
567 </select>
568 {valueResponse}
569 </div>
570 );
571
572 }
573
574 function buildSimpleListItem(container, property, path, value, uniqueId, index) {
575 // todo need to abstract this better
576 const title = getTitle(value);
577 var req = require.context("../", true, /\.svg/);
578 return (
579 <div key={uniqueId} >
580 <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
581 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
582 <span>{title}</span>
583 </a>
584 {buildRemovePropertyAction(container, property, path)}
585 </div>
586 );
587 }
588
589 function buildRemoveListItem(container, property, valuePath, index) {
590 const className = ClassNames(property.name + '-remove actions');
591 return (
592 <div className={className}>
593 <h3>
594 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
595 <span className="info">{index + 1}</span>
596 {buildRemovePropertyAction(container, property, valuePath)}
597 </h3>
598 </div>
599 );
600 }
601
602 function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
603 // look at the type to determine how to parse the value
604 return (
605 <div key={uniqueId}>
606 {buildRemoveListItem(container, property, valuePath, index)}
607 {buildField(container, property, valuePath, value, uniqueId)}
608 </div>
609
610 );
611 }
612
613 function build(container, property, path, value, props = {}) {
614
615 const fields = [];
616 const isLeaf = Property.isLeaf(property);
617 const isArray = Property.isArray(property);
618 const isObject = Property.isObject(property);
619 const isLeafList = Property.isLeafList(property);
620 const isRequired = Property.isRequired(property);
621 const title = changeCase.titleCase(property.name);
622 const columnCount = property.properties.length || 1;
623 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
624 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
625
626 // create a unique Id for use as react component keys and html element ids
627 // use uid (from ui info) instead of id property (which is not stable)
628 let uniqueId = container.uid;
629 let containerRef = container;
630 while (containerRef.parent) {
631 uniqueId = containerRef.parent.uid + ':' + uniqueId;
632 containerRef = containerRef.parent;
633 }
634 uniqueId += ':' + path.join(':')
635
636 if (!property.properties && isObject) {
637 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
638 property.properties = uiState.properties;
639 }
640
641 const hasProperties = _isArray(property.properties) && property.properties.length;
642 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
643
644 // ensure value is not undefined for non-leaf property types
645 if (isObject) {
646 if (typeof value !== 'object') {
647 value = isArray ? [] : {};
648 }
649 }
650 const valueAsArray = _isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
651
652 const isMetaField = property.name === 'meta';
653 const isCVNFD = property.name === 'constituent-vnfd';
654 const isSimpleListView = Property.isSimpleList(property);
655
656 valueAsArray.forEach((value, index) => {
657
658 let field;
659 const valuePath = path.slice();
660 // create a unique field Id for use as react component keys and html element ids
661 // notes:
662 // keys only need to be unique on components in the same array
663 // html element ids should be unique with the document (or form)
664 let fieldId = uniqueId;
665
666 if (isArray) {
667 valuePath.push(index);
668 fieldId += index;
669 }
670
671 if (isMetaField) {
672 if (typeof value === 'object') {
673 value = JSON.stringify(value, undefined, 12);
674 } else if (typeof value !== 'string') {
675 value = '{}';
676 }
677 }
678
679 if (isMissingDescriptorMeta) {
680 field = <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>;
681 } else if (property.type === 'choice') {
682 field = buildChoice(container, property, valuePath, value, fieldId);
683 } else if (isSimpleListView) {
684 field = buildSimpleListItem(container, property, valuePath, value, fieldId, index);
685 } else if (isLeafList) {
686 field = buildLeafListItem(container, property, valuePath, value, fieldId, index);
687 } else if (hasProperties) {
688 field = buildElement(container, property, valuePath, value, fieldId)
689 } else {
690 field = buildField(container, property, valuePath, value, fieldId);
691 }
692
693 function onClickLeaf(property, path, value, event) {
694 if (event.isDefaultPrevented()) {
695 return;
696 }
697 event.preventDefault();
698 event.stopPropagation();
699 this.getRoot().uiState.focusedPropertyPath = path.join('.');
700 console.debug('property selected', path.join('.'));
701 ComposerAppActions.propertySelected([path.join('.')]);
702 }
703
704 const clickHandler = isLeaf ? onClickLeaf : () => {};
705 const isContainerList = isArray && !(isSimpleListView || isLeafList);
706
707 fields.push(
708 <div key={fieldId}
709 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
710 onClick={clickHandler.bind(container, property, valuePath, value)}>
711 {isContainerList ? buildRemoveListItem(container, property, valuePath, index) : null}
712 {field}
713 </div>
714 );
715
716 });
717
718 classNames['-is-leaf'] = isLeaf;
719 classNames['-is-array'] = isArray;
720 classNames['cols-' + columnCount] = isColumnar;
721
722 if (property.type === 'choice') {
723 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
724 if(!value) {
725 property.properties.map(function(p) {
726 let pname = p.properties[0].name;
727 if(container.model.hasOwnProperty(pname)) {
728 value = container.model[pname];
729 }
730 })
731 }
732 }
733
734 let displayValue = typeof value === 'object' ? '' : value;
735 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
736
737 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
738
739 return (
740 <div key={uniqueId} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
741 <h3 className="property-label">
742 <label htmlFor={uniqueId}>
743 <span className={property.type + '-name name'}>{title}</span>
744 <small>
745 <span className={property.type + '-info info'}>{displayValueInfo}</span>
746 <span className={property.type + '-value value'}>{displayValue}</span>
747 </small>
748 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
749 </label>
750 </h3>
751 <span className={property.type + '-description description'}>{property.description}</span>
752 <val className="property-value">
753 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
754 {fields}
755 </val>
756 </div>
757 );
758
759 }
760
761 const containerType = container.uiState['qualified-type'] || container.uiState.type;
762 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
763
764 function buildBasicGroup() {
765 if (basicProperties.length === 0) {
766 return null;
767 }
768 return (
769 <div className="basic-properties-group">
770 <h2>Basic</h2>
771 <div>
772 {basicProperties.map(property => {
773 const path = [property.name];
774 const value = container.model[property.name];
775 return build(container, property, path, value);
776 })}
777 </div>
778 </div>
779 );
780 }
781
782 function buildAdvancedGroup() {
783 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
784 if (properties.length === 0) {
785 return null;
786 }
787 const hasBasicFields = basicProperties.length > 0;
788 const closeGroup = basicProperties.length > 0;
789 return (
790 <div className="advanced-properties-group">
791 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
792 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
793 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
794 </h1>
795 <div className="toggleable">
796 {properties.map(property => {
797 const path = [property.name];
798 const value = container.model[property.name];
799 return build(container, property, path, value, {toggle: true, width: props.width});
800 })}
801 </div>
802 <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>
803 </div>
804 );
805 }
806
807 function buildMoreLess(d, i) {
808 return (
809 <span key={'bread-crumb-part-' + i}>
810 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
811 <i> / </i>
812 </span>
813 );
814 }
815
816 const path = [];
817 if (container.parent) {
818 path.push(container.parent);
819 }
820 path.push(container);
821
822 return (
823 <div className="EditDescriptorModelProperties -is-tree-view">
824 <h1>{path.map(buildMoreLess)}</h1>
825 {buildBasicGroup()}
826 {buildAdvancedGroup()}
827 </div>
828 );
829 };
830