update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / admin / src / components / editor / LeafEditor.jsx
1 /*
2  *
3  *   Copyright 2016-2017 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 import _debounce from 'lodash/debounce';
20 import React from 'react'
21 import ClassNames from 'classnames'
22 import changeCase from 'change-case'
23 import _isInt from 'validator/lib/isInt'
24 import _toInt from 'validator/lib/toInt'
25 import _isFloat from 'validator/lib/isFloat'
26 import _toFloat from 'validator/lib/toFloat'
27 import _trim from 'validator/lib/trim'
28 import _isIP from 'validator/lib/isIP'
29 import yang from '../../yang/leaf-utils.js'
30 import resolveLeafRefPaths from './resolveLeafRef'
31 import Select from './Select'
32
33 function validateRequired(isRequired, value) {
34         value = value.trim();
35         return isRequired && !value ? { success: false, message: "A value is required." } : { success: true, value: null };
36 }
37
38 function editorExitHandler(isValueRequired, onExit, onError, event) {
39         const value = event.target.value;
40         const result = validateRequired(isValueRequired, value);
41         onExit && onExit(result);
42 }
43
44 function Enumeration(props) {
45         const { id, property, title, readOnly, onChange, onError, onExit } = props;
46         let value = props.value;
47         const enumDef = property['data-type'].enumeration.enum;
48         const enumeration = typeof enumDef === 'string' ?
49                 [{ name: enumDef, value: enumDef, isSelected: String(value) === enumDef }]
50                 : Object.keys(enumDef).map(enumName => {
51                         let enumValue = enumName;
52                         return { name: enumName, value: enumValue, isSelected: String(enumValue) === String(value) };
53                 });
54         const hasDefaultValue = !!yang.getDefaultValue(property);
55         const required = yang.isRequired(property) || hasDefaultValue;
56         if (!value && hasDefaultValue) {
57                 value = yang.getDefaultValue(property);
58         }
59         return (
60                 <Select
61                         id={id}
62                         value={value}
63                         options={enumeration}
64                         title={title}
65                         placeholder={property.name}
66                         onChange={onChange}
67                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
68                         required={required} r
69                         readOnly={readOnly} />
70         );
71 }
72
73 function Reference(props) {
74         const { id, property, title, value, path, model, readOnly, onChange, onError, onExit } = props;
75
76         function getLeafRef(property = {}, path, value, model) {
77                 const leafRefPath = property['data-type']['leafref']['path'];
78
79                 let leafRefPathValues = []; //resolveLeafRefPath(model, path, leafRefPath);
80
81                 let leafRefObjects = [];
82
83                 leafRefPathValues && leafRefPathValues.map((leafRefPathValue) => {
84                         leafRefObjects.push({
85                                 name: leafRefPathValue,
86                                 value: leafRefPathValue,
87                                 isSelected: String(leafRefPathValue) === String(value)
88                         });
89                 });
90
91                 return leafRefObjects;
92         }
93         const leafRefPathValues = getLeafRef(property, path, value, model);
94         const required = yang.isRequired(property);
95
96         return (
97                 <Select
98                         id={id}
99                         value={value}
100                         options={leafRefPathValues}
101                         title={title}
102                         placeholder={property.name}
103                         onChange={onChange}
104                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
105                         required={required}
106                         readOnly={readOnly} />
107         );
108 }
109
110 function Boolean(props) {
111         const { id, property, title, readOnly, onChange, onError, onExit } = props;
112         let value = props.value;
113         const typeOfValue = typeof value;
114         if (typeOfValue === 'number' || typeOfValue === 'boolean') {
115                 value = value ? 'TRUE' : 'FALSE';
116         } else if (value) {
117                 value = value.toUpperCase();
118         }
119         const options = [
120                 { name: "TRUE", value: 'TRUE' },
121                 { name: "FALSE", value: 'FALSE' }
122         ]
123         const hasDefaultValue = !!yang.getDefaultValue(property);
124         const required = yang.isRequired(property) || hasDefaultValue;
125         if (!value && hasDefaultValue) {
126                 value = yang.getDefaultValue(property);
127         }
128         return (
129                 <Select
130                         id={id}
131                         value={value}
132                         options={options}
133                         title={title}
134                         placeholder={property.name}
135                         onChange={onChange}
136                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
137                         required={required}
138                         readOnly={readOnly} />
139         );
140 }
141
142 function Empty(props) {
143         // A null value indicates the leaf exists (as opposed to undefined).
144         // We stick in a string when the user actually sets it to simplify things
145         // but the correct thing happens when we serialize to user data
146         const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
147         const { id, property, value, title, readOnly, onChange } = props;
148         let isEmptyLeafPresent = !!value;
149         let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
150         const options = [
151                 { name: "Enabled", value: EMPTY_LEAF_PRESENT }
152         ]
153
154         return (
155                 <Select
156                         id={id}
157                         value={present}
158                         placeholder={"Not Enabled"}
159                         options={options}
160                         title={title}
161                         onChange={onChange}
162                         readOnly={readOnly} />
163         );
164 }
165
166 function getValidator(property) {
167         function validateInteger(constraints, value) {
168                 return _isInt(value, constraints) ? { success: true, value: _toInt(value) } :
169                         { success: false, value, message: "The value is not an integer or does not meet the property constraints." };
170         }
171         function validateDecimal(constraints, value) {
172                 return _isFloat(value, constraints) ? { success: true, value: _toFloat(value) } :
173                         { success: false, value, message: "The value is not a decimal number or does not meet the property constraints." };
174         }
175         function validateProperty(validator, errorMessage, value) {
176                 return validator(value) ? { success: true, value } :
177                         { success: false, value, message: errorMessage };
178         }
179         const name = property.name;
180         if (name === 'ip-address' || name.endsWith('-ip-address')) {
181                 return validateProperty.bind(null, _isIP, "The value is not a valid ip address.")
182         }
183         switch (property['data-type']) {
184                 case 'int8':
185                         return validateInteger.bind(null, { min: -128, max: 127 });
186                 case 'int16':
187                         return validateInteger.bind(null, { min: -32768, max: 32767 });
188                 case 'int32':
189                         return validateInteger.bind(null, { min: -2147483648, max: 2147483647 });
190                 case 'int64':
191                         return validateInteger.bind(null, null);
192                 case 'uint8':
193                         return validateInteger.bind(null, { min: 0, max: 255 });
194                 case 'uint16':
195                         return validateInteger.bind(null, { min: 0, max: 65535 });
196                 case 'uint32':
197                         return validateInteger.bind(null, { min: 0, max: 4294967295 });
198                 case 'uint64':
199                         return validateInteger.bind(null, { min: 0 });
200                 case 'decimal64':
201                         return validateDecimal.bind(null, null)
202                 case 'string':
203                 default:
204                         return function (value) { return { success: true, value } };
205         }
206 }
207
208 function messageTemplate(strings, ...keys) {
209         return (function (...vars) {
210                 let helpInfo = vars.reduce((o, info) => Object.assign(o, info), {});
211                 return keys.reduce((s, key, i) => {
212                         return s + helpInfo[key] + strings[i + 1];
213                 }, strings[0]);
214         });
215 }
216
217 const errorMessage = messageTemplate`"${'value'}" is ${'error'}. ${'message'}`;
218
219 const inputDemensionStyle = { maxWidth: '100%', minWidth: '100%' };
220
221 class Input extends React.Component {
222         constructor(props) {
223                 super(props);
224                 let originalValue = yang.isValueSet(props.property, props.value) ? props.value : null; // normalize empty value
225                 this.state = { originalValue };
226         }
227
228         componentWillReceiveProps(nextProps) {
229                 const { value } = nextProps
230                 if (value !== this.state.originalValue) {
231                         let originalValue = value ? value : null; // normalize empty value
232                         this.setState({ originalValue })
233                 }
234         }
235
236         render() {
237                 const { id, property, value, title, readOnly, onChange, onError, onExit } = this.props;
238                 const { originalValue } = this.state;
239                 const placeholder = property.name;
240                 const required = yang.isRequired(property);
241                 const className = ClassNames(property.name + '-input', { '-is-required': required });
242
243                 const validator = getValidator(property);
244                 function handleValueChanged(newValue) {
245                         newValue = newValue.trim();
246                         const result = !newValue ? validateRequired(required, newValue) : validator(newValue);
247                         result.success ? onChange(result.value) : onError(result.message);
248                 }
249                 const changeHandler = _debounce(handleValueChanged, 2000);
250                 function onInputChange(e) {
251                         e.preventDefault();
252                         changeHandler(_trim(e.target.value));
253                 }
254                 function onBlur(e) {
255                         changeHandler.cancel();
256                         const value = _trim(e.target.value);
257                         const result = !value ? validateRequired(required, value) : validator(value);
258                         // just in case we missed it by cancelling the debouncer
259                         result.success ? onChange(result.value) : onError(result.message);
260                         onExit(result);
261                 }
262                 // if (!yang.isKey(property) && yang.isString(property)) {
263                 //      return (
264                 //              <textarea
265                 //                      cols="5"
266                 //                      id={id}
267                 //                      defaultValue={value}
268                 //                      placeholder={placeholder}
269                 //                      className={className}
270                 //                      onChange={onInputChange}
271                 //                      onBlur={onBlur}
272                 //                      required={required} 
273                 //                      readOnly={readOnly} 
274                 //                      style={inputDemensionStyle}/>
275                 //      );
276                 // }
277                 return (
278                         <input
279                                 id={id}
280                                 type="text"
281                                 defaultValue={value}
282                                 className={className}
283                                 placeholder={placeholder}
284                                 onChange={onInputChange}
285                                 onBlur={onBlur}
286                                 required={required}
287                                 readOnly={readOnly}
288                                 style={inputDemensionStyle}
289                         />
290                 );
291         }
292 }
293
294 export {
295         Input,
296         Empty,
297         Boolean,
298         Reference,
299         Enumeration
300 };