diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js index 02dc58c58..ad505a46e 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js @@ -122,6 +122,7 @@ module.exports = { } if (req.body.active) { + opts.clearContext = req.body.hasOwnProperty('clearContext')?req.body.clearContext:true runtimeAPI.projects.setActiveProject(opts).then(function() { listProjects(req,res); }).catch(function(err) { diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 0c4edb87b..1ab148217 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -1095,7 +1095,8 @@ "not-git": "Not a git repository", "no-resource": "Repository not found", "cant-get-ssh-key-path": "Error! Can't get selected SSH key path.", - "unexpected_error": "unexpected_error" + "unexpected_error": "unexpected_error", + "clearContext": "Clear context when switching projects" }, "delete": { "confirm": "Are you sure you want to delete this project?" diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js index c9143ae1c..190561e15 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js @@ -1212,6 +1212,9 @@ RED.projects = (function() { } }).appendTo(row); + row = $('
').hide().appendTo(container); + $(' ').appendTo(row).i18n(); + row = $('
').appendTo(container); $('').appendTo(row); @@ -1501,7 +1504,8 @@ RED.projects = (function() { }; } } else if (projectType === 'open') { - return switchProject(selectedProject.name,function(err,data) { + var clearContext = $("#red-ui-projects-dialog-screen-clear-context").prop("checked") + return switchProject(selectedProject.name, clearContext, function(err,data) { if (err) { if (err.code !== 'credentials_load_failed') { console.log(RED._("projects.create.unexpected_error"),err) @@ -1595,7 +1599,7 @@ RED.projects = (function() { } } - function switchProject(name,done) { + function switchProject(name,clearContext,done) { RED.deploy.setDeployInflight(true); RED.projects.settings.switchProject(name); sendRequest({ @@ -1614,7 +1618,7 @@ RED.projects = (function() { '*': done }, } - },{active:true}).then(function() { + },{active:true, clearContext:clearContext}).then(function() { dialog.dialog( "close" ); RED.events.emit("project:change", {name:name}); }).always(function() { @@ -1687,7 +1691,7 @@ RED.projects = (function() { dialogHeight = 590 - (750 - winHeight); } $(".red-ui-projects-dialog-box").height(dialogHeight); - $(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 180); + $(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 210); dialog.dialog('option','title',screen.title||""); dialog.dialog("open"); } diff --git a/packages/node_modules/@node-red/runtime/lib/api/projects.js b/packages/node_modules/@node-red/runtime/lib/api/projects.js index 14d1d0ec1..aef736637 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/projects.js +++ b/packages/node_modules/@node-red/runtime/lib/api/projects.js @@ -99,6 +99,7 @@ var api = module.exports = { * @param {Object} opts * @param {User} opts.user - the user calling the api * @param {String} opts.id - the id of the project to activate + * @param {boolean} opts.clearContext - whether to clear context * @param {Object} opts.req - the request to log (optional) * @return {Promise} - resolves when complete * @memberof @node-red/runtime_projects @@ -107,7 +108,7 @@ var api = module.exports = { var currentProject = runtime.storage.projects.getActiveProject(opts.user); runtime.log.audit({event: "projects.set",id:opts.id}, opts.req); if (!currentProject || opts.id !== currentProject.name) { - return runtime.storage.projects.setActiveProject(opts.user, opts.id); + return runtime.storage.projects.setActiveProject(opts.user, opts.id, opts.clearContext); } }, diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js index 967fae295..aff44db04 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js @@ -14,30 +14,30 @@ * limitations under the License. **/ -var clone = require("clone"); -var log = require("@node-red/util").log; -var util = require("@node-red/util").util; -var memory = require("./memory"); +const clone = require("clone"); +const log = require("@node-red/util").log; +const util = require("@node-red/util").util; +const memory = require("./memory"); -var settings; +let settings; // A map of scope id to context instance -var contexts = {}; +let contexts = {}; // A map of store name to instance -var stores = {}; -var storeList = []; -var defaultStore; +let stores = {}; +let storeList = []; +let defaultStore; // Whether there context storage has been configured or left as default -var hasConfiguredStore = false; +let hasConfiguredStore = false; // Unknown Stores -var unknownStores = {}; +let unknownStores = {}; function logUnknownStore(name) { if (name) { - var count = unknownStores[name] || 0; + let count = unknownStores[name] || 0; if (count == 0) { log.warn(log._("context.unknown-store", {name: name})); count++; @@ -52,8 +52,8 @@ function init(_settings) { stores = {}; storeList = []; hasConfiguredStore = false; - var seed = settings.functionGlobalContext || {}; - contexts['global'] = createContext("global",seed); + initialiseGlobalContext(); + // create a default memory store - used by the unit tests that skip the full // `load()` initialisation sequence. // If the user has any stores configured, this will be disgarded @@ -61,6 +61,11 @@ function init(_settings) { defaultStore = "memory"; } +function initialiseGlobalContext() { + const seed = settings.functionGlobalContext || {}; + contexts['global'] = createContext("global",seed); +} + function load() { return new Promise(function(resolve,reject) { // load & init plugins in settings.contextStorage @@ -233,12 +238,15 @@ function validateContextKey(key) { function createContext(id,seed,parent) { // Seed is only set for global context - sourced from functionGlobalContext - var scope = id; - var obj = seed || {}; - var seedKeys; - var insertSeedValues; + const scope = id; + const obj = {}; + let seedKeys; + let insertSeedValues; if (seed) { seedKeys = Object.keys(seed); + seedKeys.forEach(key => { + obj[key] = seed[key]; + }) insertSeedValues = function(keys,values) { if (!Array.isArray(keys)) { if (values[0] === undefined) { @@ -540,8 +548,28 @@ function getContext(nodeId, flowId) { return newContext; } +/** + * Delete the context of the given node/flow/global + * + * If the user has configured a context store, this + * will no-op a request to delete node/flow context. + */ function deleteContext(id,flowId) { - if(!hasConfiguredStore){ + if (id === "global") { + // 1. delete global from all configured stores + var promises = []; + for(var plugin in stores){ + if(stores.hasOwnProperty(plugin)){ + promises.push(stores[plugin].delete('global')); + } + } + return Promise.all(promises).then(function() { + // 2. delete global context + delete contexts['global']; + // 3. reinitialise global context + initialiseGlobalContext(); + }) + } else if (!hasConfiguredStore) { // only delete context if there's no configured storage. var contextId = id; if (flowId) { @@ -549,12 +577,19 @@ function deleteContext(id,flowId) { } delete contexts[contextId]; return stores["_"].delete(contextId); - }else{ + } else { return Promise.resolve(); } } +/** + * Delete any contexts that are no longer in use + * @param flowConfig object includes allNodes as object of id->node + * + * If flowConfig is undefined, all flow/node contexts will be removed + **/ function clean(flowConfig) { + flowConfig = flowConfig || { allNodes: {} }; var promises = []; for(var plugin in stores){ if(stores.hasOwnProperty(plugin)){ @@ -572,6 +607,16 @@ function clean(flowConfig) { return Promise.all(promises); } +/** + * Deletes all contexts, including global and reinitialises global to + * initial state. + */ +function clear() { + return clean().then(function() { + return deleteContext('global') + }) +} + function close() { var promises = []; for(var plugin in stores){ @@ -594,5 +639,6 @@ module.exports = { getFlowContext:getFlowContext, delete: deleteContext, clean: clean, + clear: clear, close: close }; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index b2346176f..5b859a5f8 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -206,6 +206,7 @@ module.exports = { eachNode: flows.eachNode, getContext: context.get, + clearContext: context.clear, installerEnabled: registry.installerEnabled, installModule: installModule, diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index b98900f49..b2b8c7052 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -377,8 +377,17 @@ function getActiveProject(user) { return activeProject; } -function reloadActiveProject(action) { +function reloadActiveProject(action, clearContext) { + // Stop the current flows return runtime.nodes.stopFlows().then(function() { + if (clearContext) { + // Reset context to remove any old values + return runtime.nodes.clearContext() + } else { + return Promise.resolve() + } + }).then(function() { + // Load the new project flows and start them return runtime.nodes.loadFlows(true).then(function() { events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}}); }).catch(function(err) { @@ -387,6 +396,9 @@ function reloadActiveProject(action) { events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}}); throw err; }); + }).catch(function(err) { + console.log(err.stack); + throw err; }); } function createProject(user, metadata) { @@ -424,7 +436,7 @@ function createProject(user, metadata) { return getProject(user, metadata.name); }) } -function setActiveProject(user, projectName) { +function setActiveProject(user, projectName, clearContext) { return loadProject(projectName).then(function(project) { var globalProjectSettings = settings.get("projects")||{}; globalProjectSettings.activeProject = project.name; @@ -434,7 +446,7 @@ function setActiveProject(user, projectName) { // console.log("Updated file targets to"); // console.log(flowsFullPath) // console.log(credentialsFile) - return reloadActiveProject("loaded"); + return reloadActiveProject("loaded", clearContext); }) }); } diff --git a/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js b/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js index 7e05eba8f..591504e55 100644 --- a/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js @@ -156,6 +156,22 @@ describe('context', function() { }); }); + it('deletes global context',function() { + Context.init({functionGlobalContext: {foo:"bar"}}); + return Context.load().then(function(){ + var globalContextA = Context.get("global") + + globalContextA.get('foo').should.eql('bar') + globalContextA.set("another","value"); + + return Context.delete("global").then(function(){ + var globalContextB = Context.get("global") + globalContextB.get('foo').should.eql('bar') + should.not.exist(globalContextB.get("another")); + }); + }); + }); + it('enumerates context keys - sync', function() { var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); @@ -316,6 +332,31 @@ describe('context', function() { }); }); + + describe("clear", function() { + it('clears all context',function() { + Context.init({functionGlobalContext: {foo:"bar"}}); + return Context.load().then(function(){ + var globalContextA = Context.get("global") + globalContextA.get('foo').should.eql('bar') + globalContextA.set("another","value"); + + var flowContextA = Context.getFlowContext("flowA") + flowContextA.set("foo","abc"); + flowContextA.get("foo").should.equal("abc"); + + return Context.clear().then(function(){ + var globalContextB = Context.getFlowContext("global") + globalContextB.get('foo').should.eql('bar') + should.not.exist(globalContextB.get("another")); + + flowContextA = Context.getFlowContext("flowA") + should.not.exist(flowContextA.get("foo")) + + }); + }); + }); + }) }); describe('external context storage',function() {