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