Config parameter map styling pass, 2
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditConfigParameterMap.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 import '../styles/EditConfigParameterMap.scss';
49
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 function startEditing() {
76 DeletionManager.removeEventListeners();
77 }
78
79 function endEditing() {
80 DeletionManager.addEventListeners();
81 }
82
83 function onClickSelectItem(property, path, value, event) {
84 event.preventDefault();
85 const root = this.getRoot();
86 if (SelectionManager.select(value)) {
87 CatalogItemsActions.catalogItemMetaDataChanged(root.model);
88 }
89 }
90
91 function onFocusPropertyFormInputElement(property, path, value, event) {
92
93 event.preventDefault();
94 startEditing();
95
96 function removeIsFocusedClass(event) {
97 event.target.removeEventListener('blur', removeIsFocusedClass);
98 Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
99 }
100
101 removeIsFocusedClass(event);
102
103 const propertyWrapper = getEventPath(event).reduce((parent, element) => {
104 if (parent) {
105 return parent;
106 }
107 if (!element.classList) {
108 return false;
109 }
110 if (element.classList.contains('property')) {
111 return element;
112 }
113 }, false);
114
115 if (propertyWrapper) {
116 propertyWrapper.classList.add('-is-focused');
117 event.target.addEventListener('blur', removeIsFocusedClass);
118 }
119
120 }
121
122 function buildAddPropertyAction(container, property, path) {
123 function onClickAddProperty(property, path, event) {
124 event.preventDefault();
125 //SelectionManager.resume();
126 const create = Property.getContainerCreateMethod(property, this);
127 if (create) {
128 const model = null;
129 create(model, path, property);
130 } else {
131 const name = path.join('.');
132 const value = Property.createModelInstance(property);
133 utils.assignPathValue(this.model, name, value);
134 }
135 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
136 }
137 return (
138 <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
139 );
140 }
141
142 function buildRemovePropertyAction(container, property, path) {
143 function onClickRemoveProperty(property, path, event) {
144 event.preventDefault();
145 const name = path.join('.');
146 const removeMethod = Property.getContainerMethod(property, this, 'remove');
147 if (removeMethod) {
148 removeMethod(utils.resolvePath(this.model, name));
149 } else {
150 utils.removePathValue(this.model, name);
151 }
152 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
153 }
154 return (
155 <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
156 );
157 }
158
159 function onFormFieldValueChanged(event) {
160 if (DescriptorModelFactory.isContainer(this)) {
161 event.preventDefault();
162 const name = event.target.name;
163 const value = JSON.parse(event.target.value);
164 utils.assignPathValue(this.model, 'config-parameter-source.config-parameter-source-ref', value.value);
165 utils.assignPathValue(this.model, 'config-parameter-source.member-vnf-index-ref', value.index);
166 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
167 }
168 }
169
170 function buildField(container, property, path, value, fieldKey, vnfdIndex) {
171 let cds = CatalogDataStore;
172 let catalogs = cds.getTransientCatalogs();
173
174 const name = path.join('.');
175 const isEditable = true;
176 const isGuid = Property.isGuid(property);
177 const onChange = onFormFieldValueChanged.bind(container);
178 const isEnumeration = Property.isEnumeration(property);
179 const isLeafRef = Property.isLeafRef(property);
180 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
181 const placeholder = changeCase.title(property.name);
182 const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
183 const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : undefined;
184 // if (isEnumeration) {
185 // const enumeration = Property.getEnumeration(property, value);
186 // const options = enumeration.map((d, i) => {
187 // // note yangforge generates values for enums but the system does not use them
188 // // so we categorically ignore them
189 // // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
190 // //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
191 // return <option key={fieldKey.toString() + ':' + i} value={d.name}>{d.name}</option>;
192 // });
193 // const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
194 // if (!isValueSet || property.cardinality === '0..1') {
195 // const noValueDisplayText = changeCase.title(property.name);
196 // options.unshift(<option key={'(value-not-in-enum)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
197 // }
198 // 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>;
199 // }
200
201 if (isLeafRef && (path.indexOf("config-parameter-source-ref") > -1)) {
202 let fullFieldKey = _.isArray(fieldKey) ? fieldKey.join(':') : fieldKey;
203 let containerRef = container;
204 let vnfdName = null;
205 let options = [];
206 let leafRefPathValues = [];
207 while (containerRef.parent) {
208 fullFieldKey = containerRef.parent.key + ':' + fullFieldKey;
209 containerRef = containerRef.parent;
210 }
211 let parentProperty = container.parent.constituentVnfd;
212 parentProperty.map((v, i) => {
213 let somevalues = Property.getConfigParamRef(property, path, value, fullFieldKey, catalogs, container, v.vnfdId);
214 options = somevalues && options.concat(somevalues.map((d, i) => {
215 return <option key={v.vnfdId + ':' + fieldKey.toString() + ':' + i} value={JSON.stringify({value: d.value, index: v.vnfdIndex})}>{`${v['short-name']} (${v.vnfdIndex}) / ${d.value}`}</option>;
216 }))
217 leafRefPathValues = leafRefPathValues.concat(somevalues);
218
219 });
220
221 const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
222 if (!isValueSet) {
223 const noValueDisplayText = changeCase.title(property.name);
224 options.unshift(<option key={'(value-not-in-leafref)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
225 }
226 return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={JSON.stringify({value:value, index: container.model['config-parameter-source']['member-vnf-index-ref']})} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} readOnly={!isEditable}>{options}</select>;
227 }
228 }
229
230 function buildElement(container, property, valuePath, value, vnfdIndex) {
231 return property.properties.map((property, index) => {
232 let childValue;
233 const childPath = valuePath.slice();
234 if (typeof value === 'object') {
235 childValue = value[property.name];
236 }
237 if(property.type != 'choice'){
238 childPath.push(property.name);
239 }
240 return build(container, property, childPath, childValue, {}, vnfdIndex);
241
242 });
243 }
244
245
246 function buildSimpleListItem(container, property, path, value, key, index) {
247 // todo need to abstract this better
248 const title = getTitle(value);
249 var req = require.context("../", true, /\.svg/);
250 return (
251 <div>
252 <a href="#select-list-item" key={Date.now()} className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
253 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
254 <span>{title}</span>
255 </a>
256 {buildRemovePropertyAction(container, property, path)}
257 </div>
258 );
259 }
260
261 function buildRemoveListItem(container, property, valuePath, fieldKey, index) {
262 const className = ClassNames(property.name + '-remove actions');
263 return (
264 <div key={fieldKey.concat(index).join(':')} className={className}>
265 <h3>
266 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
267 <span className="info">{index + 1}</span>
268 {buildRemovePropertyAction(container, property, valuePath)}
269 </h3>
270 </div>
271 );
272 }
273
274 function buildLeafListItem(container, property, valuePath, value, key, index, vnfdIndex) {
275 // look at the type to determine how to parse the value
276 return (
277 <div>
278 {buildRemoveListItem(container, property, valuePath, key, index)}
279 {buildField(container, property, valuePath, value, key, vnfdIndex)}
280 </div>
281
282 );
283 }
284
285 function build(container, property, path, value, props = {}, vnfdIndex) {
286 const fields = [];
287 const isLeaf = Property.isLeaf(property);
288 const isArray = Property.isArray(property);
289 const isObject = Property.isObject(property);
290 const isLeafList = Property.isLeafList(property);
291 const fieldKey = [container.id].concat(path);
292 const isRequired = Property.isRequired(property);
293 const title = changeCase.titleCase(property.name);
294 const columnCount = property.properties.length || 1;
295 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
296 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
297
298 if (!property.properties && isObject) {
299 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
300 property.properties = uiState.properties;
301 }
302
303 const hasProperties = _.isArray(property.properties) && property.properties.length;
304 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
305
306 // ensure value is not undefined for non-leaf property types
307 if (isObject) {
308 if (typeof value !== 'object') {
309 value = isArray ? [] : {};
310 }
311 }
312 const valueAsArray = _.isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
313
314 const isMetaField = property.name === 'meta';
315 const isCVNFD = property.name === 'constituent-vnfd';
316 const isSimpleListView = Property.isSimpleList(property);
317
318 valueAsArray.forEach((value, index) => {
319
320 let field;
321 const key = fieldKey.slice();
322 const valuePath = path.slice();
323
324 if (isArray) {
325 valuePath.push(index);
326 key.push(index);
327 }
328
329 if (isMetaField) {
330 if (typeof value === 'object') {
331 value = JSON.stringify(value, undefined, 12);
332 } else if (typeof value !== 'string') {
333 value = '{}';
334 }
335 }
336
337 if (isMissingDescriptorMeta) {
338 field = <span key={key.concat('warning').join(':')} className="warning">No Descriptor Meta for {property.name}</span>;
339 } else if (property.type === 'choice') {
340 field = buildChoice(container, property, valuePath, value, key.join(':'), props);
341 } else if (isSimpleListView) {
342 field = buildSimpleListItem(container, property, valuePath, value, key, index, props);
343 } else if (isLeafList) {
344 field = buildLeafListItem(container, property, valuePath, value, key, index, vnfdIndex);
345 } else if (hasProperties) {
346 field = buildElement(container, property, valuePath, value, vnfdIndex)
347 } else {
348 field = buildField(container, property, valuePath, value, key.join(':'), vnfdIndex);
349 }
350
351 function onClickLeaf(property, path, value, event) {
352 if (event.isDefaultPrevented()) {
353 return;
354 }
355 event.preventDefault();
356 event.stopPropagation();
357 this.getRoot().uiState.focusedPropertyPath = path.join('.');
358 console.log('property selected', path.join('.'));
359 ComposerAppActions.propertySelected([path.join('.')]);
360 }
361
362 const clickHandler = isLeaf ? onClickLeaf : () => {};
363 const isContainerList = isArray && !(isSimpleListView || isLeafList);
364 if(valuePath.indexOf("member-vnf-index-ref") == -1) {
365 fields.push(
366 <div key={fieldKey.concat(['property-content', index]).join(':')}
367 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
368 onClick={clickHandler.bind(container, property, valuePath, value)}>
369 {isContainerList ? buildRemoveListItem(container, property, valuePath, fieldKey, index) : null}
370 {field}
371 </div>
372 );
373 }
374 });
375
376 classNames['-is-leaf'] = isLeaf;
377 classNames['-is-array'] = isArray;
378 classNames['cols-' + columnCount] = isColumnar;
379
380 if (property.type === 'choice') {
381 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
382 if(!value) {
383 property.properties.map(function(p) {
384 let pname = p.properties[0].name;
385 if(container.model.hasOwnProperty(pname)) {
386 value = container.model[pname];
387 }
388 })
389 }
390 }
391
392 let displayValue = typeof value === 'object' ? '' : value;
393 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
394
395 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
396
397 //Remove first entry, which is member-vnf-index because I'm tired of trying to figure out how it's being added and feel pressed for time. Come back here if there's ever time and fix correctly.
398 if (fieldKey.indexOf('member-vnf-index-ref') == -1) {
399 return (
400 <div key={fieldKey.join(':')} onFocus={onFocus}>
401 {fields}
402 </div>
403 );
404 }
405 }
406 export default function EditDescriptorModelProperties(props, type) {
407
408 const container = props.container;
409
410 if (!(DescriptorModelFactory.isContainer(container))) {
411 return
412 }
413
414
415
416 const containerType = (_.isEmpty(type) ? false : type)|| container.uiState['qualified-type'] || container.uiState.type;
417 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
418
419
420 function buildAdvancedGroup() {
421 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
422 if (properties.length === 0) {
423 return null;
424 }
425 const hasBasicFields = basicProperties.length > 0;
426 const closeGroup = basicProperties.length > 0;
427 return (
428 <div className="config-parameter-map">
429 <div className="config-parameter">
430 Request
431 </div>
432 <div className="config-parameter">
433 Source
434 </div>
435 {properties.map((property,i) => {
436 const path = [property.name];
437 const value = container.model[property.name];
438 if(path == 'id') {
439 return null
440 }
441 if(path == 'config-parameter-request') {
442 let cds = CatalogDataStore;
443 let catalogs = cds.getTransientCatalogs();
444 let vnfdIndexRef = container.model[path]['member-vnf-index-ref'];
445 let vnfdIdRef = container.parent.model['constituent-vnfd'].filter(function(v){return v['member-vnf-index'] == vnfdIndexRef})[0]['vnfd-id-ref'];
446 let vnfd = catalogs[1].descriptors.filter((v) => v.id == vnfdIdRef)[0];
447 let primitiveName = vnfd['config-parameter']['config-parameter-request'].filter((p) => p.name == value['config-parameter-request-ref'] )[0].parameter[0]['config-primitive-name-ref'];
448 return (
449 <div className="config-parameter config-parameter-request" key={path + '-' + i}>
450 {`${vnfd['short-name']}(${vnfdIndexRef}) / ${primitiveName} / ${value['config-parameter-request-ref']}`}
451 </div>
452 )
453 } else if(path == 'config-parameter-source') {
454 //Builds Source
455 return <div className="config-parameter config-parameter-source"> {build(container, property, path, value, _.assign({toggle: true, width: props.width}, props), value['member-vnf-index-ref'])} </div>
456 }
457
458 })}
459 <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>
460 </div>
461 );
462 }
463
464 function buildMoreLess(d, i) {
465 return (
466 <span key={'bread-crumb-part-' + i}>
467 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
468 <i> / </i>
469 </span>
470 );
471 }
472
473 const path = [];
474 if (container.parent) {
475 path.push(container.parent);
476 }
477 path.push(container);
478
479 return (
480 <div className="EditDescriptorModelProperties -is-tree-view">
481 {buildAdvancedGroup()}
482 </div>
483 );
484
485 }
486 export {build}
487 // export buildElement;
488 // export buildChoice;