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