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