a69f3bba2d4cc0a0fbaaaa5191238725321a5216
[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 caseByNameMap[d.name] = d.properties[0];
375 return {
376 optionName: d.name,
377 optionTitle: d.description,
378 //represents case name and case element name
379 optionValue: [d.name, d.properties[0].name].join('.')
380 };
381 }
382 caseByNameMap[d.name] = d;
383 return {optionName: d.name};
384 });
385
386 const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
387 return (
388 <option key={i} value={d.optionValue} title={d.optionTitle}>
389 {d.optionName}
390 {i ? null : changeCase.title(property.name)}
391 </option>
392 );
393 });
394
395 const selectName = path.join('.');
396 let selectedOptionPath = ['uiState.choice', selectName, 'selected'].join('.');
397 //Currently selected choice/case statement on UI model
398 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
399 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
400 if(!selectedOptionValue) {
401 //get field properties for choice on container model
402 let fieldProperties = utils.resolvePath(container.model, selectName);
403 if(fieldProperties) {
404 //Check each case statement in model and see if it is present in container model.
405 cases.map(function(c){
406 if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
407 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), c.optionValue);
408 }
409 });
410 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
411 } else {
412 property.properties.map(function(p) {
413 let pname = p.properties[0].name;
414 if(container.model.hasOwnProperty(pname)) {
415 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), [p.name, pname].join('.'));
416 }
417 })
418 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
419 }
420 }
421 //If selectedOptionValue is present, take first item in string which represents the case name.
422 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
423 const isLeaf = Property.isLeaf(valueProperty);
424 const hasProperties = _.isArray(valueProperty.properties) && valueProperty.properties.length;
425 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
426 //Some magic that prevents errors for arising
427 const valueResponse = valueProperty.properties.length ? valueProperty.properties.map((d, i) => {
428 const childPath = path.concat(valueProperty.name, d.name);
429 const childValue = utils.resolvePath(container.model, childPath.join('.'));
430 return (
431 <div key={childPath.concat('info', i).join(':')}>
432 {build(container, d, childPath, childValue, props)}
433 </div>
434 );
435 }) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) : null
436 // end magic
437 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
438
439 return (
440 <div key={key} className="choice">
441 <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}>
442 {options}
443 </select>
444 {valueResponse}
445 </div>
446 );
447
448 }
449
450 function buildSimpleListItem(container, property, path, value, key, index) {
451 // todo need to abstract this better
452 const title = getTitle(value);
453 var req = require.context("../", true, /\.svg/);
454 return (
455 <div>
456 <a href="#select-list-item" key={Date.now()} className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
457 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
458 <span>{title}</span>
459 </a>
460 {buildRemovePropertyAction(container, property, path)}
461 </div>
462 );
463 }
464
465 function buildRemoveListItem(container, property, valuePath, fieldKey, index) {
466 const className = ClassNames(property.name + '-remove actions');
467 return (
468 <div key={fieldKey.concat(index).join(':')} className={className}>
469 <h3>
470 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
471 <span className="info">{index + 1}</span>
472 {buildRemovePropertyAction(container, property, valuePath)}
473 </h3>
474 </div>
475 );
476 }
477
478 function buildLeafListItem(container, property, valuePath, value, key, index) {
479 // look at the type to determine how to parse the value
480 return (
481 <div>
482 {buildRemoveListItem(container, property, valuePath, key, index)}
483 {buildField(container, property, valuePath, value, key)}
484 </div>
485
486 );
487 }
488
489 function build(container, property, path, value, props = {}) {
490
491 const fields = [];
492 const isLeaf = Property.isLeaf(property);
493 const isArray = Property.isArray(property);
494 const isObject = Property.isObject(property);
495 const isLeafList = Property.isLeafList(property);
496 const fieldKey = [container.id].concat(path);
497 const isRequired = Property.isRequired(property);
498 const title = changeCase.titleCase(property.name);
499 const columnCount = property.properties.length || 1;
500 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
501 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
502
503 if (!property.properties && isObject) {
504 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
505 property.properties = uiState.properties;
506 }
507
508 const hasProperties = _.isArray(property.properties) && property.properties.length;
509 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
510
511 // ensure value is not undefined for non-leaf property types
512 if (isObject) {
513 if (typeof value !== 'object') {
514 value = isArray ? [] : {};
515 }
516 }
517 const valueAsArray = _.isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
518
519 const isMetaField = property.name === 'meta';
520 const isCVNFD = property.name === 'constituent-vnfd';
521 const isSimpleListView = Property.isSimpleList(property);
522
523 valueAsArray.forEach((value, index) => {
524
525 let field;
526 const key = fieldKey.slice();
527 const valuePath = path.slice();
528
529 if (isArray) {
530 valuePath.push(index);
531 key.push(index);
532 }
533
534 if (isMetaField) {
535 if (typeof value === 'object') {
536 value = JSON.stringify(value, undefined, 12);
537 } else if (typeof value !== 'string') {
538 value = '{}';
539 }
540 }
541
542 if (isMissingDescriptorMeta) {
543 field = <span key={key.concat('warning').join(':')} className="warning">No Descriptor Meta for {property.name}</span>;
544 } else if (property.type === 'choice') {
545 field = buildChoice(container, property, valuePath, value, key.join(':'));
546 } else if (isSimpleListView) {
547 field = buildSimpleListItem(container, property, valuePath, value, key, index);
548 } else if (isLeafList) {
549 field = buildLeafListItem(container, property, valuePath, value, key, index);
550 } else if (hasProperties) {
551 field = buildElement(container, property, valuePath, value, key.join(':'))
552 } else {
553 field = buildField(container, property, valuePath, value, key.join(':'));
554 }
555
556 function onClickLeaf(property, path, value, event) {
557 if (event.isDefaultPrevented()) {
558 return;
559 }
560 event.preventDefault();
561 event.stopPropagation();
562 this.getRoot().uiState.focusedPropertyPath = path.join('.');
563 console.log('property selected', path.join('.'));
564 ComposerAppActions.propertySelected([path.join('.')]);
565 }
566
567 const clickHandler = isLeaf ? onClickLeaf : () => {};
568 const isContainerList = isArray && !(isSimpleListView || isLeafList);
569
570 fields.push(
571 <div key={fieldKey.concat(['property-content', index]).join(':')}
572 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
573 onClick={clickHandler.bind(container, property, valuePath, value)}>
574 {isContainerList ? buildRemoveListItem(container, property, valuePath, fieldKey, index) : null}
575 {field}
576 </div>
577 );
578
579 });
580
581 classNames['-is-leaf'] = isLeaf;
582 classNames['-is-array'] = isArray;
583 classNames['cols-' + columnCount] = isColumnar;
584
585 if (property.type === 'choice') {
586 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
587 if(!value) {
588 property.properties.map(function(p) {
589 let pname = p.properties[0].name;
590 if(container.model.hasOwnProperty(pname)) {
591 value = container.model[pname];
592 }
593 })
594 }
595 }
596
597 let displayValue = typeof value === 'object' ? '' : value;
598 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
599
600 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
601
602 return (
603 <div key={fieldKey.join(':')} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
604 <h3 className="property-label">
605 <label htmlFor={fieldKey.join(':')}>
606 <span className={property.type + '-name name'}>{title}</span>
607 <small>
608 <span className={property.type + '-info info'}>{displayValueInfo}</span>
609 <span className={property.type + '-value value'}>{displayValue}</span>
610 </small>
611 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
612 </label>
613 </h3>
614 <span className={property.type + '-description description'}>{property.description}</span>
615 <val className="property-value">
616 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
617 {fields}
618 </val>
619 </div>
620 );
621
622 }
623
624 const containerType = container.uiState['qualified-type'] || container.uiState.type;
625 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
626
627 function buildBasicGroup() {
628 if (basicProperties.length === 0) {
629 return null;
630 }
631 return (
632 <div className="basic-properties-group">
633 <h2>Basic</h2>
634 <div>
635 {basicProperties.map(property => {
636 const path = [property.name];
637 const value = container.model[property.name];
638 return build(container, property, path, value);
639 })}
640 </div>
641 </div>
642 );
643 }
644
645 function buildAdvancedGroup() {
646 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
647 if (properties.length === 0) {
648 return null;
649 }
650 const hasBasicFields = basicProperties.length > 0;
651 const closeGroup = basicProperties.length > 0;
652 return (
653 <div className="advanced-properties-group">
654 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
655 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
656 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
657 </h1>
658 <div className="toggleable">
659 {properties.map(property => {
660 const path = [property.name];
661 const value = container.model[property.name];
662 return build(container, property, path, value, {toggle: true, width: props.width});
663 })}
664 </div>
665 <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>
666 </div>
667 );
668 }
669
670 function buildMoreLess(d, i) {
671 return (
672 <span key={'bread-crumb-part-' + i}>
673 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
674 <i> / </i>
675 </span>
676 );
677 }
678
679 const path = [];
680 if (container.parent) {
681 path.push(container.parent);
682 }
683 path.push(container);
684
685 return (
686 <div className="EditDescriptorModelProperties -is-tree-view">
687 <h1>{path.map(buildMoreLess)}</h1>
688 {buildBasicGroup()}
689 {buildAdvancedGroup()}
690 </div>
691 );
692
693 }