RIFT-14828: object Top level choice statements supported in 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
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 const name = event.target.name;
249 const value = event.target.value;
250
251 /*
252 Transient State is stored for convenience in the uiState field.
253 The choice yang type uses case elements to describe the "options".
254 A choice can only ever have one option selected which allows
255 the system to determine which type is selected by the name of
256 the element contained within the field.
257 */
258
259 //const stateExample = {
260 // uiState: {
261 // choice: {
262 // 'conf-config': {
263 // selected: 'rest',
264 // 'case': {
265 // rest: {},
266 // netconf: {},
267 // script: {}
268 // }
269 // }
270 // }
271 // }
272 //};
273
274 const statePath = ['uiState.choice'].concat(name);
275 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
276 const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
277 // write state back to the model so the new state objects are captured
278 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
279
280 // write the current choice value into the state
281 const choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
282 if (choiceObject) {
283 utils.assignPathValue(stateObject, ['case', selected].join('.'), _.cloneDeep(choiceObject));
284 }
285
286 // remove the current choice value from the model
287 utils.removePathValue(this.model, [name, selected].join('.'));
288
289 // get any state for the new selected choice
290 const newChoiceObject = utils.resolvePath(stateObject, ['case', value].join('.')) || {};
291
292 // assign new choice value to the model
293 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
294
295 // update the selected name
296 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
297
298 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
299 }
300 }
301
302 const caseByNameMap = {};
303
304 const onChange = onFormFieldValueChanged.bind(container);
305
306 const cases = property.properties.map(d => {
307 if (d.type === 'case') {
308 caseByNameMap[d.name] = d.properties[0];
309 return {
310 optionName: d.name,
311 optionTitle: d.description,
312 //represents case name and case element name
313 optionValue: [d.name, d.properties[0].name].join('.')
314 };
315 }
316 caseByNameMap[d.name] = d;
317 return {optionName: d.name};
318 });
319
320 const options = [{optionName: ''}].concat(cases).map((d, i) => {
321 return (
322 <option key={i} value={d.optionValue} title={d.optionTitle}>
323 {d.optionName}
324 {i ? null : changeCase.title(property.name)}
325 </option>
326 );
327 });
328
329 const selectName = path.join('.');
330 let selectedOptionPath = ['uiState.choice', selectName, 'selected'].join('.');
331 //Currently selected choice/case statement on UI model
332 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
333 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
334 if(!selectedOptionValue) {
335 //get field properties for choice on container model
336 let fieldProperties = utils.resolvePath(container.model, selectName);
337 if(fieldProperties) {
338 //Check each case statement in model and see if it is present in container model.
339 cases.map(function(c){
340 if(fieldProperties.hasOwnProperty(c.optionName)) {
341 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), c.optionValue);
342 }
343 });
344 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
345 } else {
346 property.properties.map(function(p) {
347 let pname = p.properties[0].name;
348 if(container.model.hasOwnProperty(pname)) {
349 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), [p.name, pname].join('.'));
350 }
351 })
352 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
353 }
354 }
355 //If selectedOptionValue is present, take first item in string which represents the case name.
356 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
357 const isLeaf = Property.isLeaf(valueProperty);
358 const hasProperties = _.isArray(valueProperty.properties) && valueProperty.properties.length;
359 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
360 //Some magic that prevents errors for arising
361 const valueResponse = valueProperty.properties.length ? valueProperty.properties.map((d, i) => {
362 const childPath = path.concat(valueProperty.name, d.name);
363 const childValue = utils.resolvePath(container.model, childPath.join('.'));
364 return (
365 <div key={childPath.concat('info', i).join(':')}>
366 {build(container, d, childPath, childValue, props)}
367 </div>
368 );
369 }) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) : null
370 // end magic
371 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
372
373 return (
374 <div key={key} className="choice">
375 <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}>
376 {options}
377 </select>
378 {valueResponse}
379 </div>
380 );
381
382 }
383
384 function buildSimpleListItem(container, property, path, value, key, index) {
385 // todo need to abstract this better
386 const title = getTitle(value);
387 var req = require.context("../", true, /\.svg/);
388 return (
389 <div>
390 <a href="#select-list-item" key={Date.now()} className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
391 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
392 <span>{title}</span>
393 </a>
394 {buildRemovePropertyAction(container, property, path)}
395 </div>
396 );
397 }
398
399 function buildRemoveListItem(container, property, valuePath, fieldKey, index) {
400 const className = ClassNames(property.name + '-remove actions');
401 return (
402 <div key={fieldKey.concat(index).join(':')} className={className}>
403 <h3>
404 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
405 <span className="info">{index + 1}</span>
406 {buildRemovePropertyAction(container, property, valuePath)}
407 </h3>
408 </div>
409 );
410 }
411
412 function buildLeafListItem(container, property, valuePath, value, key, index) {
413 // look at the type to determine how to parse the value
414 return (
415 <div>
416 {buildRemoveListItem(container, property, valuePath, key, index)}
417 {buildField(container, property, valuePath, value, key)}
418 </div>
419
420 );
421 }
422
423 function build(container, property, path, value, props = {}) {
424
425 const fields = [];
426 const isLeaf = Property.isLeaf(property);
427 const isArray = Property.isArray(property);
428 const isObject = Property.isObject(property);
429 const isLeafList = Property.isLeafList(property);
430 const fieldKey = [container.id].concat(path);
431 const isRequired = Property.isRequired(property);
432 const title = changeCase.titleCase(property.name);
433 const columnCount = property.properties.length || 1;
434 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
435 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
436
437 if (!property.properties && isObject) {
438 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
439 property.properties = uiState.properties;
440 }
441
442 const hasProperties = _.isArray(property.properties) && property.properties.length;
443 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
444
445 // ensure value is not undefined for non-leaf property types
446 if (isObject) {
447 if (typeof value !== 'object') {
448 value = isArray ? [] : {};
449 }
450 }
451 const valueAsArray = _.isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
452
453 const isMetaField = property.name === 'meta';
454 const isCVNFD = property.name === 'constituent-vnfd';
455 const isSimpleListView = Property.isSimpleList(property);
456
457 valueAsArray.forEach((value, index) => {
458
459 let field;
460 const key = fieldKey.slice();
461 const valuePath = path.slice();
462
463 if (isArray) {
464 valuePath.push(index);
465 key.push(index);
466 }
467
468 if (isMetaField) {
469 if (typeof value === 'object') {
470 value = JSON.stringify(value, undefined, 12);
471 } else if (typeof value !== 'string') {
472 value = '{}';
473 }
474 }
475
476 if (isMissingDescriptorMeta) {
477 field = <span key={key.concat('warning').join(':')} className="warning">No Descriptor Meta for {property.name}</span>;
478 } else if (property.type === 'choice') {
479 field = buildChoice(container, property, valuePath, value, key.join(':'));
480 } else if (isSimpleListView) {
481 field = buildSimpleListItem(container, property, valuePath, value, key, index);
482 } else if (isLeafList) {
483 field = buildLeafListItem(container, property, valuePath, value, key, index);
484 } else if (hasProperties) {
485 field = buildElement(container, property, valuePath, value, key.join(':'))
486 } else {
487 field = buildField(container, property, valuePath, value, key.join(':'));
488 }
489
490 function onClickLeaf(property, path, value, event) {
491 if (event.isDefaultPrevented()) {
492 return;
493 }
494 event.preventDefault();
495 event.stopPropagation();
496 this.getRoot().uiState.focusedPropertyPath = path.join('.');
497 console.log('property selected', path.join('.'));
498 ComposerAppActions.propertySelected([path.join('.')]);
499 }
500
501 const clickHandler = isLeaf ? onClickLeaf : () => {};
502 const isContainerList = isArray && !(isSimpleListView || isLeafList);
503
504 fields.push(
505 <div key={fieldKey.concat(['property-content', index]).join(':')}
506 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
507 onClick={clickHandler.bind(container, property, valuePath, value)}>
508 {isContainerList ? buildRemoveListItem(container, property, valuePath, fieldKey, index) : null}
509 {field}
510 </div>
511 );
512
513 });
514
515 classNames['-is-leaf'] = isLeaf;
516 classNames['-is-array'] = isArray;
517 classNames['cols-' + columnCount] = isColumnar;
518
519 if (property.type === 'choice') {
520 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
521 if(!value) {
522 property.properties.map(function(p) {
523 let pname = p.properties[0].name;
524 if(container.model.hasOwnProperty(pname)) {
525 value = container.model[pname];
526 }
527 })
528 }
529 }
530
531 let displayValue = typeof value === 'object' ? '' : value;
532 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
533
534 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
535
536 return (
537 <div key={fieldKey.join(':')} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
538 <h3 className="property-label">
539 <label htmlFor={fieldKey.join(':')}>
540 <span className={property.type + '-name name'}>{title}</span>
541 <small>
542 <span className={property.type + '-info info'}>{displayValueInfo}</span>
543 <span className={property.type + '-value value'}>{displayValue}</span>
544 </small>
545 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
546 </label>
547 </h3>
548 <span className={property.type + '-description description'}>{property.description}</span>
549 <val className="property-value">
550 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
551 {fields}
552 </val>
553 </div>
554 );
555
556 }
557
558 const containerType = container.uiState['qualified-type'] || container.uiState.type;
559 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
560
561 function buildBasicGroup() {
562 if (basicProperties.length === 0) {
563 return null;
564 }
565 return (
566 <div className="basic-properties-group">
567 <h2>Basic</h2>
568 <div>
569 {basicProperties.map(property => {
570 const path = [property.name];
571 const value = container.model[property.name];
572 return build(container, property, path, value);
573 })}
574 </div>
575 </div>
576 );
577 }
578
579 function buildAdvancedGroup() {
580 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
581 if (properties.length === 0) {
582 return null;
583 }
584 const hasBasicFields = basicProperties.length > 0;
585 const closeGroup = basicProperties.length > 0;
586 return (
587 <div className="advanced-properties-group">
588 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
589 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
590 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
591 </h1>
592 <div className="toggleable">
593 {properties.map(property => {
594 const path = [property.name];
595 const value = container.model[property.name];
596 return build(container, property, path, value, {toggle: true, width: props.width});
597 })}
598 </div>
599 <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>
600 </div>
601 );
602 }
603
604 function buildMoreLess(d, i) {
605 return (
606 <span key={'bread-crumb-part-' + i}>
607 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
608 <i> / </i>
609 </span>
610 );
611 }
612
613 const path = [];
614 if (container.parent) {
615 path.push(container.parent);
616 }
617 path.push(container);
618
619 return (
620 <div className="EditDescriptorModelProperties -is-tree-view">
621 <h1>{path.map(buildMoreLess)}</h1>
622 {buildBasicGroup()}
623 {buildAdvancedGroup()}
624 </div>
625 );
626
627 }