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