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