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