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