1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Add JSONata expr tester and improved feedback

This commit is contained in:
Nick O'Leary 2017-05-05 11:23:24 +01:00
parent b030e935ce
commit dbf0486acb
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
15 changed files with 362 additions and 52 deletions

View File

@ -124,6 +124,7 @@ module.exports = function(grunt) {
"editor/js/ui/utils.js", "editor/js/ui/utils.js",
"editor/js/ui/common/editableList.js", "editor/js/ui/common/editableList.js",
"editor/js/ui/common/menu.js", "editor/js/ui/common/menu.js",
"editor/js/ui/common/panels.js",
"editor/js/ui/common/popover.js", "editor/js/ui/common/popover.js",
"editor/js/ui/common/searchBox.js", "editor/js/ui/common/searchBox.js",
"editor/js/ui/common/tabs.js", "editor/js/ui/common/tabs.js",

View File

@ -0,0 +1,83 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.panels = (function() {
function createPanel(options) {
var container = options.container || $("#"+options.id);
var children = container.children();
if (children.length !== 2) {
throw new Error("Container must have exactly two children");
}
container.addClass("red-ui-panels");
var separator = $('<div class="red-ui-panels-separator"></div>').insertAfter(children[0]);
var startPosition;
var panelHeights = [];
var modifiedHeights = false;
var panelRatio;
separator.draggable({
axis: "y",
containment: container,
scroll: false,
start:function(event,ui) {
var height = container.height();
startPosition = ui.position.top;
panelHeights = [$(children[0]).height(),$(children[1]).height()];
console.log("START",height,panelHeights,panelHeights[0]+panelHeights[1],height-(panelHeights[0]+panelHeights[1]));
},
drag: function(event,ui) {
var height = container.height();
var delta = ui.position.top-startPosition;
var newHeights = [panelHeights[0]+delta,panelHeights[1]-delta];
$(children[0]).height(newHeights[0]);
$(children[1]).height(newHeights[1]);
if (options.resize) {
options.resize(newHeights[0],newHeights[1]);
}
ui.position.top -= delta;
panelRatio = newHeights[0]/height;
},
stop:function(event,ui) {
modifiedHeights = true;
}
});
return {
resize: function(height) {
var panelHeights = [$(children[0]).height(),$(children[1]).height()];
container.height(height);
if (modifiedHeights) {
var topPanelHeight = panelRatio*height;
var bottomPanelHeight = height - topPanelHeight - 48;
panelHeights = [topPanelHeight,bottomPanelHeight];
$(children[0]).height(panelHeights[0]);
$(children[1]).height(panelHeights[1]);
console.log("SET",height,panelHeights,panelHeights[0]+panelHeights[1],height-(panelHeights[0]+panelHeights[1]));
}
if (options.resize) {
options.resize(panelHeights[0],panelHeights[1]);
}
}
}
}
return {
create: createPanel
}
})();

View File

@ -1598,6 +1598,9 @@ RED.editor = (function() {
editStack.push({type:type}); editStack.push({type:type});
RED.view.state(RED.state.EDITING); RED.view.state(RED.state.EDITING);
var expressionEditor; var expressionEditor;
var testDataEditor;
var testResultEditor
var panels;
var trayOptions = { var trayOptions = {
title: getEditStackTitle(), title: getEditStackTitle(),
@ -1621,20 +1624,18 @@ RED.editor = (function() {
} }
], ],
resize: function(dimensions) { resize: function(dimensions) {
if (dimensions) {
editTrayWidthCache[type] = dimensions.width; editTrayWidthCache[type] = dimensions.width;
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var editorRow = $("#dialog-form>div.node-text-editor-row");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
} }
height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom"))); var height = $("#dialog-form").height();
$(".node-text-editor").css("height",height+"px"); if (panels) {
expressionEditor.resize(); panels.resize(height);
}
}, },
open: function(tray) { open: function(tray) {
var trayBody = tray.find('.editor-tray-body'); var trayBody = tray.find('.editor-tray-body');
trayBody.addClass("node-input-expression-editor")
var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form','_expression','editor'); var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form','_expression','editor');
var funcSelect = $("#node-input-expression-func"); var funcSelect = $("#node-input-expression-func");
Object.keys(jsonata.functions).forEach(function(f) { Object.keys(jsonata.functions).forEach(function(f) {
@ -1749,6 +1750,118 @@ RED.editor = (function() {
} }
expressionEditor.getSession().setValue(v||"",-1); expressionEditor.getSession().setValue(v||"",-1);
}); });
var tabs = RED.tabs.create({
element: $("#node-input-expression-tabs"),
onchange:function(tab) {
$(".node-input-expression-tab-content").hide();
tab.content.show();
trayOptions.resize();
}
})
tabs.addTab({
id: 'expression-help',
label: 'Function reference',
content: $("#node-input-expression-tab-help")
});
tabs.addTab({
id: 'expression-tests',
label: 'Test',
content: $("#node-input-expression-tab-test")
});
testDataEditor = RED.editor.createEditor({
id: 'node-input-expression-test-data',
value: '{\n "payload": "hello world"\n}',
mode:"ace/mode/json",
lineNumbers: false
});
var changeTimer;
$(".node-input-expression-legacy").click(function(e) {
e.preventDefault();
RED.sidebar.info.set(RED._("expressionEditor.compatModeDesc"));
RED.sidebar.info.show();
})
var testExpression = function() {
var value = testDataEditor.getValue();
var parsedData;
var currentExpression = expressionEditor.getValue();
var expr;
var usesContext = false;
var legacyMode = false;
try {
expr = jsonata(currentExpression);
expr.assign('flow',function(val) {
usesContext = true;
return null;
});
expr.assign('global',function(val) {
usesContext = true;
return null;
});
legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression);
} catch(err) {
testResultEditor.setValue("Invalid jsonata expression:\n "+err.message);
return;
}
$(".node-input-expression-legacy").toggle(legacyMode);
try {
parsedData = JSON.parse(value);
} catch(err) {
testResultEditor.setValue("Invalid example message:\n "+err.toString())
return;
}
try {
var result = expr.evaluate(legacyMode?{msg:parsedData}:parsedData);
if (usesContext) {
testResultEditor.setValue("Cannot test a function that uses context values");
return;
}
var formattedResult = JSON.stringify(result,null,4);
testResultEditor.setValue(formattedResult || "No match");
} catch(err) {
testResultEditor.setValue("Error evaluating expression:\n "+err.message);
}
}
testDataEditor.getSession().on('change', function() {
clearTimeout(changeTimer);
changeTimer = setTimeout(testExpression,200);
});
expressionEditor.getSession().on('change', function() {
clearTimeout(changeTimer);
changeTimer = setTimeout(testExpression,200);
});
testResultEditor = RED.editor.createEditor({
id: 'node-input-expression-test-result',
value: "",
mode:"ace/mode/json",
lineNumbers: false,
readOnly: true
});
panels = RED.panels.create({
id:"node-input-expression-panels",
resize: function(p1Height,p2Height) {
var p1 = $("#node-input-expression-panel-expr");
p1Height -= $(p1.children()[0]).outerHeight(true);
var editorRow = $(p1.children()[1]);
p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-expression").css("height",(p1Height-5)+"px");
expressionEditor.resize();
var p2 = $("#node-input-expression-panel-info > .form-row > div:first-child");
p2Height -= p2.outerHeight(true) + 20;
$(".node-input-expression-tab-content").height(p2Height);
$("#node-input-expression-test-data").css("height",(p2Height-5)+"px");
testDataEditor.resize();
$("#node-input-expression-test-result").css("height",(p2Height-5)+"px");
testResultEditor.resize();
}
});
testExpression();
}, },
close: function() { close: function() {
editStack.pop(); editStack.pop();
@ -1855,6 +1968,7 @@ RED.editor = (function() {
validateNode: validateNode, validateNode: validateNode,
updateNodeProperties: updateNodeProperties, // TODO: only exposed for edit-undo updateNodeProperties: updateNodeProperties, // TODO: only exposed for edit-undo
createEditor: function(options) { createEditor: function(options) {
var editor = ace.edit(options.id); var editor = ace.edit(options.id);
editor.setTheme("ace/theme/tomorrow"); editor.setTheme("ace/theme/tomorrow");
@ -1875,6 +1989,12 @@ RED.editor = (function() {
enableSnippets:true enableSnippets:true
}); });
} }
if (options.readOnly) {
editor.setOption('readOnly',options.readOnly);
}
if (options.hasOwnProperty('lineNumbers')) {
editor.renderer.setOption('showGutter',options.lineNumbers);
}
editor.$blockScrolling = Infinity; editor.$blockScrolling = Infinity;
if (options.value) { if (options.value) {
session.setValue(options.value,-1); session.setValue(options.value,-1);

View File

@ -350,7 +350,8 @@ RED.sidebar.info = (function() {
// tips.stop(); // tips.stop();
sections.show(); sections.show();
nodeSection.container.hide(); nodeSection.container.hide();
$(infoSection.content).html(html); var wrapped = $('<div class="node-help"></div>').html(html);
$(infoSection.content).empty().append(wrapped);
} }

View File

@ -48,8 +48,10 @@
.editor-tray-body { .editor-tray-body {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
form { padding: 0.1px; // prevent margin collapsing
.dialog-form,#dialog-form, #dialog-config-form {
margin: 20px; margin: 20px;
height: calc(100% - 40px);
} }
} }
.editor-tray-content { .editor-tray-content {
@ -154,7 +156,7 @@
.dialog-form,#dialog-form, #dialog-config-form { .dialog-form,#dialog-form, #dialog-config-form {
height: calc(100% - 20px); height: 100%;
} }
.input-error { .input-error {
@ -245,3 +247,48 @@
font-size: 12px; font-size: 12px;
line-height: 35px; line-height: 35px;
} }
.node-input-expression-editor #dialog-form {
margin: 0;
height: 100%;
.red-ui-panel {
&:first-child {
padding: 20px 20px 0;
}
&:last-child {
padding-bottom: 20px;
}
}
}
.node-input-expression-tab-content {
position: relative;
padding: 0 20px;
}
#node-input-expression-help {
position: absolute;
top: 35px;
left:0;
right: 0;
bottom:0;
padding: 0 20px;
overflow: auto;
box-sizing: border-box;
}
#node-input-expression-panel-info {
& > .form-row {
margin: 0;
& > div:first-child {
margin-top: 10px;
}
}
}
.node-input-expression-legacy {
float: left;
cursor: pointer;
border: 1px solid white;
padding: 0 5px;
border-radius: 2px;
&:hover {
border-color: $primary-border-color;
}
}

23
editor/sass/panels.scss Normal file
View File

@ -0,0 +1,23 @@
.red-ui-panels {
position: relative;
& > div {
// border: 1px solid red;
box-sizing: border-box;
}
}
.red-ui-panels-separator {
border-top: 1px solid $secondary-border-color;
border-bottom: 1px solid $secondary-border-color;
height: 7px;
box-sizing: border-box;
cursor: ns-resize;
background: $background-color url(images/grip.png) no-repeat 50% 50%;
}
.red-ui-panel {
overflow: auto;
height: calc(50% - 4px);
}

View File

@ -37,6 +37,7 @@
@import "library"; @import "library";
@import "search"; @import "search";
@import "panels";
@import "tabs"; @import "tabs";
@import "tab-config"; @import "tab-config";
@import "tab-info"; @import "tab-info";

View File

@ -166,22 +166,41 @@
</script> </script>
<script type="text/x-red" data-template-name="_expression"> <script type="text/x-red" data-template-name="_expression">
<div id="node-input-expression-panels">
<div id="node-input-expression-panel-expr" class="red-ui-panel">
<div class="form-row" style="margin-bottom: 3px; text-align: right;"> <div class="form-row" style="margin-bottom: 3px; text-align: right;">
<button id="node-input-expression-reformat" class="editor-button editor-button-small">format</button> <span class="node-input-expression-legacy"><i class="fa fa-exclamation-circle"></i> <span data-i18n="expressionEditor.compatMode"></span></span>
<button id="node-input-expression-reformat" class="editor-button editor-button-small"><span data-i18n="expressionEditor.format"></span></button>
</div> </div>
<div class="form-row node-text-editor-row"> <div class="form-row node-text-editor-row">
<div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-expression"></div> <div class="node-text-editor" id="node-input-expression"></div>
</div> </div>
</div>
<div id="node-input-expression-panel-info" class="red-ui-panel">
<div class="form-row"> <div class="form-row">
<label for="node-input-expression-func" data-i18n="expressionEditor.functions"></label> <ul id="node-input-expression-tabs"></ul>
<div id="node-input-expression-tab-help" class="node-input-expression-tab-content hide">
<div>
<select id="node-input-expression-func"></select> <select id="node-input-expression-func"></select>
<button id="node-input-expression-func-insert" class="editor-button" data-i18n="expressionEditor.insert"></button> <button id="node-input-expression-func-insert" class="editor-button" data-i18n="expressionEditor.insert"></button>
<div style="min-height: 200px;" id="node-input-expression-help"></div> </div>
<div id="node-input-expression-help"></div>
</div>
<div id="node-input-expression-tab-test" class="node-input-expression-tab-content hide">
<div>
<span style="display: inline-block; width: calc(50% - 5px);" data-i18n="expressionEditor.data"></span>
<span style="display: inline-block; width: calc(50% - 5px);" data-i18n="expressionEditor.result"></span>
</div>
<div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-data"></div>
<div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-result"></div>
</div>
</div>
</div>
</div> </div>
</script> </script>
<script type="text/x-red" data-template-name="_json"> <script type="text/x-red" data-template-name="_json">
<div class="form-row" style="margin-bottom: 3px; text-align: right;"> <div class="form-row" style="margin-bottom: 3px; text-align: right;">
<button id="node-input-expression-reformat" class="editor-button editor-button-small">format</button> <button id="node-input-expression-reformat" class="editor-button editor-button-small"><span data-i18n="jsonEditor.format"></span></button>
</div> </div>
<div class="form-row node-text-editor-row"> <div class="form-row node-text-editor-row">
<div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div> <div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div>

View File

@ -111,11 +111,10 @@
'$average':{ args:['value'] }, '$average':{ args:['value'] },
'$boolean':{ args:['value'] }, '$boolean':{ args:['value'] },
'$contains':{ args:['str','pattern']}, '$contains':{ args:['str','pattern']},
'$context': {args:['string']},
'$count':{ args:['array'] }, '$count':{ args:['array'] },
'$exists':{ args:['value'] }, '$exists':{ args:['value'] },
'$flow': {args:['string']}, '$flowContext': {args:['string']},
'$global': {args:['string']}, '$globalContext': {args:['string']},
'$join':{ args:['array','separator'] }, '$join':{ args:['array','separator'] },
'$keys':{ args:['object'] }, '$keys':{ args:['object'] },
'$length':{ args:['string'] }, '$length':{ args:['string'] },

View File

@ -519,7 +519,7 @@
"else":"otherwise" "else":"otherwise"
}, },
"errors": { "errors": {
"invalid-expr": "Invalid expression: __error__" "invalid-expr": "Invalid JSONata expression: __error__"
} }
}, },
"change": { "change": {
@ -544,7 +544,8 @@
}, },
"errors": { "errors": {
"invalid-from": "Invalid 'from' property: __error__", "invalid-from": "Invalid 'from' property: __error__",
"invalid-json": "Invalid 'to' JSON property" "invalid-json": "Invalid 'to' JSON property",
"invalid-expr": "Invalid JSONata expression: __error__"
} }
}, },
"range": { "range": {

View File

@ -104,7 +104,7 @@ module.exports = function(RED) {
try { try {
var prop; var prop;
if (node.propertyType === 'jsonata') { if (node.propertyType === 'jsonata') {
prop = node.property.evaluate({msg:msg}); prop = RED.util.evaluateJSONataExpression(node.property,msg);
} else { } else {
prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
} }
@ -117,7 +117,7 @@ module.exports = function(RED) {
v1 = node.previousValue; v1 = node.previousValue;
} else if (rule.vt === 'jsonata') { } else if (rule.vt === 'jsonata') {
try { try {
v1 = rule.v.evaluate({msg:msg}); v1 = RED.util.evaluateJSONataExpression(rule.v,msg);
} catch(err) { } catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message})); node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return; return;
@ -134,7 +134,7 @@ module.exports = function(RED) {
v2 = node.previousValue; v2 = node.previousValue;
} else if (rule.v2t === 'jsonata') { } else if (rule.v2t === 'jsonata') {
try { try {
v2 = rule.v2.evaluate({msg:msg}); v2 = RED.util.evaluateJSONataExpression(rule.v2,msg);
} catch(err) { } catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message})); node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return; return;

View File

@ -19,6 +19,7 @@ module.exports = function(RED) {
function ChangeNode(n) { function ChangeNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
var node = this;
this.rules = n.rules; this.rules = n.rules;
var rule; var rule;
@ -90,7 +91,7 @@ module.exports = function(RED) {
rule.to = RED.util.prepareJSONataExpression(rule.to,this); rule.to = RED.util.prepareJSONataExpression(rule.to,this);
} catch(e) { } catch(e) {
valid = false; valid = false;
this.error(RED._("change.errors.invalid-from",{error:e.message})); this.error(RED._("change.errors.invalid-expr",{error:e.message}));
} }
} }
} }
@ -115,7 +116,12 @@ module.exports = function(RED) {
} else if (rule.tot === 'date') { } else if (rule.tot === 'date') {
value = Date.now(); value = Date.now();
} else if (rule.tot === 'jsonata') { } else if (rule.tot === 'jsonata') {
value = rule.to.evaluate({msg:msg}); try{
value = RED.util.evaluateJSONataExpression(rule.to,msg);
} catch(err) {
node.error(RED._("change.errors.invalid-expr",{error:err.message}));
return;
}
} }
if (rule.t === 'change') { if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
@ -219,7 +225,6 @@ module.exports = function(RED) {
return msg; return msg;
} }
if (valid) { if (valid) {
var node = this;
this.on('input', function(msg) { this.on('input', function(msg) {
for (var i=0; i<this.rules.length; i++) { for (var i=0; i<this.rules.length; i++) {
if (this.rules[i].t === "move") { if (this.rules[i].t === "move") {

View File

@ -398,9 +398,15 @@
"expressionEditor": { "expressionEditor": {
"functions": "Functions", "functions": "Functions",
"insert": "Insert", "insert": "Insert",
"title": "JSONata Expression editor" "title": "JSONata Expression editor",
"data": "Example message",
"result": "Result",
"format": "format expression",
"compatMode": "Compatibility mode enabled",
"compatModeDesc": "<h3>JSONata compatibility mode</h3><p> The current expression appears to still reference <code>msg</code> so will be evaluated in compatibility mode. Please update the expression to not use <code>msg</code> as this mode will be removed in the future.</p><p> When JSONata support was first added to Node-RED, it required the expression to reference the <code>msg</code> object. For example <code>msg.payload</code> would be used to access the payload.</p><p> That is no longer necessary as the expression will be evaluated against the message directly. To access the payload, the expression should be just <code>payload</code>.</p>"
}, },
"jsonEditor": { "jsonEditor": {
"title": "JSON editor" "title": "JSON editor",
"format": "format JSON"
} }
} }

View File

@ -110,16 +110,11 @@
"args": "object", "args": "object",
"desc": "Splits an object containing key/value pairs into an array of objects, each of which has a single key/value pair from the input object. If the parameter is an array of objects, then the resultant array contains an object for every key/value pair in every object in the supplied array." "desc": "Splits an object containing key/value pairs into an array of objects, each of which has a single key/value pair from the input object. If the parameter is an array of objects, then the resultant array contains an object for every key/value pair in every object in the supplied array."
}, },
"$flowContext": {
"$context": {
"args": "string",
"desc": "Retrieves a node context property."
},
"$flow": {
"args": "string", "args": "string",
"desc": "Retrieves a flow context property." "desc": "Retrieves a flow context property."
}, },
"$global": { "$globalContext": {
"args": "string", "args": "string",
"desc": "Retrieves a global context property." "desc": "Retrieves a global context property."
} }

View File

@ -323,25 +323,33 @@ function evaluateNodeProperty(value, type, node, msg) {
} else if (type === 'bool') { } else if (type === 'bool') {
return /^true$/i.test(value); return /^true$/i.test(value);
} else if (type === 'jsonata') { } else if (type === 'jsonata') {
return prepareJSONataExpression(value,node).evaluate({msg:msg}); var expr = prepareJSONataExpression(value,node);
return evaluateJSONataExpression(expr,msg);
} }
return value; return value;
} }
function prepareJSONataExpression(value,node) { function prepareJSONataExpression(value,node) {
var expr = jsonata(value); var expr = jsonata(value);
expr.assign('context',function(val) { expr.assign('flowContext',function(val) {
return node.context().get(val);
});
expr.assign('flow',function(val) {
return node.context().flow.get(val); return node.context().flow.get(val);
}); });
expr.assign('global',function(val) { expr.assign('globalContext',function(val) {
return node.context().global(val); return node.context().global(val);
}); });
expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value);
return expr; return expr;
} }
function evaluateJSONataExpression(expr,msg) {
var context = msg;
if (expr._legacyMode) {
context = {msg:msg};
}
return expr.evaluate(context);
}
function normaliseNodeTypeName(name) { function normaliseNodeTypeName(name) {
var result = name.replace(/[^a-zA-Z0-9]/g, " "); var result = name.replace(/[^a-zA-Z0-9]/g, " ");
result = result.trim(); result = result.trim();
@ -366,5 +374,6 @@ module.exports = {
evaluateNodeProperty: evaluateNodeProperty, evaluateNodeProperty: evaluateNodeProperty,
normalisePropertyExpression: normalisePropertyExpression, normalisePropertyExpression: normalisePropertyExpression,
normaliseNodeTypeName: normaliseNodeTypeName, normaliseNodeTypeName: normaliseNodeTypeName,
prepareJSONataExpression: prepareJSONataExpression prepareJSONataExpression: prepareJSONataExpression,
evaluateJSONataExpression: evaluateJSONataExpression
}; };