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