Merge branch 'master' into repackage

This commit is contained in:
Nick O'Leary
2018-08-15 20:46:56 +01:00
13 changed files with 510 additions and 228 deletions

View File

@@ -16,7 +16,7 @@
RED.editor.types._js = (function() {
var template = '<script type="text/x-red" data-template-name="_js"><div class="form-row node-text-editor-row" style="width: 700px"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-js"></div></div></script>';
var template = '<script type="text/x-red" data-template-name="_js"><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-js"></div></div></script>';
return {
init: function() {
@@ -58,7 +58,6 @@ RED.editor.types._js = (function() {
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
expressionEditor.resize();
},

View File

@@ -280,8 +280,7 @@ RED.sidebar.info = (function() {
if (infoText) {
setInfoText(infoText);
}
$(".sidebar-node-info-stack").scrollTop(0);
$(".node-info-property-header").click(function(e) {
e.preventDefault();
expandedSections["property"] = !expandedSections["property"];
@@ -395,10 +394,9 @@ RED.sidebar.info = (function() {
function set(html,title) {
// tips.stop();
// sections.show();
// nodeSection.container.hide();
infoSection.title.text(title||"");
refresh(null);
$(infoSection.content).empty();
nodeSection.container.hide();
infoSection.title.text(title||RED._("sidebar.info.info"));
setInfoText(html);
$(".sidebar-node-info-stack").scrollTop(0);
}

View File

@@ -65,13 +65,28 @@
.attr('height',chartSize[1]/nav_scale/scaleFactor)
}
}
function toggle() {
if (!isShowing) {
isShowing = true;
$("#btn-navigate").addClass("selected");
resizeNavBorder();
refreshNodes();
$("#chart").on("scroll",onScroll);
navContainer.fadeIn(200);
} else {
isShowing = false;
navContainer.fadeOut(100);
$("#chart").off("scroll",onScroll);
$("#btn-navigate").removeClass("selected");
}
}
return {
init: function() {
$(window).resize(resizeNavBorder);
RED.events.on("sidebar:resize",resizeNavBorder);
RED.actions.add("core:toggle-navigator",toggle);
var hideTimeout;
navContainer = $('<div>').css({
@@ -141,23 +156,12 @@
$("#btn-navigate").click(function(evt) {
evt.preventDefault();
if (!isShowing) {
isShowing = true;
$("#btn-navigate").addClass("selected");
resizeNavBorder();
refreshNodes();
$("#chart").on("scroll",onScroll);
navContainer.fadeIn(200);
} else {
isShowing = false;
navContainer.fadeOut(100);
$("#chart").off("scroll",onScroll);
$("#btn-navigate").removeClass("selected");
}
toggle();
})
},
refresh: refreshNodes,
resize: resizeNavBorder
resize: resizeNavBorder,
toggle: toggle
}

View File

@@ -61,7 +61,13 @@
}
#user-settings-tab-view {
position: absolute;
top:0;
right: 0;
left: 0;
bottom: 0;
padding: 8px 20px 20px;
overflow-y: scroll;
}
.user-settings-row {
padding: 5px 10px 2px;

View File

@@ -19,6 +19,19 @@ module.exports = function(RED) {
var mustache = require("mustache");
var yaml = require("js-yaml");
function extractTokens(tokens,set) {
set = set || new Set();
tokens.forEach(function(token) {
if (token[0] !== 'text') {
set.add(token[1]);
if (token.length > 4) {
extractTokens(token[4],set);
}
}
});
return set;
}
function parseContext(key) {
var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
if (match) {
@@ -36,22 +49,16 @@ module.exports = function(RED) {
* flow and global context
*/
function NodeContext(msg, nodeContext, parent, escapeStrings, promises, results) {
function NodeContext(msg, nodeContext, parent, escapeStrings, cachedContextTokens) {
this.msgContext = new mustache.Context(msg,parent);
this.nodeContext = nodeContext;
this.escapeStrings = escapeStrings;
this.promises = promises;
this.results = results;
this.cachedContextTokens = cachedContextTokens;
}
NodeContext.prototype = new mustache.Context();
NodeContext.prototype.lookup = function (name) {
var results = this.results;
if (results) {
var val = results.shift();
return val;
}
// try message first:
try {
var value = this.msgContext.lookup(name);
@@ -64,7 +71,6 @@ module.exports = function(RED) {
value = value.replace(/\f/g, "\\f");
value = value.replace(/[\b]/g, "\\b");
}
this.promises.push(Promise.resolve(value));
return value;
}
@@ -76,28 +82,10 @@ module.exports = function(RED) {
var field = context.field;
var target = this.nodeContext[type];
if (target) {
var promise = new Promise((resolve, reject) => {
var callback = (err, val) => {
if (err) {
reject(err);
} else {
resolve(val);
}
};
target.get(field, store, callback);
});
this.promises.push(promise);
return '';
}
else {
this.promises.push(Promise.resolve(''));
return '';
return this.cachedContextTokens[name];
}
}
else {
this.promises.push(Promise.resolve(''));
return '';
}
return '';
}
catch(err) {
throw err;
@@ -105,7 +93,7 @@ module.exports = function(RED) {
}
NodeContext.prototype.push = function push (view) {
return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.promises, this.results);
return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens);
};
function TemplateNode(n) {
@@ -118,33 +106,36 @@ module.exports = function(RED) {
this.outputFormat = n.output || "str";
var node = this;
node.on("input", function(msg) {
function output(value) {
/* istanbul ignore else */
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
/* istanbul ignore else */
if (node.outputFormat === "yaml") {
value = yaml.load(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg, node.field, value);
node.send(msg);
} else if ((node.fieldType === 'flow') ||
(node.fieldType === 'global')) {
var context = RED.util.parseContextStore(node.field);
var target = node.context()[node.fieldType];
target.set(context.key, value, context.store, function (err) {
if (err) {
node.error(err, msg);
} else {
node.send(msg);
}
});
}
function output(msg,value) {
/* istanbul ignore else */
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
/* istanbul ignore else */
if (node.outputFormat === "yaml") {
value = yaml.load(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg, node.field, value);
node.send(msg);
} else if ((node.fieldType === 'flow') ||
(node.fieldType === 'global')) {
var context = RED.util.parseContextStore(node.field);
var target = node.context()[node.fieldType];
target.set(context.key, value, context.store, function (err) {
if (err) {
node.error(err, msg);
} else {
node.send(msg);
}
});
}
}
node.on("input", function(msg) {
try {
/***
* Allow template contents to be defined externally
@@ -160,19 +151,44 @@ module.exports = function(RED) {
if (node.syntax === "mustache") {
var is_json = (node.outputFormat === "json");
var promises = [];
mustache.render(template, new NodeContext(msg, node.context(), null, is_json, promises, null));
Promise.all(promises).then(function (values) {
var value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, null, values));
output(value);
var tokens = extractTokens(mustache.parse(template));
var resolvedTokens = {};
tokens.forEach(function(name) {
var context = parseContext(name);
if (context) {
var type = context.type;
var store = context.store;
var field = context.field;
var target = node.context()[type];
if (target) {
var promise = new Promise((resolve, reject) => {
target.get(field, store, (err, val) => {
if (err) {
reject(err);
} else {
resolvedTokens[name] = val;
resolve();
}
});
});
promises.push(promise);
return;
}
}
});
Promise.all(promises).then(function() {
var value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, resolvedTokens));
output(msg, value);
}).catch(function (err) {
node.error(err.message);
node.error(err.message,msg);
});
} else {
output(template);
output(msg, template);
}
}
catch(err) {
node.error(err.message);
node.error(err.message, msg);
}
});
}

View File

@@ -112,7 +112,15 @@ function load() {
try {
// Create a new instance of the plugin by calling its module function
stores[pluginName] = plugin(config);
log.info(log._("context.log-store-init", {name:pluginName, info:"module="+plugins[pluginName].module}));
var moduleInfo = plugins[pluginName].module;
if (typeof moduleInfo !== 'string') {
if (moduleInfo.hasOwnProperty("toString")) {
moduleInfo = moduleInfo.toString();
} else {
moduleInfo = "custom";
}
}
log.info(log._("context.log-store-init", {name:pluginName, info:"module="+moduleInfo}));
} catch(err) {
return reject(new Error(log._("context.error-loading-module",{module:pluginName,message:err.toString()})));
}

View File

@@ -19,12 +19,16 @@
*
* Configuration options:
* {
* base: "contexts", // the base directory to use
* // default: "contexts"
* base: "context", // the base directory to use
* // default: "context"
* dir: "/path/to/storage", // the directory to create the base directory in
* // default: settings.userDir
* cache: true // whether to cache contents in memory
* cache: true, // whether to cache contents in memory
* // default: true
* flushInterval: 30 // if cache is enabled, the minimum interval
* // between writes to storage, in seconds. This
* can be used to reduce wear on underlying storage.
* default: 30 seconds
* }
*
*
@@ -44,9 +48,12 @@
var fs = require('fs-extra');
var path = require("path");
var util = require("../../util");
var log = require("../../log");
var safeJSONStringify = require("json-stringify-safe");
var MemoryStore = require("./memory");
function getStoragePath(storageBaseDir, scope) {
if(scope.indexOf(":") === -1){
if(scope === "global"){
@@ -61,7 +68,7 @@ function getStoragePath(storageBaseDir, scope) {
}
function getBasePath(config) {
var base = config.base || "contexts";
var base = config.base || "context";
var storageBaseDir;
if (!config.dir) {
if(config.settings && config.settings.userDir){
@@ -102,12 +109,38 @@ function loadFile(storagePath){
});
}
function listFiles(storagePath) {
var promises = [];
return fs.readdir(storagePath).then(function(files) {
files.forEach(function(file) {
promises.push(fs.readdir(path.join(storagePath,file)).then(function(subdirFiles) {
return subdirFiles.map(subfile => path.join(file,subfile));
}))
});
return Promise.all(promises);
}).then(dirs => dirs.reduce((acc, val) => acc.concat(val), []));
}
function stringify(value) {
var hasCircular;
var result = safeJSONStringify(value,null,4,function(k,v){hasCircular = true})
return { json: result, circular: hasCircular };
}
function LocalFileSystem(config){
this.config = config;
this.storageBaseDir = getBasePath(this.config);
if (config.hasOwnProperty('cache')?config.cache:true) {
this.cache = MemoryStore({});
}
this.pendingWrites = {};
this.knownCircularRefs = {};
if (config.hasOwnProperty('flushInterval')) {
this.flushInterval = Math.max(0,config.flushInterval) * 1000;
} else {
this.flushInterval = 30000;
}
}
LocalFileSystem.prototype.open = function(){
@@ -115,25 +148,17 @@ LocalFileSystem.prototype.open = function(){
if (this.cache) {
var scopes = [];
var promises = [];
var subdirs = [];
var subdirPromises = [];
return fs.readdir(self.storageBaseDir).then(function(dirs){
dirs.forEach(function(fn) {
var p = getStoragePath(self.storageBaseDir ,fn)+".json";
scopes.push(fn);
promises.push(loadFile(p));
subdirs.push(path.join(self.storageBaseDir,fn));
subdirPromises.push(fs.readdir(path.join(self.storageBaseDir,fn)));
})
return Promise.all(subdirPromises);
}).then(function(dirs) {
dirs.forEach(function(files,i) {
files.forEach(function(fn) {
if (fn !== 'flow.json' && fn !== 'global.json') {
scopes.push(fn.substring(0,fn.length-5)+":"+scopes[i]);
promises.push(loadFile(path.join(subdirs[i],fn)))
}
});
return listFiles(self.storageBaseDir).then(function(files) {
files.forEach(function(file) {
var parts = file.split(path.sep);
if (parts[0] === 'global') {
scopes.push("global");
} else if (parts[1] === 'flow.json') {
scopes.push(parts[0])
} else {
scopes.push(parts[1].substring(0,parts[1].length-5)+":"+parts[0]);
}
promises.push(loadFile(path.join(self.storageBaseDir,file)));
})
return Promise.all(promises);
}).then(function(res) {
@@ -149,13 +174,40 @@ LocalFileSystem.prototype.open = function(){
}else{
return Promise.reject(err);
}
}).then(function() {
self._flushPendingWrites = function() {
var scopes = Object.keys(self.pendingWrites);
self.pendingWrites = {};
var promises = [];
var newContext = self.cache._export();
scopes.forEach(function(scope) {
var storagePath = getStoragePath(self.storageBaseDir,scope);
var context = newContext[scope];
var stringifiedContext = stringify(context);
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
log.warn("Context "+scope+" contains a circular reference that cannot be persisted");
self.knownCircularRefs[scope] = true;
} else {
delete self.knownCircularRefs[scope];
}
log.debug("Flushing localfilesystem context scope "+scope);
promises.push(fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8"));
});
delete self._pendingWriteTimeout;
return Promise.all(promises);
}
});
} else {
return Promise.resolve();
return fs.ensureDir(self.storageBaseDir);
}
}
LocalFileSystem.prototype.close = function(){
if (this.cache && this._flushPendingWrites) {
clearTimeout(this._pendingWriteTimeout);
delete this._pendingWriteTimeout;
return this._flushPendingWrites();
}
return Promise.resolve();
}
@@ -188,13 +240,17 @@ LocalFileSystem.prototype.get = function(scope, key, callback) {
};
LocalFileSystem.prototype.set = function(scope, key, value, callback) {
var self = this;
var storagePath = getStoragePath(this.storageBaseDir ,scope);
if (this.cache) {
this.cache.set(scope,key,value,callback);
// With cache enabled, no need to re-read the file prior to writing.
var newContext = this.cache._export()[scope];
fs.outputFile(storagePath + ".json", JSON.stringify(newContext, undefined, 4), "utf8").catch(function(err) {
});
this.pendingWrites[scope] = true;
if (this._pendingWriteTimeout) {
// there's a pending write which will handle this
return;
} else {
this._pendingWriteTimeout = setTimeout(function() { self._flushPendingWrites.call(self)}, this.flushInterval);
}
} else if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
} else {
@@ -214,7 +270,14 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) {
}
util.setObjectProperty(obj,key[i],v);
}
return fs.outputFile(storagePath + ".json", JSON.stringify(obj, undefined, 4), "utf8");
var stringifiedContext = stringify(obj);
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
log.warn("Context "+scope+" contains a circular reference that cannot be persisted");
self.knownCircularRefs[scope] = true;
} else {
delete self.knownCircularRefs[scope];
}
return fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8");
}).then(function(){
if(typeof callback === "function"){
callback(null);
@@ -254,36 +317,45 @@ LocalFileSystem.prototype.delete = function(scope){
cachePromise = Promise.resolve();
}
var that = this;
delete this.pendingWrites[scope];
return cachePromise.then(function() {
var storagePath = getStoragePath(that.storageBaseDir,scope);
return fs.remove(storagePath + ".json");
});
}
LocalFileSystem.prototype.clean = function(activeNodes){
LocalFileSystem.prototype.clean = function(_activeNodes) {
var activeNodes = {};
_activeNodes.forEach(function(node) { activeNodes[node] = true });
var self = this;
var cachePromise;
if (this.cache) {
cachePromise = this.cache.clean(activeNodes);
cachePromise = this.cache.clean(_activeNodes);
} else {
cachePromise = Promise.resolve();
}
return cachePromise.then(function() {
return fs.readdir(self.storageBaseDir).then(function(dirs){
return Promise.all(dirs.reduce(function(result, item){
if(item !== "global" && activeNodes.indexOf(item) === -1){
result.push(fs.remove(path.join(self.storageBaseDir,item)));
}
return result;
},[]));
}).catch(function(err){
if(err.code == 'ENOENT') {
return Promise.resolve();
}else{
return Promise.reject(err);
this.knownCircularRefs = {};
return cachePromise.then(() => listFiles(self.storageBaseDir)).then(function(files) {
var promises = [];
files.forEach(function(file) {
var parts = file.split(path.sep);
var removePromise;
if (parts[0] === 'global') {
// never clean global
return;
} else if (!activeNodes[parts[0]]) {
// Flow removed - remove the whole dir
removePromise = fs.remove(path.join(self.storageBaseDir,parts[0]));
} else if (parts[1] !== 'flow.json' && !activeNodes[parts[1].substring(0,parts[1].length-5)]) {
// Node removed - remove the context file
removePromise = fs.remove(path.join(self.storageBaseDir,file));
}
if (removePromise) {
promises.push(removePromise);
}
});
});
return Promise.all(promises)
})
}
module.exports = function(config){