3 * Copyright 2016 RIFT.IO Inc
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * Created by onvelocity on 1/18/16.
21 * This class generates the form fields used to edit the CONFD JSON model.
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'
43 import imgAdd
from '../../../node_modules/open-iconic/svg/plus.svg'
44 import imgRemove
from '../../../node_modules/open-iconic/svg/trash.svg'
46 import '../styles/EditDescriptorModelProperties.scss'
48 function getDescriptorMetaBasicForType(type
) {
49 const basicPropertiesFilter
= d
=> _
.contains(DESCRIPTOR_MODEL_FIELDS
[type
], d
.name
);
50 return DescriptorModelMetaFactory
.getModelMetaForType(type
, basicPropertiesFilter
) || {properties
: []};
53 function getDescriptorMetaAdvancedForType(type
) {
54 const advPropertiesFilter
= d
=> !_
.contains(DESCRIPTOR_MODEL_FIELDS
[type
], d
.name
);
55 return DescriptorModelMetaFactory
.getModelMetaForType(type
, advPropertiesFilter
) || {properties
: []};
58 function getTitle(model
= {}) {
59 if (typeof model
['short-name'] === 'string' && model
['short-name']) {
60 return model
['short-name'];
62 if (typeof model
.name
=== 'string' && model
.name
) {
65 if (model
.uiState
&& typeof model
.uiState
.displayName
=== 'string' && model
.uiState
.displayName
) {
66 return model
.uiState
.displayName
68 if (typeof model
.id
=== 'string') {
73 export default function EditDescriptorModelProperties(props
) {
75 const container
= props
.container
;
77 if (!(DescriptorModelFactory
.isContainer(container
))) {
81 function startEditing() {
82 DeletionManager
.removeEventListeners();
85 function endEditing() {
86 DeletionManager
.addEventListeners();
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
);
97 function onFocusPropertyFormInputElement(property
, path
, value
, event
) {
99 event
.preventDefault();
102 function removeIsFocusedClass(event
) {
103 event
.target
.removeEventListener('blur', removeIsFocusedClass
);
104 Array
.from(document
.querySelectorAll('.-is-focused')).forEach(d
=> d
.classList
.remove('-is-focused'));
107 removeIsFocusedClass(event
);
109 const propertyWrapper
= getEventPath(event
).reduce((parent
, element
) => {
113 if (!element
.classList
) {
116 if (element
.classList
.contains('property')) {
121 if (propertyWrapper
) {
122 propertyWrapper
.classList
.add('-is-focused');
123 event
.target
.addEventListener('blur', removeIsFocusedClass
);
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);
135 create(model
, path
, property
);
137 const name
= path
.join('.');
138 const value
= Property
.createModelInstance(property
);
139 utils
.assignPathValue(this.model
, name
, value
);
141 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
144 <Button className
="inline-hint" onClick
={onClickAddProperty
.bind(container
, property
, path
)} label
="Add" src
={imgAdd
} />
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');
154 removeMethod(utils
.resolvePath(this.model
, name
));
156 utils
.removePathValue(this.model
, name
);
158 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
161 <Button className
="remove-property-action inline-hint" title
="Remove" onClick
={onClickRemoveProperty
.bind(container
, property
, path
)} label
="Remove" src
={imgRemove
}/>
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());
175 function buildField(container
, property
, path
, value
, fieldKey
) {
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
});
186 const enumeration
= Property
.getEnumeration(property
, value
);
187 const options
= enumeration
.map((d
, i
) => {
188 // note yangforge generates values for enums but the system does not use them
189 // so we categorically ignore them
190 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
191 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
192 return <option key
={fieldKey
.toString() + ':' + i
} value
={d
.name
}>{d
.name
}</option
>;
194 const isValueSet
= enumeration
.filter(d
=> d
.isSelected
).length
> 0;
195 if (!isValueSet
|| property
.cardinality
=== '0..1') {
196 const noValueDisplayText
= changeCase
.title(property
.name
);
197 options
.unshift(<option key
={'(value-not-in-enum)' + fieldKey
.toString()} value
="" placeholder
={placeholder
}>{noValueDisplayText
}</option
>);
199 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
>;
202 if (property
['preserve-line-breaks']) {
203 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
} />;
206 return <input key
={fieldKey
.toString()}
207 id
={fieldKey
.toString()}
211 className
={className
}
212 placeholder
={placeholder
}
216 onMouseDown
={startEditing
}
217 onMouseOver
={startEditing
}
218 onMouseOut
={endEditing
}
219 onMouseLeave
={endEditing
}
220 readOnly
={!isEditable
}
225 function buildElement(container
, property
, valuePath
, value
) {
226 return property
.properties
.map((property
, index
) => {
228 const childPath
= valuePath
.slice();
229 if (typeof value
=== 'object') {
230 childValue
= value
[property
.name
];
232 childPath
.push(property
.name
);
234 return build(container
, property
, childPath
, childValue
);
239 function buildChoice(container
, property
, path
, value
, key
) {
241 function onFormFieldValueChanged(event
) {
242 if (DescriptorModelFactory
.isContainer(this)) {
244 event
.preventDefault();
246 const name
= event
.target
.name
;
247 const value
= event
.target
.value
;
250 Transient State is stored for convenience in the uiState field.
251 The choice yang type uses case elements to describe the "options".
252 A choice can only ever have one option selected which allows
253 the system to determine which type is selected by the name of
254 the element contained within the field.
257 //const stateExample = {
272 const statePath
= ['uiState.choice'].concat(name
);
273 const stateObject
= utils
.resolvePath(this.model
, statePath
.join('.')) || {};
274 // write state back to the model so the new state objects are captured
275 utils
.assignPathValue(this.model
, statePath
.join('.'), stateObject
);
277 // write the current choice value into the state
278 const choiceObject
= utils
.resolvePath(this.model
, [name
, stateObject
.selected
].join('.'));
280 utils
.assignPathValue(stateObject
, ['case', stateObject
.selected
].join('.'), _
.cloneDeep(choiceObject
));
283 // remove the current choice value from the model
284 utils
.removePathValue(this.model
, [name
, stateObject
.selected
].join('.'));
286 // get any state for the new selected choice
287 const newChoiceObject
= utils
.resolvePath(stateObject
, ['case', value
].join('.')) || {};
289 // assign new choice value to the model
290 utils
.assignPathValue(this.model
, [name
, value
].join('.'), newChoiceObject
);
292 // update the selected name
293 utils
.assignPathValue(this.model
, statePath
.concat('selected').join('.'), value
);
295 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
299 const caseByNameMap
= {};
301 const onChange
= onFormFieldValueChanged
.bind(container
);
303 const cases
= property
.properties
.map(d
=> {
304 if (d
.type
=== 'case') {
305 caseByNameMap
[d
.name
] = d
.properties
[0];
306 return {optionName
: d
.name
, optionTitle
: d
.description
};
308 caseByNameMap
[d
.name
] = d
;
309 return {optionName
: d
.name
};
312 const options
= [{optionName
: ''}].concat(cases
).map((d
, i
) => {
314 <option key
={i
} value
={d
.optionName
} title
={d
.optionTitle
}>
316 {i
? null : changeCase
.title(property
.name
)}
321 const selectName
= path
.join('.');
322 const selectedOptionPath
= ['uiState.choice', selectName
, 'selected'].join('.');
323 const selectedOptionValue
= utils
.resolvePath(container
.model
, selectedOptionPath
);
324 const valueProperty
= caseByNameMap
[selectedOptionValue
] || {properties
: []};
326 const valueResponse
= valueProperty
.properties
.map((d
, i
) => {
327 const childPath
= path
.concat(valueProperty
.name
, d
.name
);
328 const childValue
= utils
.resolvePath(container
.model
, childPath
.join('.'));
330 <div key
={childPath
.concat('info', i
).join(':')}>
331 {build(container
, d
, childPath
, childValue
, props
)}
336 const onFocus
= onFocusPropertyFormInputElement
.bind(container
, property
, path
, value
);
339 <div key
={key
} className
="choice">
340 <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
}>
349 function buildSimpleListItem(container
, property
, path
, value
, key
, index
) {
350 // todo need to abstract this better
351 const title
= getTitle(value
);
352 var req
= require
.context("../", true, /\.svg
/);
355 <a href
="#select-list-item" key
={Date
.now()} className
={property
.name
+ '-list-item simple-list-item '} onClick
={onClickSelectItem
.bind(container
, property
, path
, value
)}>
356 <img src
={req('./' + DescriptorModelIconFactory
.getUrlForType(property
.name
))} width
="20px" />
359 {buildRemovePropertyAction(container
, property
, path
)}
364 function buildRemoveListItem(container
, property
, valuePath
, fieldKey
, index
) {
365 const className
= ClassNames(property
.name
+ '-remove actions');
367 <div key
={fieldKey
.concat(index
).join(':')} className
={className
}>
369 <span className
={property
.type
+ '-name name'}>{changeCase
.title(property
.name
)}</span
>
370 <span className
="info">{index
+ 1}</span
>
371 {buildRemovePropertyAction(container
, property
, valuePath
)}
377 function buildLeafListItem(container
, property
, valuePath
, value
, key
, index
) {
378 // look at the type to determine how to parse the value
381 {buildRemoveListItem(container
, property
, valuePath
, key
, index
)}
382 {buildField(container
, property
, valuePath
, value
, key
)}
388 function build(container
, property
, path
, value
, props
= {}) {
391 const isLeaf
= Property
.isLeaf(property
);
392 const isArray
= Property
.isArray(property
);
393 const isObject
= Property
.isObject(property
);
394 const isLeafList
= Property
.isLeafList(property
);
395 const fieldKey
= [container
.id
].concat(path
);
396 const isRequired
= Property
.isRequired(property
);
397 const title
= changeCase
.titleCase(property
.name
);
398 const columnCount
= property
.properties
.length
|| 1;
399 const isColumnar
= isArray
&& (Math
.round(props
.width
/ columnCount
) > 155);
400 const classNames
= {'-is-required': isRequired
, '-is-columnar': isColumnar
};
402 if (!property
.properties
&& isObject
) {
403 const uiState
= DescriptorModelMetaFactory
.getModelMetaForType(property
.name
) || {};
404 property
.properties
= uiState
.properties
;
407 const hasProperties
= _
.isArray(property
.properties
) && property
.properties
.length
;
408 const isMissingDescriptorMeta
= !hasProperties
&& !Property
.isLeaf(property
);
410 // ensure value is not undefined for non-leaf property types
412 if (typeof value
!== 'object') {
413 value
= isArray
? [] : {};
416 const valueAsArray
= _
.isArray(value
) ? value
: isLeafList
&& typeof value
=== 'undefined' ? [] : [value
];
418 const isMetaField
= property
.name
=== 'meta';
419 const isCVNFD
= property
.name
=== 'constituent-vnfd';
420 const isSimpleListView
= Property
.isSimpleList(property
);
422 valueAsArray
.forEach((value
, index
) => {
425 const key
= fieldKey
.slice();
426 const valuePath
= path
.slice();
429 valuePath
.push(index
);
434 if (typeof value
=== 'object') {
435 value
= JSON
.stringify(value
, undefined, 12);
436 } else if (typeof value
!== 'string') {
441 if (isMissingDescriptorMeta
) {
442 field
= <span key
={key
.concat('warning').join(':')} className
="warning">No Descriptor Meta
for {property
.name
}</span
>;
443 } else if (property
.type
=== 'choice') {
444 field
= buildChoice(container
, property
, valuePath
, value
, key
.join(':'));
445 } else if (isSimpleListView
) {
446 field
= buildSimpleListItem(container
, property
, valuePath
, value
, key
, index
);
447 } else if (isLeafList
) {
448 field
= buildLeafListItem(container
, property
, valuePath
, value
, key
, index
);
449 } else if (hasProperties
) {
450 field
= buildElement(container
, property
, valuePath
, value
, key
.join(':'))
452 field
= buildField(container
, property
, valuePath
, value
, key
.join(':'));
455 function onClickLeaf(property
, path
, value
, event
) {
456 if (event
.isDefaultPrevented()) {
459 event
.preventDefault();
460 event
.stopPropagation();
461 this.getRoot().uiState
.focusedPropertyPath
= path
.join('.');
462 console
.log('property selected', path
.join('.'));
463 ComposerAppActions
.propertySelected([path
.join('.')]);
466 const clickHandler
= isLeaf
? onClickLeaf
: () => {};
467 const isContainerList
= isArray
&& !(isSimpleListView
|| isLeafList
);
470 <div key
={fieldKey
.concat(['property-content', index
]).join(':')}
471 className
={ClassNames('property-content', {'simple-list': isSimpleListView
})}
472 onClick
={clickHandler
.bind(container
, property
, valuePath
, value
)}>
473 {isContainerList
? buildRemoveListItem(container
, property
, valuePath
, fieldKey
, index
) : null}
480 classNames
['-is-leaf'] = isLeaf
;
481 classNames
['-is-array'] = isArray
;
482 classNames
['cols-' + columnCount
] = isColumnar
;
484 if (property
.type
=== 'choice') {
485 value
= utils
.resolvePath(container
.model
, ['uiState.choice'].concat(path
, 'selected').join('.'));
488 let displayValue
= typeof value
=== 'object' ? '' : value
;
489 const displayValueInfo
= isArray
? valueAsArray
.filter(d
=> typeof d
!== 'undefined').length
+ ' items' : '';
491 const onFocus
= isLeaf
? event
=> event
.target
.classList
.add('-is-focused') : false;
494 <div key
={fieldKey
.join(':')} className
={ClassNames(property
.type
+ '-property property', classNames
)} onFocus
={onFocus
}>
495 <h3 className
="property-label">
496 <label htmlFor
={fieldKey
.join(':')}>
497 <span className
={property
.type
+ '-name name'}>{title
}</span
>
499 <span className
={property
.type
+ '-info info'}>{displayValueInfo
}</span
>
500 <span className
={property
.type
+ '-value value'}>{displayValue
}</span
>
502 {isArray
? buildAddPropertyAction(container
, property
, path
.concat(valueAsArray
.length
)) : null}
505 <span className
={property
.type
+ '-description description'}>{property
.description
}</span
>
506 <val className
="property-value">
507 {isCVNFD
? <span className
={property
.type
+ '-tip tip'}>Drag a VNFD
from the Catalog to add more
.</span
> : null}
515 const containerType
= container
.uiState
['qualified-type'] || container
.uiState
.type
;
516 const basicProperties
= getDescriptorMetaBasicForType(containerType
).properties
;
518 function buildBasicGroup() {
519 if (basicProperties
.length
=== 0) {
523 <div className
="basic-properties-group">
526 {basicProperties
.map(property
=> {
527 const path
= [property
.name
];
528 const value
= container
.model
[property
.name
];
529 return build(container
, property
, path
, value
);
536 function buildAdvancedGroup() {
537 const properties
= getDescriptorMetaAdvancedForType(containerType
).properties
;
538 if (properties
.length
=== 0) {
541 const hasBasicFields
= basicProperties
.length
> 0;
542 const closeGroup
= basicProperties
.length
> 0;
544 <div className
="advanced-properties-group">
545 <h1 data
-toggle
={closeGroup
? 'true' : 'false'} className
={ClassNames({'-is-toggled': closeGroup
})} onClick
={toggle
} style
={{display
: hasBasicFields
? 'block' : 'none'}}>
546 <a className
="toggle-show-more" href
="#show-more-properties">more
&hellip
;</a
>
547 <a className
="toggle-show-less" href
="#show-more-properties">less
&hellip
;</a
>
549 <div className
="toggleable">
550 {properties
.map(property
=> {
551 const path
= [property
.name
];
552 const value
= container
.model
[property
.name
];
553 return build(container
, property
, path
, value
, {toggle
: true, width
: props
.width
});
556 <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>
561 function buildMoreLess(d, i) {
563 <span key={'bread
-crumb
-part
-' + i}>
564 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
571 if (container.parent) {
572 path.push(container.parent);
574 path.push(container);
577 <div className="EditDescriptorModelProperties -is-tree-view">
578 <h1>{path.map(buildMoreLess)}</h1>
580 {buildAdvancedGroup()}