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