From 55e66ebcacd4cb7daf8e2701afd0a3e2e5bc9cc0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 2 Mar 2016 23:34:24 +0000 Subject: [PATCH] Allow node modules to include example flows --- editor/js/main.js | 2 + editor/js/ui/library.js | 16 ++- red/api/library.js | 126 ++++++++++++++++-- red/api/locales/en-US/editor.json | 1 + red/runtime/nodes/registry/installer.js | 4 +- red/runtime/nodes/registry/localfilesystem.js | 6 + test/red/api/library_spec.js | 4 + 7 files changed, 142 insertions(+), 17 deletions(-) diff --git a/editor/js/main.js b/editor/js/main.js index 052edaadc..64d71bb8c 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -142,6 +142,8 @@ var RED = (function() { RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success"); } } + // Refresh flow library to ensure any examples are updated + RED.library.loadFlowLibrary(); }); } }); diff --git a/editor/js/ui/library.js b/editor/js/ui/library.js index 102271dcc..d98455ba5 100644 --- a/editor/js/ui/library.js +++ b/editor/js/ui/library.js @@ -1,5 +1,5 @@ /** - * Copyright 2013, 2015 IBM Corp. + * Copyright 2013, 2016 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,9 @@ RED.library = (function() { var li; var a; var ul = document.createElement("ul"); - ul.id = "menu-item-import-library-submenu"; + if (root === "") { + ul.id = "menu-item-import-library-submenu"; + } ul.className = "dropdown-menu"; if (data.d) { for (i in data.d) { @@ -63,7 +65,17 @@ RED.library = (function() { } return ul; }; + var examples; + if (data.d && data.d._examples_) { + examples = data.d._examples_; + delete data.d._examples_; + } var menu = buildMenu(data,""); + $("#menu-item-import-examples").remove(); + if (examples) { + RED.menu.addItem("menu-item-import",{id:"menu-item-import-examples",label:RED._("menu.label.examples"),options:[]}) + $("#menu-item-import-examples-submenu").replaceWith(buildMenu(examples,"_examples_")); + } //TODO: need an api in RED.menu for this $("#menu-item-import-library-submenu").replaceWith(menu); }); diff --git a/red/api/library.js b/red/api/library.js index a73c1dd47..9908c75ac 100644 --- a/red/api/library.js +++ b/red/api/library.js @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +var fs = require('fs'); +var fspath = require('path'); +var when = require('when'); var redApp = null; var storage; @@ -68,40 +71,135 @@ function createLibrary(type) { }); } } + +var exampleRoots = {}; +var exampleFlows = {d:{}}; +var exampleCount = 0; + +function getFlowsFromPath(path) { + return when.promise(function(resolve,reject) { + var result = {}; + fs.readdir(path,function(err,files) { + var promises = []; + var validFiles = []; + files.forEach(function(file) { + var fullPath = fspath.join(path,file); + var stats = fs.lstatSync(fullPath); + if (stats.isDirectory()) { + validFiles.push(file); + promises.push(getFlowsFromPath(fullPath)); + } else if (/\.json$/.test(file)){ + validFiles.push(file); + exampleCount++; + promises.push(when.resolve(file.split(".")[0])) + } + }) + var i=0; + when.all(promises).then(function(results) { + results.forEach(function(r) { + if (typeof r === 'string') { + result.f = result.f||[]; + result.f.push(r); + } else { + result.d = result.d||{}; + result.d[validFiles[i]] = r; + } + i++; + }) + + resolve(result); + }) + }); + }) +} + +function addNodeExamplesDir(module) { + exampleRoots[module.name] = module.path; + getFlowsFromPath(module.path).then(function(result) { + exampleFlows.d[module.name] = result; + }); +} +function removeNodeExamplesDir(module) { + delete exampleRoots[module]; + delete exampleFlows.d[module]; +} + module.exports = { init: function(app,runtime) { redApp = app; log = runtime.log; storage = runtime.storage; + // TODO: this allows init to be called multiple times without + // registering multiple instances of the listener. + // It isn't.... ideal. + runtime.events.removeListener("node-examples-dir",addNodeExamplesDir); + runtime.events.on("node-examples-dir",addNodeExamplesDir); + runtime.events.removeListener("node-module-uninstalled",removeNodeExamplesDir); + runtime.events.on("node-module-uninstalled",removeNodeExamplesDir); + }, register: createLibrary, getAll: function(req,res) { storage.getAllFlows().then(function(flows) { log.audit({event: "library.get.all",type:"flow"},req); + if (exampleCount > 0) { + flows.d = flows.d||{}; + flows.d._examples_ = exampleFlows; + } res.json(flows); }); }, get: function(req,res) { - storage.getFlow(req.params[0]).then(function(data) { - // data is already a JSON string - log.audit({event: "library.get",type:"flow",path:req.params[0]},req); - res.set('Content-Type', 'application/json'); - res.send(data); - }).otherwise(function(err) { - if (err) { - log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req); - res.status(403).end(); - return; + if (req.params[0].indexOf("_examples_/") === 0) { + var m = /^_examples_\/([^\/]+)\/(.*)$/.exec(req.params[0]); + if (m) { + var module = m[1]; + var path = m[2]+".json"; + if (exampleRoots[module]) { + var fullPath = fspath.join(exampleRoots[module],path); + try { + fs.statSync(fullPath); + log.audit({event: "library.get",type:"flow",path:req.params[0]},req); + return res.sendFile(fullPath,{ + headers:{ + 'Content-Type': 'application/json' + } + }) + } catch(err) { + console.log(err); + } } } + // IF we get here, we didn't find the file log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); - res.status(404).end(); - }); + return res.status(404).end(); + } else { + storage.getFlow(req.params[0]).then(function(data) { + // data is already a JSON string + log.audit({event: "library.get",type:"flow",path:req.params[0]},req); + res.set('Content-Type', 'application/json'); + res.send(data); + }).otherwise(function(err) { + if (err) { + log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()})); + if (err.code === 'forbidden') { + log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req); + res.status(403).end(); + return; + } + } + log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); + res.status(404).end(); + }); + } }, post: function(req,res) { + // if (req.params[0].indexOf("_examples_/") === 0) { + // log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"})); + // log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); + // return res.status(403).send({error:"unexpected_error", message:"forbidden"}); + // } var flow = JSON.stringify(req.body); storage.saveFlow(req.params[0],flow).then(function() { log.audit({event: "library.set",type:"flow",path:req.params[0]},req); diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index beb30e668..10fbf478b 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -31,6 +31,7 @@ "export": "Export", "clipboard": "Clipboard", "library": "Library", + "examples": "Examples", "subflows": "Subflows", "createSubflow": "Create Subflow", "selectionToSubflow": "Selection to Subflow", diff --git a/red/runtime/nodes/registry/installer.js b/red/runtime/nodes/registry/installer.js index 942f5cd69..621660b02 100644 --- a/red/runtime/nodes/registry/installer.js +++ b/red/runtime/nodes/registry/installer.js @@ -1,5 +1,5 @@ /** - * Copyright 2015 IBM Corp. + * Copyright 2015, 2016 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,6 +174,8 @@ function uninstallModule(module) { } else { log.info(log._("server.install.uninstalled",{name:module})); reportRemovedModules(list); + // TODO: tidy up internal event names + events.emit("node-module-uninstalled",module) resolve(list); } } diff --git a/red/runtime/nodes/registry/localfilesystem.js b/red/runtime/nodes/registry/localfilesystem.js index 503e1ea66..5793e5220 100644 --- a/red/runtime/nodes/registry/localfilesystem.js +++ b/red/runtime/nodes/registry/localfilesystem.js @@ -184,6 +184,12 @@ function getModuleNodeFiles(module) { } } } + var examplesDir = path.join(moduleDir,"examples"); + try { + fs.statSync(examplesDir) + events.emit("node-examples-dir",{name:pkg.name,path:examplesDir}); + } catch(err) { + } return results; } diff --git a/test/red/api/library_spec.js b/test/red/api/library_spec.js index 50713a58e..881fd661d 100644 --- a/test/red/api/library_spec.js +++ b/test/red/api/library_spec.js @@ -80,6 +80,10 @@ describe("library api", function() { libraryEntries[type][path] = body; return when.resolve(); } + }, + events: { + on: function(){}, + removeListener: function(){} } }); }