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