diff --git a/Gruntfile.js b/Gruntfile.js index 4056474db..472d70c2d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -153,6 +153,7 @@ module.exports = function(grunt) { "editor/js/ui/typeSearch.js", "editor/js/ui/subflow.js", "editor/js/ui/userSettings.js", + "editor/js/ui/projects.js", "editor/js/ui/touch/radialMenu.js" ], dest: "public/red/red.js" diff --git a/editor/js/history.js b/editor/js/history.js index 98c2ca918..ac155bd88 100644 --- a/editor/js/history.js +++ b/editor/js/history.js @@ -321,6 +321,9 @@ RED.history = (function() { }, peek: function() { return undo_history[undo_history.length-1]; + }, + clear: function() { + undo_history = []; } } diff --git a/editor/js/keymap.json b/editor/js/keymap.json index b60372b42..5fa045ab6 100644 --- a/editor/js/keymap.json +++ b/editor/js/keymap.json @@ -13,7 +13,11 @@ "ctrl-e": "core:show-export-dialog", "ctrl-i": "core:show-import-dialog", "ctrl-space": "core:toggle-sidebar", - "ctrl-,": "core:show-user-settings" + "ctrl-,": "core:show-user-settings", + + "ctrl-alt-n": "core:new-project", + "ctrl-alt-o": "core:open-project", + "ctrl-g p": "core:show-projects-tab" }, "workspace": { "backspace": "core:delete-selection", diff --git a/editor/js/main.js b/editor/js/main.js index f4f42b90d..1ca25546a 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -42,12 +42,127 @@ $("#palette > .palette-spinner").hide(); $(".palette-scroll").removeClass("hide"); $("#palette-search").removeClass("hide"); - loadFlows(); + loadFlows(function() { + RED.projects.refreshSidebar(); + + var persistentNotifications = {}; + RED.comms.subscribe("notification/#",function(topic,msg) { + var parts = topic.split("/"); + var notificationId = parts[1]; + if (notificationId === "runtime-deploy") { + // handled in ui/deploy.js + return; + } + if (notificationId === "node") { + // handled below + return; + } + if (notificationId === "project-change") { + RED.nodes.clear(); + RED.history.clear(); + RED.projects.refreshSidebar(); + RED.projects.showSidebar(); + loadFlows(function() { + RED.notify("NLS: Project changed to "+msg.project); + }); + return; + } + if (msg.text) { + var text = RED._(msg.text,{default:msg.text}); + if (notificationId === "runtime-state") { + if (msg.error === "credentials_load_failed") { + text += '

'+'Setup credentials'+'

'; + } + } + + + if (!persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout); + } else { + persistentNotifications[notificationId].update(text,msg.timeout); + } + } else if (persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId].close(); + delete persistentNotifications[notificationId]; + } + }); + RED.comms.subscribe("status/#",function(topic,msg) { + var parts = topic.split("/"); + var node = RED.nodes.node(parts[1]); + if (node) { + if (msg.hasOwnProperty("text")) { + if (msg.text[0] !== ".") { + msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()}); + } + } + node.status = msg; + node.dirty = true; + RED.view.redraw(); + } + }); + RED.comms.subscribe("notification/node/#",function(topic,msg) { + var i,m; + var typeList; + var info; + if (topic == "notification/node/added") { + var addedTypes = []; + msg.forEach(function(m) { + var id = m.id; + RED.nodes.addNodeSet(m); + addedTypes = addedTypes.concat(m.types); + RED.i18n.loadCatalog(id, function() { + $.get('nodes/'+id, function(data) { + $("body").append(data); + }); + }); + }); + if (addedTypes.length) { + typeList = ""; + RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success"); + } + } else if (topic == "notification/node/removed") { + for (i=0;i
  • ")+"
  • "; + RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success"); + } + } + } else if (topic == "notification/node/enabled") { + if (msg.types) { + info = RED.nodes.getNodeSet(msg.id); + if (info.added) { + RED.nodes.enableNodeSet(msg.id); + typeList = ""; + RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success"); + } else { + $.get('nodes/'+msg.id, function(data) { + $("body").append(data); + typeList = ""; + RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); + }); + } + } + } else if (topic == "notification/node/disabled") { + if (msg.types) { + RED.nodes.disableNodeSet(msg.id); + typeList = ""; + RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success"); + } + } else if (topic == "node/upgraded") { + RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success"); + RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version); + } + // Refresh flow library to ensure any examples are updated + RED.library.loadFlowLibrary(); + }); + }); } }); } - function loadFlows() { + function loadFlows(done) { $.ajax({ headers: { "Accept":"application/json", @@ -55,110 +170,17 @@ cache: false, url: 'flows', success: function(nodes) { - var currentHash = window.location.hash; - RED.nodes.version(nodes.rev); - RED.nodes.import(nodes.flows); - RED.nodes.dirty(false); - RED.view.redraw(true); - if (/^#flow\/.+$/.test(currentHash)) { - RED.workspaces.show(currentHash.substring(6)); + if (nodes) { + var currentHash = window.location.hash; + RED.nodes.version(nodes.rev); + RED.nodes.import(nodes.flows); + RED.nodes.dirty(false); + RED.view.redraw(true); + if (/^#flow\/.+$/.test(currentHash)) { + RED.workspaces.show(currentHash.substring(6)); + } } - - var persistentNotifications = {}; - RED.comms.subscribe("notification/#",function(topic,msg) { - var parts = topic.split("/"); - var notificationId = parts[1]; - if (notificationId === "runtime-deploy") { - // handled in ui/deploy.js - return; - } - if (notificationId === "node") { - // handled below - return; - } - if (msg.text) { - var text = RED._(msg.text,{default:msg.text}); - if (!persistentNotifications.hasOwnProperty(notificationId)) { - persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout); - } else { - persistentNotifications[notificationId].update(text,msg.timeout); - } - } else if (persistentNotifications.hasOwnProperty(notificationId)) { - persistentNotifications[notificationId].close(); - delete persistentNotifications[notificationId]; - } - }); - RED.comms.subscribe("status/#",function(topic,msg) { - var parts = topic.split("/"); - var node = RED.nodes.node(parts[1]); - if (node) { - if (msg.hasOwnProperty("text")) { - if (msg.text[0] !== ".") { - msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()}); - } - } - node.status = msg; - node.dirty = true; - RED.view.redraw(); - } - }); - RED.comms.subscribe("notification/node/#",function(topic,msg) { - var i,m; - var typeList; - var info; - if (topic == "notification/node/added") { - var addedTypes = []; - msg.forEach(function(m) { - var id = m.id; - RED.nodes.addNodeSet(m); - addedTypes = addedTypes.concat(m.types); - RED.i18n.loadCatalog(id, function() { - $.get('nodes/'+id, function(data) { - $("body").append(data); - }); - }); - }); - if (addedTypes.length) { - typeList = ""; - RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success"); - } - } else if (topic == "notification/node/removed") { - for (i=0;i
  • ")+"
  • "; - RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success"); - } - } - } else if (topic == "notification/node/enabled") { - if (msg.types) { - info = RED.nodes.getNodeSet(msg.id); - if (info.added) { - RED.nodes.enableNodeSet(msg.id); - typeList = ""; - RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success"); - } else { - $.get('nodes/'+msg.id, function(data) { - $("body").append(data); - typeList = ""; - RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); - }); - } - } - } else if (topic == "notification/node/disabled") { - if (msg.types) { - RED.nodes.disableNodeSet(msg.id); - typeList = ""; - RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success"); - } - } else if (topic == "node/upgraded") { - RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success"); - RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version); - } - // Refresh flow library to ensure any examples are updated - RED.library.loadFlowLibrary(); - }); + done(); } }); } @@ -176,6 +198,13 @@ function loadEditor() { var menuOptions = []; + + menuOptions.push({id:"menu-item-projects-menu",label:"NLS: Projects",options:[ + {id:"menu-item-projects-new",label:"New...",disabled:false,onselect:"core:new-project"}, + {id:"menu-item-projects-open",label:"Open...",disabled:false,onselect:"core:open-project"}, + ]}); + + menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ // {id:"menu-item-view-show-grid",setting:"view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:"core:toggle-show-grid"}, // {id:"menu-item-view-snap-grid",setting:"view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:"core:toggle-snap-grid"}, @@ -241,6 +270,7 @@ } RED.sidebar.init(); + RED.projects.init(); RED.subflow.init(); RED.workspaces.init(); RED.clipboard.init(); diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 2cd2f62fc..6398f20b6 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -1201,11 +1201,12 @@ RED.nodes = (function() { }); defaultWorkspace = null; - RED.nodes.dirty(true); + RED.nodes.dirty(false); RED.view.redraw(true); RED.palette.refresh(); RED.workspaces.refresh(); RED.sidebar.config.refresh(); + RED.sidebar.info.refresh(); // var node_defs = {}; // var nodes = []; diff --git a/editor/js/ui/common/stack.js b/editor/js/ui/common/stack.js index 6370649a9..b35131634 100644 --- a/editor/js/ui/common/stack.js +++ b/editor/js/ui/common/stack.js @@ -17,11 +17,25 @@ RED.stack = (function() { function createStack(options) { var container = options.container; - + container.addClass("red-ui-stack"); + var contentHeight = 0; var entries = []; var visible = true; - + var resizeStack = function() { + if (entries.length > 0) { + var headerHeight = entries[0].header.outerHeight(); + var height = container.innerHeight(); + contentHeight = height - entries.length*headerHeight - (entries.length-1); + entries.forEach(function(e) { + e.contentWrap.height(contentHeight); + }); + } + } + if (options.fill && options.singleExpanded) { + $(window).resize(resizeStack); + $(window).focus(resizeStack); + } return { add: function(entry) { entries.push(entry); @@ -30,7 +44,12 @@ RED.stack = (function() { entry.container.hide(); } var header = $('
    ').appendTo(entry.container); - entry.content = $('
    ').appendTo(entry.container); + entry.header = header; + entry.contentWrap = $('
    ',{style:"position:relative"}).appendTo(entry.container); + if (options.fill) { + entry.contentWrap.css("height",contentHeight); + } + entry.content = $('
    ').appendTo(entry.contentWrap); if (entry.collapsible !== false) { header.click(function() { if (options.singleExpanded) { @@ -49,11 +68,13 @@ RED.stack = (function() { var icon = $('').appendTo(header); if (entry.expanded) { + entry.container.addClass("palette-category-expanded"); icon.addClass("expanded"); } else { - entry.content.hide(); + entry.contentWrap.hide(); } } else { + $('').appendTo(header); header.css("cursor","default"); } entry.title = $('').html(entry.title).appendTo(header); @@ -75,23 +96,26 @@ RED.stack = (function() { entry.onexpand.call(entry); } icon.addClass("expanded"); - entry.content.slideDown(200); + entry.container.addClass("palette-category-expanded"); + entry.contentWrap.slideDown(200); return true; } }; entry.collapse = function() { if (entry.isExpanded()) { icon.removeClass("expanded"); - entry.content.slideUp(200); + entry.container.removeClass("palette-category-expanded"); + entry.contentWrap.slideUp(200); return true; } }; entry.isExpanded = function() { - return icon.hasClass("expanded"); + return entry.container.hasClass("palette-category-expanded"); }; - + if (options.fill && options.singleExpanded) { + resizeStack(); + } return entry; - }, hide: function() { @@ -108,9 +132,13 @@ RED.stack = (function() { entry.container.show(); }); return this; + }, + resize: function() { + if (resizeStack) { + resizeStack(); + } } } - } return { diff --git a/editor/js/ui/deploy.js b/editor/js/ui/deploy.js index b2589342f..c22fa55c2 100644 --- a/editor/js/ui/deploy.js +++ b/editor/js/ui/deploy.js @@ -437,6 +437,10 @@ RED.deploy = (function() { } } return { - init: init + init: init, + setDeployInflight: function(state) { + deployInflight = state; + } + } })(); diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index ceea0beff..bb58caba9 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -496,8 +496,12 @@ RED.editor = (function() { label = RED._("expressionEditor.title"); } else if (node.type === '_json') { label = RED._("jsonEditor.title"); + } else if (node.type === '_markdown') { + label = RED._("markdownEditor.title"); } else if (node.type === '_buffer') { label = RED._("bufferEditor.title"); + } else if (node.type === '_project') { + label = "NLS: Edit project settings"; } else if (node.type === 'subflow') { label = RED._("subflow.editSubflow",{name:node.name}) } else if (node.type.indexOf("subflow:")===0) { @@ -1979,7 +1983,7 @@ RED.editor = (function() { var expressionEditor; var trayOptions = { - title: getEditStackTitle(), + title: options.title || getEditStackTitle(), buttons: [ { id: "node-dialog-cancel", @@ -2044,6 +2048,139 @@ RED.editor = (function() { RED.tray.show(trayOptions); } + function editMarkdown(options) { + var value = options.value; + var onComplete = options.complete; + var type = "_markdown" + editStack.push({type:type}); + RED.view.state(RED.state.EDITING); + var expressionEditor; + + var trayOptions = { + title: options.title || getEditStackTitle(), + buttons: [ + { + id: "node-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + RED.tray.close(); + } + }, + { + id: "node-dialog-ok", + text: RED._("common.label.done"), + class: "primary", + click: function() { + onComplete(expressionEditor.getValue()); + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + 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;idiv:not(.node-text-editor-row)"); + // var editorRow = $("#dialog-form>div.node-text-editor-row"); + // var height = $("#dialog-form").height(); + // for (var i=0;i'+RED._("sidebar.info.none")+''); } else { - helpText = $("script[data-help-name='"+d.type+"']").html()||""; + helpText = $("script[data-help-name='"+d.type+"']").html()||(''+RED._("sidebar.info.none")+''); } - RED.sidebar.info.set(helpText); + RED.sidebar.info.set(helpText,RED._("sidebar.info.nodeHelp")); }); var chart = $("#chart"); var chartOffset = chart.offset(); diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js new file mode 100644 index 000000000..bc53f84bc --- /dev/null +++ b/editor/js/ui/projects.js @@ -0,0 +1,998 @@ +/** + * 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.projects = (function() { + + var dialog; + var dialogBody; + + var activeProject; + + var screens = {}; + function initScreens() { + screens = { + 'welcome': { + content: function() { + var container = $('
    '); + var buttons = $('
    ').appendTo(container); + var createNew = $('').appendTo(buttons); + createNew.click(function(e) { + e.preventDefault(); + show('create'); + }); + var openExisting = $('').appendTo(buttons); + openExisting.click(function(e) { + e.preventDefault(); + show('open') + }); + return container; + }, + buttons: [ + ] + }, + 'create': (function() { + var projectNameInput; + var projectSummaryEditor; + var projectSecretInput; + var projectSecretSelect; + var copyProject; + var projectRepoInput; + + return { + title: "Create a new project", // TODO: NLS + content: function() { + var container = $('
    '); + var row; + + var validateForm = function() { + var projectName = projectNameInput.val(); + var valid = true; + if (!/^[a-zA-Z0-9\-_]+$/.test(projectName)) { + if (projectNameInputChanged) { + projectNameInput.addClass("input-error"); + } + valid = false; + } else { + projectNameInput.removeClass("input-error"); + } + var projectType = $(".projects-dialog-screen-create-type.selected").data('type'); + if (projectType === 'copy') { + if (!copyProject) { + valid = false; + } + } else if (projectType === 'clone') { + var repo = projectRepoInput.val(); + if (repo.trim() === '') { + // TODO: could do more url regex checking... + if (projectRepoChanged) { + projectRepoInput.addClass("input-error"); + } + valid = false; + } else { + projectRepoInput.removeClass("input-error"); + + } + } + + $("#projects-dialog-create").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); + } + + row = $('
    ').appendTo(container); + var createAsEmpty = $('').appendTo(row); + var createAsCopy = $('').appendTo(row); + var createAsClone = $('').appendTo(row); + row.find(".projects-dialog-screen-create-type").click(function(evt) { + evt.preventDefault(); + $(".projects-dialog-screen-create-type").removeClass('selected'); + $(this).addClass('selected'); + $(".projects-dialog-screen-create-row").hide(); + $(".projects-dialog-screen-create-row-"+$(this).data('type')).show(); + validateForm(); + }) + + + row = $('
    ').appendTo(container); + $('').appendTo(row); + + projectNameInput = $('').appendTo(row); + var projectNameInputChanged = false; + projectNameInput.on("change keyup paste",function() { validateForm(); }); + + // Empty Project + row = $('
    ').appendTo(container); + $('').appendTo(row); + projectSummaryEditor = $('').appendTo(row); + + // Copy Project + row = $('
    ').appendTo(container); + $('').appendTo(row); + var autoInsertedName = ""; + createProjectList({ + height: "250px", + small: true, + select: function(project) { + copyProject = project; + var projectName = projectNameInput.val(); + if (projectName === "" || projectName === autoInsertedName) { + autoInsertedName = project.name+"-copy"; + projectNameInput.val(autoInsertedName); + } + validateForm(); + } + }).appendTo(row); + + // Clone Project + row = $('
    ').appendTo(container); + $('').appendTo(row); + projectRepoInput = $('').appendTo(row); + var projectRepoChanged = false; + projectRepoInput.on("change keyup paste",function() { + var repo = $(this).val(); + var m = /\/([^/]+)\.git/.exec(repo); + if (m) { + var projectName = projectNameInput.val(); + if (projectName === "" || projectName === autoInsertedName) { + autoInsertedName = m[1]; + projectNameInput.val(autoInsertedName); + } + } + validateForm(); + }); + + // Secret - empty/clone + row = $('
    ').appendTo(container); + $('').appendTo(row); + projectSecretInput = $('').appendTo(row); + + return container; + }, + buttons: [ + { + // id: "clipboard-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + $( this ).dialog( "close" ); + } + }, + { + id: "projects-dialog-create", + text: "Create project", // TODO: nls + class: "primary disabled", + disabled: true, + click: function() { + var projectType = $(".projects-dialog-screen-create-type.selected").data('type'); + var projectData = { + name: projectNameInput.val(), + } + if (projectType === 'empty') { + projectData.summary = projectSummaryEditor.val(); + projectData.credentialSecret = projectSecretInput.val(); + } else if (projectType === 'copy') { + projectData.copy = copyProject.name; + } else if (projectType === 'clone') { + projectData.credentialSecret = projectSecretInput.val(); + projectData.remote = { + url: projectRepoInput.val() + } + } + + sendRequest({ + url: "projects", + type: "POST", + responses: { + 200: function(data) { + switchProject(projectData.name,function(err,data) { + if (err) { + console.log("unexpected_error",error) + } else { + dialog.dialog( "close" ); + } + }) + }, + 400: { + 'project_exists': function(error) { + console.log("already exists"); + }, + 'git_error': function(error) { + console.log("git error",error); + }, + 'git_auth_failed': function(error) { + // getRepoAuthDetails(req); + console.log("git auth error",error); + }, + 'unexpected_error': function(error) { + console.log("unexpected_error",error) + } + } + } + },projectData) + + + + // if (projectType === 'empty') { + // show('credentialSecret'); + // } else if (projectType === 'copy') { + // show('copy'); + // } else if (projectType === 'clone') { + // show('clone'); + // } + + // var projectName = projectNameInput.val().trim(); + // var projectRepoEnabled = projectRepoEnabledInput.prop('checked'); + // var projectRepo = projectRepoInput.val().trim(); + // if (projectName !== '') { + // var req = { + // name:projectName + // }; + // if (projectRepoEnabled && projectRepo !== '') { + // req.remote = projectRepo; + // } + // console.log(req); + // sendRequest({ + // url: "projects", + // type: "POST", + // responses: { + // 200: function(data) { + // console.log("Success!",data); + // }, + // 400: { + // 'project_exists': function(error) { + // console.log("already exists"); + // }, + // 'git_error': function(error) { + // console.log("git error",error); + // }, + // 'git_auth_failed': function(error) { + // // getRepoAuthDetails(req); + // console.log("git auth error",error); + // }, + // 'unexpected_error': function(error) { + // console.log("unexpected_error",error) + // } + // } + // } + // },req) + // } + + + // $( this ).dialog( "close" ); + } + } + ] + } + })(), + 'credentialSecret': { + content: function() { + // Provide new secret or reset credentials. + var container = $('
    '); + var row = $('
    ').appendTo(container); + // $('').appendTo(row); + $('').appendTo(row); + var projectSecret = $('').appendTo(row); + + return container; + }, + buttons: [ + { + // id: "clipboard-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + dialog.dialog( "close" ); + } + }, + { + // id: "clipboard-dialog-cancel", + text: "Update", + click: function() { + var done = function(err,data) { + if (err) { + console.log(err); + } + dialog.dialog( "close" ); + } + RED.deploy.setDeployInflight(true); + sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 200: function(data) { + done(null,data); + }, + 400: { + 'credentials_load_failed': function(error) { + done(error,null); + }, + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{credentialSecret:$('#projects-dialog-secret').val()}).always(function() { + RED.deploy.setDeployInflight(false); + }) + + + } + } + ] + }, + 'open': { + content: function() { + return createProjectList({ + canSelectActive: false, + dblclick: function() { + $("#projects-dialog-open").click(); + } + }) + }, + buttons: [ + { + // id: "clipboard-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + $( this ).dialog( "close" ); + } + }, + { + id: "projects-dialog-open", + text: "Open project", // TODO: nls + class: "primary disabled", + disabled: true, + click: function() { + switchProject(selectedProject.name,function(err,data) { + if (err) { + if (err.error === 'credentials_load_failed') { + dialog.dialog( "close" ); + } else { + console.log("unexpected_error",err) + } + } else { + dialog.dialog( "close" ); + } + }) + } + } + ] + } + } + } + + function switchProject(name,done) { + RED.deploy.setDeployInflight(true); + modulesInUse = {}; + sendRequest({ + url: "projects/"+name, + type: "PUT", + responses: { + 200: function(data) { + done(null,data); + }, + 400: { + 'credentials_load_failed': function(error) { + done(error,null); + }, + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{active:true}).always(function() { + RED.deploy.setDeployInflight(false); + }) + } + + function show(s,options) { + if (!dialog) { + RED.projects.init(); + } + var screen = screens[s]; + var container = screen.content(); + + dialogBody.empty(); + dialog.dialog('option','buttons',screen.buttons); + dialogBody.append(container); + dialog.dialog('option','title',screen.title||""); + dialog.dialog("open"); + dialog.dialog({position: { 'my': 'center', 'at': 'center', 'of': window }}); + } + + var selectedProject = null; + + function createProjectList(options) { + options = options||{}; + var height = options.height || "300px"; + selectedProject = null; + var container = $('
    ',{style:"min-height: "+height+"; height: "+height+";"}); + + var list = $('
      ',{class:"projects-dialog-project-list", style:"height:"+height}).appendTo(container).editableList({ + addButton: false, + scrollOnAdd: false, + addItem: function(row,index,entry) { + var header = $('
      ',{class:"projects-dialog-project-list-entry"}).appendTo(row); + $('').appendTo(header); + $('').text(entry.name).appendTo(header); + if (activeProject && activeProject.name === entry.name) { + header.addClass("projects-list-entry-current"); + $('current').appendTo(header); + if (options.canSelectActive === false) { + // active project cannot be selected; so skip the rest + return + } + } + header.addClass("selectable"); + row.click(function(evt) { + $('.projects-dialog-project-list-entry').removeClass('selected'); + header.addClass('selected'); + $("#projects-dialog-open").prop('disabled',false).removeClass('disabled ui-button-disabled ui-state-disabled'); + selectedProject = entry; + if (options.select) { + options.select(entry); + } + }) + if (options.dblclick) { + row.dblclick(function(evt) { + evt.preventDefault(); + options.dblclick(); + }) + } + } + }); + if (options.small) { + list.addClass("projects-dialog-project-list-small") + } + $.getJSON("projects", function(data) { + data.projects.forEach(function(project) { + list.editableList('addItem',{name:project}); + }); + }) + return container; + } + + function sendRequest(options,body) { + // dialogBody.hide(); + console.log(options.url); + var start = Date.now(); + $(".projects-dialog-spinner").show(); + $("#projects-dialog").parent().find(".ui-dialog-buttonset").children().css("visibility","hidden") + if (body) { + options.data = JSON.stringify(body); + options.contentType = "application/json; charset=utf-8"; + } + var resultCallback; + var resultCallbackArgs; + return $.ajax(options).done(function(data,textStatus,xhr) { + if (options.responses && options.responses[200]) { + resultCallback = options.responses[200]; + resultCallbackArgs = data; + } + }).fail(function(xhr,textStatus,err) { + if (options.responses && options.responses[xhr.status]) { + var responses = options.responses[xhr.status]; + if (typeof responses === 'function') { + resultCallback = responses; + resultCallbackArgs = {error:responses.statusText}; + return; + } else if (responses[xhr.responseJSON.error]) { + resultCallback = responses[xhr.responseJSON.error]; + resultCallbackArgs = xhr.responseJSON; + return; + } + } + console.log("Unhandled error response:"); + console.log(xhr); + console.log(textStatus); + console.log(err); + }).always(function() { + var delta = Date.now() - start; + delta = Math.max(0,1000-delta); + setTimeout(function() { + // dialogBody.show(); + $(".projects-dialog-spinner").hide(); + $("#projects-dialog").parent().find(".ui-dialog-buttonset").children().css("visibility","") + if (resultCallback) { + resultCallback(resultCallbackArgs) + } + },delta); + }); + } + + function init() { + dialog = $('
      ') + .appendTo("body") + .dialog({ + modal: true, + autoOpen: false, + width: 600, + resizable: false, + open: function(e) { + $(this).parent().find(".ui-dialog-titlebar-close").hide(); + // $("#header-shade").show(); + // $("#editor-shade").show(); + // $("#palette-shade").show(); + // $("#sidebar-shade").show(); + }, + close: function(e) { + // $("#header-shade").hide(); + // $("#editor-shade").hide(); + // $("#palette-shade").hide(); + // $("#sidebar-shade").hide(); + } + }); + dialogBody = dialog.find("form"); + + RED.actions.add("core:new-project",RED.projects.newProject); + RED.actions.add("core:open-project",RED.projects.selectProject); + + initScreens(); + initSidebar(); + } + + // function getRepoAuthDetails(req) { + // var container = $('
      '); + // + // var row = $('
      ').appendTo(container); + // $('').appendTo(row); + // var usernameInput = $('').appendTo(row); + // + // row = $('
      ').appendTo(container); + // $('').appendTo(row); + // var passwordInput = $('').appendTo(row); + // + // dialogBody.empty(); + // dialogBody.append(container); + // dialog.dialog('option','buttons',[ + // { + // // id: "clipboard-dialog-cancel", + // text: RED._("common.label.cancel"), + // click: function() { + // // $( this ).dialog( "close" ); + // } + // }, + // { + // id: "projects-dialog-create", + // text: "Create project", // TODO: nls + // class: "primary", + // // disabled: true, + // click: function() { + // var username = usernameInput.val(); + // var password = passwordInput.val(); + // + // req.remote = parts[1]+username+":"+password+"@"+parts[3]; + // sendRequest({ + // url: "projects", + // type: "POST", + // responses: { + // 200: function(data) { + // console.log("Success!",data); + // }, + // 400: { + // 'project_exists': function(error) { + // console.log("already exists"); + // }, + // 'git_error': function(error) { + // console.log("git error",error); + // }, + // 'git_auth_failed': function(error) { + // console.log("git auth error",error); + // }, + // 'unexpected_error': function(error) { + // console.log("unexpected_error",error) + // } + // } + // } + // },req) + // } + // } + // ]) + // } + + + var sidebarContent; + var sidebarSections; + var sidebarSectionsInfo; + var sidebarSectionsDesc; + var sidebarSectionsDeps; + var sidebarSectionsDepsList; + var sidebarSectionsSettings; + var modulesInUse = {}; + + function initSidebar() { + sidebarContent = $('
      ', {class:"sidebar-projects"}); + var infoStackContainer = $("
      ",{class:"sidebar-projects-stack-info"}).appendTo(sidebarContent); + var stackContainer = $("
      ",{class:"sidebar-projects-stack"}).appendTo(sidebarContent); + + RED.actions.add("core:show-projects-tab",showSidebar); + + + outerSections = RED.stack.create({ + container: infoStackContainer, + fill: true + }); + + var a = outerSections.add({ + title: "Project", + collapsible: false + }) + + sidebarSectionsInfo = $("
      ",{class:"node-help"}).appendTo(a.content); + + sidebarSections = RED.stack.create({ + container: stackContainer, + singleExpanded: true, + fill: true + }); + + sidebarSectionsDesc = sidebarSections.add({ + title: RED._("sidebar.project.description"), + expanded: true + }); + sidebarSectionsDesc.content.css({padding:"6px"}); + + var editDescription = $('').appendTo(sidebarSectionsDesc.header); + var editDescriptionFunc = function() { + RED.editor.editMarkdown({ + title: RED._('sidebar.project.editDescription'), + value: activeProject.description, + complete: function(v) { + var spinner = addSpinnerOverlay(sidebarSectionsDesc.content); + editDescription.addClass('disabled'); + var done = function(err,res) { + if (err) { + editDescriptionFunc(); + } + activeProject.description = v; + updateProjectDescription(); + } + sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + done(null,data); + }, + 400: { + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{description:v}).always(function() { + spinner.remove(); + editDescription.removeClass('disabled'); + }); + } + }); + } + + editDescription.click(function(evt) { + evt.preventDefault(); + if ($(this).hasClass('disabled')) { + return; + } + editDescriptionFunc(); + }); + + + + + sidebarSectionsDeps = sidebarSections.add({ + title:RED._("sidebar.project.dependencies") + }); + sidebarSectionsDeps.content.addClass("sidebar-projects-dependencies"); + + var editDependencies = $('').appendTo(sidebarSectionsDeps.header); + var editDependenciesFunc = function(depsJSON) { + + RED.editor.editJSON({ + title: RED._('sidebar.project.editDependencies'), + value: JSON.stringify(depsJSON||activeProject.dependencies||{},"",4), + complete: function(v) { + try { + var parsed = JSON.parse(v); + var spinner = addSpinnerOverlay(sidebarSectionsDeps.content); + + editDependencies.addClass('disabled'); + var done = function(err,res) { + if (err) { + editDependenciesFunc(depsJSON); + } + activeProject.dependencies = parsed; + updateProjectDependencies(); + } + sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + done(null,data); + }, + 400: { + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{dependencies:parsed}).always(function() { + spinner.remove(); + editDependencies.removeClass('disabled'); + }); + } catch(err) { + editDependenciesFunc(depsJSON); + } + } + }); + } + editDependencies.click(function(evt) { + evt.preventDefault(); + editDependenciesFunc(); + }); + + sidebarSectionsDepsList = $("
        ",{style:"position: absolute;top: 0;bottom: 0;left: 0;right: 0;"}).appendTo(sidebarSectionsDeps.content); + sidebarSectionsDepsList.editableList({ + addButton: false, + addItem: function(row,index,entry) { + // console.log(entry); + var headerRow = $('
        ',{class:"palette-module-header"}).appendTo(row); + if (entry.label) { + row.parent().addClass("palette-module-section"); + headerRow.text(entry.label); + if (entry.index === 1) { + var addButton = $('').appendTo(headerRow).click(function(evt) { + evt.preventDefault(); + var deps = $.extend(true, {}, activeProject.dependencies); + for (var m in modulesInUse) { + if (modulesInUse.hasOwnProperty(m) && !modulesInUse[m].known) { + deps[m] = modulesInUse[m].version; + } + } + editDependenciesFunc(deps); + }); + } else if (entry.index === 3) { + var removeButton = $('').appendTo(headerRow).click(function(evt) { + evt.preventDefault(); + var deps = $.extend(true, {}, activeProject.dependencies); + for (var m in modulesInUse) { + if (modulesInUse.hasOwnProperty(m) && modulesInUse[m].known && modulesInUse[m].count === 0) { + delete deps[m]; + } + } + editDependenciesFunc(deps); + }); + } + } else { + headerRow.toggleClass("palette-module-unused",entry.count === 0); + entry.element = headerRow; + var titleRow = $('
        ').appendTo(headerRow); + var icon = $('').appendTo(titleRow); + entry.icon = icon; + $('').html(entry.module).appendTo(titleRow); + var metaRow = $('
        ').appendTo(headerRow); + var versionSpan = $('').html(entry.version).appendTo(metaRow); + if (!entry.known) { + headerRow.addClass("palette-module-unknown"); + } else if (entry.known && entry.count === 0) { + + } + } + }, + sort: function(A,B) { + if (A.index && B.index) { + return A.index - B.index; + } + var Acategory = A.index?A.index:(A.known?(A.count>0?0:4):2); + var Bcategory = B.index?B.index:(B.known?(B.count>0?0:4):2); + if (Acategory === Bcategory) { + return A.module.localeCompare(B.module); + } else { + return Acategory - Bcategory; + } + } + }); + sidebarSectionsDeps.container.css("border-bottom","none"); + + // sidebarSectionsSettings = sidebarSections.add({ + // title: RED._("sidebar.project.settings") + // }); + // sidebarSectionsSettings.container.css("border-bottom","none"); + + RED.sidebar.addTab({ + id: "project", + label: RED._("sidebar.project.label"), + name: RED._("sidebar.project.name"), + content: sidebarContent, + onchange: function() { + setTimeout(function() { + sidebarSections.resize(); + },10); + } + }); + + RED.events.on('nodes:add', function(n) { + if (!/^subflow:/.test(n.type)) { + var module = RED.nodes.registry.getNodeSetForType(n.type).module; + if (module !== 'node-red') { + if (!modulesInUse.hasOwnProperty(module)) { + modulesInUse[module] = { + module: module, + version: RED.nodes.registry.getModule(module).version, + count: 0, + known: false + } + } + modulesInUse[module].count++; + if (modulesInUse[module].count === 1 && !modulesInUse[module].known) { + sidebarSectionsDepsList.editableList('addItem',modulesInUse[module]); + } else { + sidebarSectionsDepsList.editableList('sort'); + if (modulesInUse[module].element) { + modulesInUse[module].element.removeClass("palette-module-unused"); + } + } + } + } + }) + RED.events.on('nodes:remove', function(n) { + if (!/^subflow:/.test(n.type)) { + var module = RED.nodes.registry.getNodeSetForType(n.type).module; + if (module !== 'node-red' && modulesInUse.hasOwnProperty(module)) { + modulesInUse[module].count--; + if (modulesInUse[module].count === 0) { + if (!modulesInUse[module].known) { + sidebarSectionsDepsList.editableList('removeItem',modulesInUse[module]); + delete modulesInUse[module]; + } else { + // TODO: a known dependency is now unused by the flow + sidebarSectionsDepsList.editableList('sort'); + modulesInUse[module].element.addClass("palette-module-unused"); + } + } + } + } + }) + + + } + function showSidebar() { + RED.sidebar.show("project"); + } + function addSpinnerOverlay(container) { + var spinner = $('
        ').appendTo(container); + return spinner; + + } + function updateProjectSummary() { + sidebarSectionsInfo.empty(); + if (activeProject) { + var table = $('
        ').appendTo(sidebarSectionsInfo); + var tableBody = $('').appendTo(table); + var propRow; + propRow = $('Project').appendTo(tableBody); + $(propRow.children()[1]).html(' '+(activeProject.name||"")) + } + } + function updateProjectDescription() { + sidebarSectionsDesc.content.empty(); + if (activeProject) { + var div = $('
        ').appendTo(sidebarSectionsDesc.content); + var desc = marked(activeProject.description||""); + var description = addTargetToExternalLinks($('
        '+desc+'
        ')).appendTo(div); + description.find(".bidiAware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "" ); + } + } + function updateProjectDependencies() { + if (activeProject) { + sidebarSectionsDepsList.editableList('empty'); + sidebarSectionsDepsList.editableList('addItem',{index:1, label:"Unknown Dependencies"}); + sidebarSectionsDepsList.editableList('addItem',{index:3, label:"Unused dependencies"}); + var dependencies = activeProject.dependencies||{}; + var moduleList = Object.keys(dependencies); + if (moduleList.length > 0) { + moduleList.sort(); + moduleList.forEach(function(module) { + if (modulesInUse.hasOwnProperty(module)) { + // TODO: this module is used by not currently 'known' + modulesInUse[module].known = true; + } else { + modulesInUse[module] = {module:module,version:dependencies[module], known: true, count:0}; + } + }) + } + for (var module in modulesInUse) { + if (modulesInUse.hasOwnProperty(module)) { + var m = modulesInUse[module]; + if (!dependencies.hasOwnProperty(module) && m.count === 0) { + delete modulesInUse[module]; + } else { + sidebarSectionsDepsList.editableList('addItem',modulesInUse[module]); + } + } + } + } + } + + function refreshSidebar() { + $.getJSON("projects",function(data) { + if (data.active) { + $.getJSON("projects/"+data.active, function(project) { + activeProject = project; + updateProjectSummary(); + updateProjectDescription(); + updateProjectDependencies(); + }); + } + }); + } + + // function getUsedModules() { + // var inuseModules = {}; + // var inuseTypes = {}; + // var getNodeModule = function(node) { + // if (inuseTypes[node.type]) { + // return; + // } + // inuseTypes[node.type] = true; + // inuseModules[RED.nodes.registry.getNodeSetForType(node.type).module] = true; + // } + // RED.nodes.eachNode(getNodeModule); + // RED.nodes.eachConfig(getNodeModule); + // console.log(Object.keys(inuseModules)); + // } + + // TODO: DRY - tab-info.js + function addTargetToExternalLinks(el) { + $(el).find("a").each(function(el) { + var href = $(this).attr('href'); + if (/^https?:/.test(href)) { + $(this).attr('target','_blank'); + } + }); + return el; + } + + return { + init: init, + showStartup: function() { + show('welcome'); + }, + newProject: function() { + show('create') + }, + selectProject: function() { + show('open') + }, + showCredentialsPrompt: function() { + show('credentialSecret'); + }, + showSidebar: showSidebar, + refreshSidebar: refreshSidebar, + showProjectEditor: function() { + RED.editor.editProject({ + project: activeProject, + complete: function(result) { + console.log(result); + } + }); + }, + getActiveProject: function() { + return activeProject; + } + } +})(); diff --git a/editor/js/ui/sidebar.js b/editor/js/ui/sidebar.js index 2a27a45bc..222217668 100644 --- a/editor/js/ui/sidebar.js +++ b/editor/js/ui/sidebar.js @@ -35,7 +35,8 @@ RED.sidebar = (function() { tab.onremove.call(tab); } }, - minimumActiveTabWidth: 110 + minimumActiveTabWidth: 70 + // scrollable: true }); var knownTabs = { diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index d2f37e194..9cfb72cc2 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -50,13 +50,15 @@ RED.sidebar.info = (function() { }).hide(); nodeSection = sections.add({ - title: RED._("sidebar.info.node"), - collapsible: false + title: RED._("sidebar.info.info"), + collapsible: true }); + nodeSection.expand(); infoSection = sections.add({ - title: RED._("sidebar.info.information"), - collapsible: false + title: RED._("sidebar.info.help"), + collapsible: true }); + infoSection.expand(); infoSection.content.css("padding","6px"); infoSection.container.css("border-bottom","none"); @@ -96,23 +98,7 @@ RED.sidebar.info = (function() { RED.sidebar.show("info"); } - function jsonFilter(key,value) { - if (key === "") { - return value; - } - var t = typeof value; - if ($.isArray(value)) { - return "[array:"+value.length+"]"; - } else if (t === "object") { - return "[object]" - } else if (t === "string") { - if (value.length > 30) { - return value.substring(0,30)+" ..."; - } - } - return value; - } - + // TODO: DRY - projects.js function addTargetToExternalLinks(el) { $(el).find("a").each(function(el) { var href = $(this).attr('href'); @@ -127,72 +113,29 @@ RED.sidebar.info = (function() { $(nodeSection.content).empty(); $(infoSection.content).empty(); - var table = $('
        '); - var tableBody = $('').appendTo(table); var propRow; + + var table = $('
        ').appendTo(nodeSection.content); + var tableBody = $('').appendTo(table); var subflowNode; - if (node.type === "tab") { - nodeSection.title.html(RED._("sidebar.info.flow")); - propRow = $(''+RED._("sidebar.info.tabName")+'').appendTo(tableBody); - $(propRow.children()[1]).html(' '+(node.label||"")) - propRow = $(''+RED._("sidebar.info.id")+"").appendTo(tableBody); - RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); - propRow = $(''+RED._("sidebar.info.status")+'').appendTo(tableBody); - $(propRow.children()[1]).html((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled")) + var subflowUserCount; + + var activeProject = RED.projects.getActiveProject(); + if (activeProject) { + propRow = $('Project').appendTo(tableBody); + $(propRow.children()[1]).html(' '+(activeProject.name||"")) + $('').appendTo(tableBody); + } + infoSection.container.show(); + if (node === undefined) { + return; + } else if (Array.isArray(node)) { + infoSection.container.hide(); + propRow = $(''+RED._("sidebar.info.selection")+"").appendTo(tableBody); + $(propRow.children()[1]).html(' '+RED._("sidebar.info.nodes",{count:node.length})) + } else { - nodeSection.title.html(RED._("sidebar.info.node")); - if (node.type !== "subflow" && node.name) { - $(''+RED._("common.label.name")+' '+node.name+'').appendTo(tableBody); - } - $(''+RED._("sidebar.info.type")+" "+node.type+"").appendTo(tableBody); - propRow = $(''+RED._("sidebar.info.id")+"").appendTo(tableBody); - RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); - var m = /^subflow(:(.+))?$/.exec(node.type); - - if (!m && node.type != "subflow" && node.type != "comment") { - if (node._def) { - var count = 0; - var defaults = node._def.defaults; - for (var n in defaults) { - if (n != "name" && defaults.hasOwnProperty(n)) { - var val = node[n]; - var type = typeof val; - count++; - propRow = $(''+n+"").appendTo(tableBody); - if (defaults[n].type) { - var configNode = RED.nodes.node(val); - if (!configNode) { - RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]); - } else { - var configLabel = RED.utils.getNodeLabel(configNode,val); - var container = propRow.children()[1]; - - var div = $('',{class:""}).appendTo(container); - var nodeDiv = $('
        ',{class:"palette_node palette_node_small"}).appendTo(div); - var colour = configNode._def.color; - var icon_url = RED.utils.getNodeIcon(configNode._def); - nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"}); - var iconContainer = $('
        ',{class:"palette_icon_container"}).appendTo(nodeDiv); - $('
        ',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); - var nodeContainer = $('').css({"verticalAlign":"top","marginLeft":"6px"}).html(configLabel).appendTo(container); - - nodeDiv.on('dblclick',function() { - RED.editor.editConfig("", configNode.type, configNode.id); - }) - - } - } else { - RED.utils.createObjectElement(val).appendTo(propRow.children()[1]); - } - } - } - if (count > 0) { - $(''+RED._("sidebar.info.showMore")+''+RED._("sidebar.info.showLess")+' ').appendTo(tableBody); - } - } - } - if (m) { if (m[2]) { subflowNode = RED.nodes.subflow(m[2]); @@ -200,49 +143,120 @@ RED.sidebar.info = (function() { subflowNode = node; } - $(''+RED._("sidebar.info.subflow")+'').appendTo(tableBody); - - var userCount = 0; + subflowUserCount = 0; var subflowType = "subflow:"+subflowNode.id; RED.nodes.eachNode(function(n) { if (n.type === subflowType) { - userCount++; + subflowUserCount++; } }); - $(''+RED._("common.label.name")+''+subflowNode.name+'').appendTo(tableBody); - $(''+RED._("sidebar.info.instances")+""+userCount+'').appendTo(tableBody); } + if (node.type === "tab" || node.type === "subflow") { + propRow = $(''+RED._("sidebar.info."+(node.type==='tab'?'flow':'subflow'))+'').appendTo(tableBody); + RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); + propRow = $(''+RED._("sidebar.info.tabName")+"").appendTo(tableBody); + $(propRow.children()[1]).html(' '+(node.label||node.name||"")) + if (node.type === "tab") { + propRow = $(''+RED._("sidebar.info.status")+'').appendTo(tableBody); + $(propRow.children()[1]).html((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled")) + } + } else { + propRow = $(''+RED._("sidebar.info.node")+"").appendTo(tableBody); + RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); + + + if (node.type !== "subflow" && node.name) { + $(''+RED._("common.label.name")+' '+node.name+'').appendTo(tableBody); + } + if (!m) { + $(''+RED._("sidebar.info.type")+" "+node.type+"").appendTo(tableBody); + } + + if (!m && node.type != "subflow" && node.type != "comment") { + if (node._def) { + var count = 0; + var defaults = node._def.defaults; + for (var n in defaults) { + if (n != "name" && defaults.hasOwnProperty(n)) { + var val = node[n]; + var type = typeof val; + count++; + propRow = $(''+n+"").appendTo(tableBody); + if (defaults[n].type) { + var configNode = RED.nodes.node(val); + if (!configNode) { + RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]); + } else { + var configLabel = RED.utils.getNodeLabel(configNode,val); + var container = propRow.children()[1]; + + var div = $('',{class:""}).appendTo(container); + var nodeDiv = $('
        ',{class:"palette_node palette_node_small"}).appendTo(div); + var colour = configNode._def.color; + var icon_url = RED.utils.getNodeIcon(configNode._def); + nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"}); + var iconContainer = $('
        ',{class:"palette_icon_container"}).appendTo(nodeDiv); + $('
        ',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); + var nodeContainer = $('').css({"verticalAlign":"top","marginLeft":"6px"}).html(configLabel).appendTo(container); + + nodeDiv.on('dblclick',function() { + RED.editor.editConfig("", configNode.type, configNode.id); + }) + + } + } else { + RED.utils.createObjectElement(val).appendTo(propRow.children()[1]); + } + } + } + if (count > 0) { + $(''+RED._("sidebar.info.showMore")+''+RED._("sidebar.info.showLess")+' ').appendTo(tableBody); + } + } + } + if (node.type !== 'tab') { + if (m) { + $(''+RED._("sidebar.info.subflow")+'').appendTo(tableBody); + $(''+RED._("common.label.name")+''+subflowNode.name+'').appendTo(tableBody); + } + } + } + if (m) { + $(''+RED._("sidebar.info.instances")+""+subflowUserCount+'').appendTo(tableBody); + } + + var infoText = ""; + if (!subflowNode && node.type !== "comment" && node.type !== "tab") { + infoSection.title.html(RED._("sidebar.info.nodeHelp")); + var helpText = $("script[data-help-name='"+node.type+"']").html()||(''+RED._("sidebar.info.none")+''); + infoText = helpText; + } else if (node.type === "tab") { + infoSection.title.html(RED._("sidebar.info.flowDesc")); + infoText = marked(node.info||"")||(''+RED._("sidebar.info.none")+''); + } + + if (subflowNode) { + infoText = infoText + (marked(subflowNode.info||"")||(''+RED._("sidebar.info.none")+'')); + infoSection.title.html(RED._("sidebar.info.subflowDesc")); + } else if (node._def && node._def.info) { + infoSection.title.html(RED._("sidebar.info.nodeHelp")); + var info = node._def.info; + var textInfo = (typeof info === "function" ? info.call(node) : info); + // TODO: help + infoText = infoText + marked(textInfo); + } + if (infoText) { + setInfoText(infoText); + } + + + $(".node-info-property-header").click(function(e) { + e.preventDefault(); + expandedSections["property"] = !expandedSections["property"]; + $(this).toggleClass("expanded",expandedSections["property"]); + $(".node-info-property-row").toggle(expandedSections["property"]); + }); } - $(table).appendTo(nodeSection.content); - - var infoText = ""; - - if (!subflowNode && node.type !== "comment" && node.type !== "tab") { - var helpText = $("script[data-help-name='"+node.type+"']").html()||""; - infoText = helpText; - } else if (node.type === "tab") { - infoText = marked(node.info||""); - } - - if (subflowNode) { - infoText = infoText + marked(subflowNode.info||""); - } else if (node._def && node._def.info) { - var info = node._def.info; - var textInfo = (typeof info === "function" ? info.call(node) : info); - // TODO: help - infoText = infoText + marked(textInfo); - } - if (infoText) { - setInfoText(infoText); - } - - - $(".node-info-property-header").click(function(e) { - e.preventDefault(); - expandedSections["property"] = !expandedSections["property"]; - $(this).toggleClass("expanded",expandedSections["property"]); - $(".node-info-property-row").toggle(expandedSections["property"]); - }); } function setInfoText(infoText) { var info = addTargetToExternalLinks($('
        '+infoText+'
        ')).appendTo(infoSection.content); @@ -342,14 +356,16 @@ RED.sidebar.info = (function() { })(); function clear() { - sections.hide(); - // + // sections.hide(); + refresh(); } - function set(html) { + function set(html,title) { // tips.stop(); - sections.show(); - nodeSection.container.hide(); + // sections.show(); + // nodeSection.container.hide(); + infoSection.title.text(title||""); + refresh(); $(infoSection.content).empty(); setInfoText(html); $(".sidebar-node-info-stack").scrollTop(0); @@ -366,6 +382,8 @@ RED.sidebar.info = (function() { } else { refresh(node); } + } else { + refresh(selection.nodes); } } else { var activeWS = RED.workspaces.active(); @@ -378,7 +396,8 @@ RED.sidebar.info = (function() { if (workspace && workspace.info) { refresh(workspace); } else { - clear(); + refresh() + // clear(); } } } diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 453066f58..c83abf9d2 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -396,10 +396,10 @@ RED.view = (function() { } }); $("#chart").focus(function() { - $("#workspace-tabs").addClass("workspace-focussed") + $("#workspace-tabs").addClass("workspace-focussed"); }); $("#chart").blur(function() { - $("#workspace-tabs").removeClass("workspace-focussed") + $("#workspace-tabs").removeClass("workspace-focussed"); }); RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection); diff --git a/editor/js/ui/workspaces.js b/editor/js/ui/workspaces.js index 3d880e2a2..1f3b3c52e 100644 --- a/editor/js/ui/workspaces.js +++ b/editor/js/ui/workspaces.js @@ -244,10 +244,16 @@ RED.workspaces = (function() { if (tab.disabled) { $("#red-ui-tab-"+(tab.id.replace(".","-"))).addClass('workspace-disabled'); } - RED.menu.setDisabled("menu-item-workspace-delete",workspace_tabs.count() == 1); + RED.menu.setDisabled("menu-item-workspace-delete",workspace_tabs.count() <= 1); + if (workspace_tabs.count() === 1) { + showWorkspace(); + } }, onremove: function(tab) { - RED.menu.setDisabled("menu-item-workspace-delete",workspace_tabs.count() == 1); + RED.menu.setDisabled("menu-item-workspace-delete",workspace_tabs.count() <= 1); + if (workspace_tabs.count() === 0) { + hideWorkspace(); + } }, onreorder: function(oldOrder, newOrder) { RED.history.push({t:'reorder',order:oldOrder,dirty:RED.nodes.dirty()}); @@ -261,6 +267,16 @@ RED.workspaces = (function() { } }); } + function showWorkspace() { + $("#workspace .red-ui-tabs").show() + $("#chart").show() + $("#workspace-footer").children().show() + } + function hideWorkspace() { + $("#workspace .red-ui-tabs").hide() + $("#chart").hide() + $("#workspace-footer").children().hide() + } function init() { createWorkspaceTabs(); @@ -280,6 +296,8 @@ RED.workspaces = (function() { RED.actions.add("core:add-flow",addWorkspace); RED.actions.add("core:edit-flow",editWorkspace); RED.actions.add("core:remove-flow",removeWorkspace); + + hideWorkspace(); } function editWorkspace(id) { diff --git a/editor/sass/notifications.scss b/editor/sass/notifications.scss index 80640dd28..7f1fe4f43 100644 --- a/editor/sass/notifications.scss +++ b/editor/sass/notifications.scss @@ -15,7 +15,7 @@ **/ #notifications { - z-index: 10000; + z-index: 100; width: 500px; margin-left: -250px; left: 50%; diff --git a/editor/sass/palette-editor.scss b/editor/sass/palette-editor.scss index 48e007e10..0c83071b3 100644 --- a/editor/sass/palette-editor.scss +++ b/editor/sass/palette-editor.scss @@ -95,29 +95,6 @@ .palette-module-shade-status { color: #666; } - - .palette-module-meta { - color: #666; - position: relative; - &.disabled { - color: #ccc; - } - - .fa { - width: 15px; - text-align: center; - margin-right: 5px; - } - } - .palette-module-name { - white-space: nowrap; - @include enable-selection; - } - .palette-module-version, .palette-module-updated, .palette-module-link { - font-style:italic; - font-size: 0.8em; - @include enable-selection; - } .palette-module-updated { margin-left: 10px; } @@ -224,3 +201,32 @@ } } +.palette-module-meta { + color: #666; + position: relative; + &.disabled { + color: #ccc; + } + + .fa { + width: 15px; + text-align: center; + margin-right: 5px; + } +} +.palette-module-name { + white-space: nowrap; + @include enable-selection; +} +.palette-module-version, .palette-module-updated, .palette-module-link { + font-style:italic; + font-size: 0.8em; + @include enable-selection; +} +.palette-module-section { + padding:0 !important; + background: #f9f9f9 !important; + font-size: 0.9em; + color: #666; + +} diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss new file mode 100644 index 000000000..e0ee31cf6 --- /dev/null +++ b/editor/sass/projects.scss @@ -0,0 +1,233 @@ +/** + * 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. + **/ + +#projects-dialog { + .red-ui-editableList-container { + padding: 0px; + } + +} +.projects-edit-form form { + margin: 0; + .form-row { + margin-bottom: 20px; + label { + width: auto; + display: block; + } + input[type=text], input[type=password],textarea { + width: 100%; + } + input[type=checkbox], input[type=radio] { + width: auto; + vertical-align: top; + } } +} +.projects-dialog-spinner { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + text-align: center; + padding: 40px; + background: white; + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -0.25em; + } + img { + display: inline-block; + vertical-align: middle; + width: 80px; + } + &.projects-dialog-spinner-sidebar { + padding: 20px; + background: $shade-color; + img { + width: 50px; + } + } + + +} +.projects-dialog-screen-start { + button.editor-button { + width: calc(50% - 40px); + margin: 20px; + height: 200px; + line-height: 2em; + font-size: 1.5em !important; + i { + color: #ccc; + } + &:hover i { + color: #aaa; + } + } + .button-group { + text-align: center; + } +} +.projects-dialog-screen-create { + min-height: 500px; + button.editor-button { + height: auto; + padding: 10px; + + } + .button-group { + text-align: center; + } +} + +.projects-dialog-screen-secret { + min-height: auto; +} +.projects-dialog-project-list { + li { + padding: 0 !important; + } + &.projects-dialog-project-list-small { + .projects-dialog-project-list-entry { + padding: 6px 0; + i { + font-size: 1em; + } + } + .projects-dialog-project-list-entry-name { + font-size: 1em; + } + .projects-dialog-project-list-entry-current { + margin-right: 10px; + padding-top: 2px; + } + } + +} +.projects-dialog-project-list-entry { + padding: 12px 0; + border-left: 3px solid #fff; + border-right: 3px solid #fff; + &.projects-list-entry-current { + &:not(.selectable) { + background: #f9f9f9; + } + i { + color: #999; + } + } + &.selectable { + cursor: pointer; + &:hover { + background: #f3f3f3; + border-left-color: #aaa; + border-right-color: #aaa; + } + } + + i { + color: #ccc; + font-size: 2em; + + } + &.selected { + background: #efefef; + border-left-color:#999; + border-right-color:#999; + } + span { + display: inline-block; + vertical-align:middle; + } + .projects-dialog-project-list-entry-icon { + margin: 0 10px 0 5px; + } + .projects-dialog-project-list-entry-name { + font-size: 1.2em; + } + .projects-dialog-project-list-entry-current { + float: right; + margin-right: 20px; + font-size: 0.9em; + color: #999; + padding-top: 4px; + } +} + + +.sidebar-projects { + height: 100%; +} +.sidebar-projects-stack-info { + height: 100px; + box-sizing: border-box; + border-bottom: 1px solid $secondary-border-color; + color: #333; + i { + color: #999; + } +} +.sidebar-projects-stack { + position: absolute; + top: 100px; + bottom: 0; + left: 0; + right: 0; + overflow-y: scroll; + + .palette-category { + &:not(.palette-category-expanded) button { + display: none; + } + } +} + +.sidebar-projects-dependencies { + position: relative; + height: 100%; + .red-ui-editableList-container { + padding: 0; + } + .red-ui-editableList-border { + border: none; + border-radius: 0; + } + .red-ui-editableList-item-content { + padding: 0px 6px; + } + .palette-module-header { + padding: 6px 4px; + } + .palette-module-button { + float: right; + } + .palette-module-unused { + & > * { + color: #bbb; + } + // border: 1px dashed #bbb; + } + .palette-module-unknown { + // border: 1px dashed #b72828; + i.fa-warning { + color: #b07575; //#b72828; + } + } +} diff --git a/editor/sass/style.scss b/editor/sass/style.scss index ed683bb8c..5d536c742 100644 --- a/editor/sass/style.scss +++ b/editor/sass/style.scss @@ -48,12 +48,15 @@ @import "userSettings"; +@import "projects"; + @import "ui/common/editableList"; @import "ui/common/searchBox"; @import "ui/common/typedInput"; @import "ui/common/nodeList"; @import "ui/common/checkboxSet"; +@import "ui/common/stack"; @import "dragdrop"; diff --git a/editor/sass/tab-info.scss b/editor/sass/tab-info.scss index 8b8e0377c..cd278e445 100644 --- a/editor/sass/tab-info.scss +++ b/editor/sass/tab-info.scss @@ -87,12 +87,16 @@ table.node-info tr.blank { padding-left: 5px; } } - +.node-info-none { + font-style: italic; + color: #aaa; +} table.node-info tr:not(.blank) td:first-child{ - color: #666; + color: #444; vertical-align: top; width: 90px; padding: 3px 3px 3px 6px; + background:#f9f9f9; border-right: 1px solid #ddd; } table.node-info tr:not(.blank) td:last-child{ diff --git a/editor/sass/ui/common/stack.scss b/editor/sass/ui/common/stack.scss new file mode 100644 index 000000000..96a5798d6 --- /dev/null +++ b/editor/sass/ui/common/stack.scss @@ -0,0 +1,22 @@ +/** + * 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-ui-stack { + background: white; + .palette-category { + background: white; + } +} diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 21c5a6376..4a8abe80b 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -209,6 +209,14 @@
        + + diff --git a/nodes/core/io/10-mqtt.js b/nodes/core/io/10-mqtt.js index b01df652b..f57e8cc79 100644 --- a/nodes/core/io/10-mqtt.js +++ b/nodes/core/io/10-mqtt.js @@ -120,6 +120,8 @@ module.exports = function(RED) { tlsNode.addTLSOptions(this.options); } } + console.log(this.brokerurl,this.options); + // If there's no rejectUnauthorized already, then this could be an // old config where this option was provided on the broker node and // not the tls node @@ -164,6 +166,7 @@ module.exports = function(RED) { }; this.connect = function () { + console.log("CONNECT"); if (!node.connected && !node.connecting) { node.connecting = true; node.client = mqtt.connect(node.brokerurl ,node.options); diff --git a/red/api/admin/info.js b/red/api/admin/info.js index 83b16210f..8a5efa0ff 100644 --- a/red/api/admin/info.js +++ b/red/api/admin/info.js @@ -20,7 +20,6 @@ var settings; module.exports = { init: function(_runtime) { - console.log("info.init"); runtime = _runtime; settings = runtime.settings; }, diff --git a/red/api/editor/index.js b/red/api/editor/index.js index c236bbb26..20461ee52 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -66,10 +66,10 @@ module.exports = { editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); - // //Projects - // var projects = require("./projects"); - // projects.init(runtime); - // editorApp.get("/projects",projects.app()); + //Projects + var projects = require("./projects"); + projects.init(runtime); + editorApp.use("/projects",projects.app()); // Locales var locales = require("./locales"); diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index 4fd50f177..7ed7f574f 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -84,7 +84,8 @@ "undeployedChanges": "node has undeployed changes", "nodeActionDisabled": "node actions disabled within subflow", "missing-types": "Flows stopped due to missing node types. Check logs for details.", - "restartRequired": "Node-RED must be restarted to enable upgraded modules" + "restartRequired": "Node-RED must be restarted to enable upgraded modules", + "invalid-credentials-secret": "Flows stopped due to missing or invalid credentialSecret" }, "error": "Error: __message__", @@ -388,7 +389,12 @@ "showMore": "show more", "showLess": "show less", "flow": "Flow", - "information": "Information", + "selection":"Selection", + "nodes":"__count__ nodes", + "flowDesc": "Flow Description", + "subflowDesc": "Subflow Description", + "nodeHelp": "Node Help", + "none":"None", "arrayItems": "__count__ items", "showTips":"You can open the tips from the settings panel" }, @@ -406,6 +412,15 @@ "palette": { "name": "Palette management", "label": "palette" + }, + "project": { + "label": "project", + "name": "Project", + "description": "Description", + "dependencies": "Dependencies", + "settings": "Settings", + "editDescription": "Edit project description", + "editDependencies": "Edit project dependencies" } }, "typedInput": { @@ -447,6 +462,9 @@ "title": "JSON editor", "format": "format JSON" }, + "markdownEditor": { + "title": "Markdown editor" + }, "bufferEditor": { "title": "Buffer editor", "modeString": "Handle as UTF-8 String", diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js new file mode 100644 index 000000000..79a4faad2 --- /dev/null +++ b/red/api/editor/projects/index.js @@ -0,0 +1,137 @@ +/** + * 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. + **/ + +var express = require("express"); +var runtime; +var settings; + +module.exports = { + init: function(_runtime) { + runtime = _runtime; + settings = runtime.settings; + }, + app: function() { + var app = express(); + + // Projects + + app.get("/", function(req,res) { + // List projects + runtime.storage.projects.listProjects().then(function(list) { + var active = runtime.storage.projects.getActiveProject(); + var response = { + active: active, + projects: list + }; + res.json(response); + }).otherwise(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + }); + + app.post("/", function(req,res) { + // Create project + runtime.storage.projects.createProject(req.body).then(function(name) { + console.log("Created project",name); + runtime.storage.projects.getProject(name).then(function(data) { + res.json(data); + }); + }).otherwise(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + }); + + app.put("/:id", function(req,res) { + // Update a project + //TODO: validate the payload properly + if (req.body.active) { + var currentProject = runtime.storage.projects.getActiveProject(); + if (req.params.id !== currentProject) { + runtime.storage.projects.setActiveProject(req.params.id).then(function() { + res.redirect(303,req.baseUrl + '/'); + }).otherwise(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + } else { + res.redirect(303,req.baseUrl + '/'); + } + } else if (req.body.credentialSecret || req.body.description || req.body.dependencies) { + runtime.storage.projects.updateProject(req.params.id, req.body).then(function() { + res.redirect(303,req.baseUrl + '/'); + }).otherwise(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) + } + + }); + + app.get("/:id", function(req,res) { + // Get project metadata + runtime.storage.projects.getProject(req.params.id).then(function(data) { + if (data) { + res.json(data); + } else { + res.status(404).end(); + } + }).otherwise(function(err) { + console.log(err.stack); + res.status(400).json({error:"unexpected_error", message:err.toString()}); + }) + }); + + app.delete("/:id",function(req,res) { + // Delete project + }); + + // Project Files + + app.get("/:id/files", function(req,res) { + runtime.storage.projects.getFiles(req.params.id).then(function(data) { + res.json(data); + }) + .otherwise(function(err) { + console.log(err.stack); + res.status(400).json({error:"unexpected_error", message:err.toString()}); + }) + }); + + app.get(new RegExp("/([^\/]+)\/files\/(.*)"), function(req,res) { + // Get project file + }); + + app.post(new RegExp("/([^\/]+)\/files\/(.*)"), function(req,res) { + // Update project file + }); + + return app; + } +} diff --git a/red/runtime/index.js b/red/runtime/index.js index 47ff205f7..c8f269d37 100644 --- a/red/runtime/index.js +++ b/red/runtime/index.js @@ -158,7 +158,7 @@ function start() { if (settings.httpStatic) { log.info(log._("runtime.paths.httpStatic",{path:path.resolve(settings.httpStatic)})); } - redNodes.loadFlows().then(redNodes.startFlows); + redNodes.loadFlows().then(redNodes.startFlows).otherwise(function(err) {}); started = true; }).otherwise(function(err) { console.log(err); diff --git a/red/runtime/locales/en-US/runtime.json b/red/runtime/locales/en-US/runtime.json index 37c82dc14..9853a2b3f 100644 --- a/red/runtime/locales/en-US/runtime.json +++ b/red/runtime/locales/en-US/runtime.json @@ -133,6 +133,8 @@ "localfilesystem": { "user-dir": "User directory : __path__", "flows-file": "Flows file : __path__", + "changing-project": "Setting active project : __project__", + "active-project": "Active project : __project__", "create": "Creating new __type__ file", "empty": "Existing __type__ file is empty", "invalid": "Existing __type__ file is not valid json", diff --git a/red/runtime/nodes/credentials.js b/red/runtime/nodes/credentials.js index 1101949f8..25f20d5f0 100644 --- a/red/runtime/nodes/credentials.js +++ b/red/runtime/nodes/credentials.js @@ -58,7 +58,7 @@ var api = module.exports = { */ var credentialsEncrypted = credentials.hasOwnProperty("$") && Object.keys(credentials).length === 1; var setupEncryptionPromise = when.resolve(); - if (encryptionEnabled === null) { + // if (encryptionEnabled === null) { var defaultKey; try { defaultKey = settings.get('_credentialSecret'); @@ -73,12 +73,34 @@ var api = module.exports = { } catch(err) { userKey = false; } + + var projectKey = false; + try { + var projects = settings.get('projects'); + if (projects && projects.activeProject) { + projectKey = projects.projects[projects.activeProject].credentialSecret; + } + } catch(err) { + } + if (projectKey) { + log.debug("red/runtime/nodes/credentials.load : using active project key - ignoring user provided key"); + console.log(projectKey); + userKey = projectKey; + } + + //TODO: Need to consider the various migration scenarios from no-project to project + // - _credentialSecret exists, projectKey exists + // - _credentialSecret does not exist, projectKey exists + // - userKey exists, projectKey exists if (userKey === false) { log.debug("red/runtime/nodes/credentials.load : user disabled encryption"); // User has disabled encryption encryptionEnabled = false; // Check if we have a generated _credSecret to decrypt with and remove if (defaultKey) { + console.log("****************************************************************"); + console.log("* Oh oh - default key present. We don't handle this well today *"); + console.log("****************************************************************"); log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate"); if (credentialsEncrypted) { try { @@ -86,13 +108,18 @@ var api = module.exports = { } catch(err) { credentials = {}; log.warn(log._("nodes.credentials.error",{message:err.toString()})) + var error = new Error("Failed to decrypt credentials"); + error.code = "credentials_load_failed"; + return when.reject(error); } } dirty = true; removeDefaultKey = true; } } else if (typeof userKey === 'string') { - log.debug("red/runtime/nodes/credentials.load : user provided key"); + if (!projectKey) { + log.debug("red/runtime/nodes/credentials.load : user provided key"); + } // User has provided own encryption key, get the 32-byte hash of it encryptionKey = crypto.createHash('sha256').update(userKey).digest(); encryptionEnabled = true; @@ -107,6 +134,9 @@ var api = module.exports = { } catch(err) { credentials = {}; log.warn(log._("nodes.credentials.error",{message:err.toString()})) + var error = new Error("Failed to decrypt credentials"); + error.code = "credentials_load_failed"; + return when.reject(error); } } dirty = true; @@ -136,7 +166,7 @@ var api = module.exports = { log.debug("red/runtime/nodes/credentials.load : using default key"); } } - } + //} if (encryptionEnabled && !dirty) { encryptedCredentials = credentials; } @@ -149,6 +179,9 @@ var api = module.exports = { credentialCache = {}; dirty = true; log.warn(log._("nodes.credentials.error",{message:err.toString()})) + var error = new Error("Failed to decrypt credentials"); + error.code = "credentials_load_failed"; + return when.reject(error); } } else { credentialCache = credentials; diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index 18f154158..80753e38a 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -73,15 +73,18 @@ function loadFlows() { return storage.getFlows().then(function(config) { log.debug("loaded flow revision: "+config.rev); return credentials.load(config.credentials).then(function() { + events.emit("runtime-event",{id:"runtime-state",retain:true}); return config; }); }).otherwise(function(err) { + activeConfig = null; + events.emit("runtime-event",{id:"runtime-state",payload:{type:"warning",error:"credentials_load_failed",text:"notification.warnings.invalid-credentials-secret"},retain:true}); log.warn(log._("nodes.flows.error",{message:err.toString()})); - console.log(err.stack); + throw err; }); } -function load() { - return setFlows(null,"load",false); +function load(forceStart) { + return setFlows(null,"load",false,forceStart); } /* @@ -89,7 +92,7 @@ function load() { * type - full/nodes/flows/load (default full) * muteLog - don't emit the standard log messages (used for individual flow api) */ -function setFlows(_config,type,muteLog) { +function setFlows(_config,type,muteLog,forceStart) { type = type||"full"; var configSavePromise = null; @@ -131,7 +134,7 @@ function setFlows(_config,type,muteLog) { rev:flowRevision }; activeFlowConfig = newFlowConfig; - if (started) { + if (forceStart || started) { return stop(type,diff,muteLog).then(function() { context.clean(activeFlowConfig); start(type,diff,muteLog).then(function() { @@ -228,6 +231,7 @@ function handleStatus(node,statusMessage) { function start(type,diff,muteLog) { + console.log("START----") //dumpActiveNodes(); type = type||"full"; started = true; @@ -323,6 +327,9 @@ function start(type,diff,muteLog) { } function stop(type,diff,muteLog) { + if (!started) { + return when.resolve(); + } type = type||"full"; diff = diff||{ added:[], diff --git a/red/runtime/storage/index.js b/red/runtime/storage/index.js index 1d6a13c65..1262ab91a 100644 --- a/red/runtime/storage/index.js +++ b/red/runtime/storage/index.js @@ -54,7 +54,10 @@ var storageModuleInterface = { } catch (e) { return when.reject(e); } - return storageModule.init(runtime.settings); + if (storageModule.projects) { + storageModuleInterface.projects = storageModule.projects; + } + return storageModule.init(runtime.settings,runtime); }, getFlows: function() { return storageModule.getFlows().then(function(flows) { diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js index 05c43cd40..a4c3da56c 100644 --- a/red/runtime/storage/localfilesystem/index.js +++ b/red/runtime/storage/localfilesystem/index.js @@ -16,7 +16,6 @@ var fs = require('fs-extra'); var when = require('when'); -var nodeFn = require('when/node/function'); var fspath = require("path"); var log = require("../../log"); @@ -24,6 +23,7 @@ var util = require("./util"); var library = require("./library"); var sessions = require("./sessions"); var runtimeSettings = require("./settings"); +var projects = require("./projects"); var initialFlowLoadComplete = false; var settings; @@ -32,10 +32,9 @@ var flowsFullPath; var flowsFileBackup; var credentialsFile; var credentialsFileBackup; -var oldCredentialsFile; var localfilesystem = { - init: function(_settings) { + init: function(_settings, runtime) { settings = _settings; var promises = []; @@ -62,50 +61,14 @@ var localfilesystem = { } } - if (settings.flowFile) { - flowsFile = settings.flowFile; - // handle Unix and Windows "C:\" - if ((flowsFile[0] == "/") || (flowsFile[1] == ":")) { - // Absolute path - flowsFullPath = flowsFile; - } else if (flowsFile.substring(0,2) === "./") { - // Relative to cwd - flowsFullPath = fspath.join(process.cwd(),flowsFile); - } else { - try { - fs.statSync(fspath.join(process.cwd(),flowsFile)); - // Found in cwd - flowsFullPath = fspath.join(process.cwd(),flowsFile); - } catch(err) { - // Use userDir - flowsFullPath = fspath.join(settings.userDir,flowsFile); - } - } - - } else { - flowsFile = 'flows_'+require('os').hostname()+'.json'; - flowsFullPath = fspath.join(settings.userDir,flowsFile); - } - var ffExt = fspath.extname(flowsFullPath); - var ffName = fspath.basename(flowsFullPath); - var ffBase = fspath.basename(flowsFullPath,ffExt); - var ffDir = fspath.dirname(flowsFullPath); - - credentialsFile = fspath.join(settings.userDir,ffBase+"_cred"+ffExt); - credentialsFileBackup = fspath.join(settings.userDir,"."+ffBase+"_cred"+ffExt+".backup"); - - oldCredentialsFile = fspath.join(settings.userDir,"credentials.json"); - - flowsFileBackup = fspath.join(ffDir,"."+ffName+".backup"); - sessions.init(settings); runtimeSettings.init(settings); + promises.push(library.init(settings)); + promises.push(projects.init(settings, runtime)); var packageFile = fspath.join(settings.userDir,"package.json"); var packagePromise = when.resolve(); - promises.push(library.init(settings)); - if (!settings.readOnly) { packagePromise = function() { try { @@ -124,64 +87,19 @@ var localfilesystem = { return when.all(promises).then(packagePromise); }, - getFlows: function() { - if (!initialFlowLoadComplete) { - initialFlowLoadComplete = true; - log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir})); - log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath})); - } - return util.readFile(flowsFullPath,flowsFileBackup,[],'flow'); - }, - saveFlows: function(flows) { - if (settings.readOnly) { - return when.resolve(); - } + getFlows: projects.getFlows, + saveFlows: projects.saveFlows, + getCredentials: projects.getCredentials, + saveCredentials: projects.saveCredentials, - try { - fs.renameSync(flowsFullPath,flowsFileBackup); - } catch(err) { - } - - var flowData; - - if (settings.flowFilePretty) { - flowData = JSON.stringify(flows,null,4); - } else { - flowData = JSON.stringify(flows); - } - return util.writeFile(flowsFullPath, flowData); - }, - - getCredentials: function() { - return util.readFile(credentialsFile,credentialsFileBackup,{},'credentials'); - }, - - saveCredentials: function(credentials) { - if (settings.readOnly) { - return when.resolve(); - } - - try { - fs.renameSync(credentialsFile,credentialsFileBackup); - } catch(err) { - } - var credentialData; - if (settings.flowFilePretty) { - credentialData = JSON.stringify(credentials,null,4); - } else { - credentialData = JSON.stringify(credentials); - } - return util.writeFile(credentialsFile, credentialData); - }, - getSettings: runtimeSettings.getSettings, saveSettings: runtimeSettings.saveSettings, getSessions: sessions.getSessions, saveSessions: sessions.saveSessions, getLibraryEntry: library.getLibraryEntry, - saveLibraryEntry: library.saveLibraryEntry - + saveLibraryEntry: library.saveLibraryEntry, + projects: projects }; module.exports = localfilesystem; diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js new file mode 100644 index 000000000..eb974ca34 --- /dev/null +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -0,0 +1,86 @@ +/** + * 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. + **/ + +var when = require('when'); +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; + + +function execCommand(command,args,cwd) { + return when.promise(function(resolve,reject) { + var fullCommand = command+" "+args.join(" "); + child = exec(fullCommand, {cwd: cwd, timeout:3000, killSignal: 'SIGTERM'}, function (error, stdout, stderr) { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); +} + +function runCommand(command,args,cwd) { + console.log(cwd,command,args); + return when.promise(function(resolve,reject) { + var child = spawn(command, args, {cwd:cwd, detached:true}); + var stdout = ""; + var stderr = ""; + child.stdout.on('data', function(data) { + stdout += data; + }); + + child.stderr.on('data', function(data) { + stderr += data; + }); + + child.on('close', function(code) { + if (code !== 0) { + var err = new Error(stderr); + if (/fatal: could not read Username/.test(stderr)) { + err.code = "git_auth_failed"; + } else if(/HTTP Basic: Access denied/.test(stderr)) { + err.code = "git_auth_failed"; + } else { + err.code = "git_error"; + } + return reject(err); + } + resolve(stdout); + }); + }); +} +function isAuthError(err) { + // var lines = err.toString().split("\n"); + // lines.forEach(console.log); +} + +var gitCommand = "git"; +module.exports = { + initRepo: function(cwd) { + return runCommand(gitCommand,["init"],cwd); + }, + pull: function(repo, cwd) { + if (repo.url) { + repo = repo.url; + } + var args = ["pull",repo,"master"]; + return runCommand(gitCommand,args,cwd); + }, + clone: function(repo, cwd) { + var args = ["clone",repo,"."]; + return runCommand(gitCommand,args,cwd); + } +} diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js new file mode 100644 index 000000000..b1907494e --- /dev/null +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -0,0 +1,519 @@ +/** + * 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. + **/ + +var fs = require('fs-extra'); +var when = require('when'); +var fspath = require("path"); +var nodeFn = require('when/node/function'); +var crypto = require('crypto'); + +var storageSettings = require("../settings"); +var util = require("../util"); +var gitTools = require("./git"); + +var settings; +var runtime; + +var projectsDir; + + +function init(_settings, _runtime) { + settings = _settings; + runtime = _runtime; + log = runtime.log; + + projectsDir = fspath.join(settings.userDir,"projects"); + + if (settings.flowFile) { + flowsFile = settings.flowFile; + // handle Unix and Windows "C:\" + if ((flowsFile[0] == "/") || (flowsFile[1] == ":")) { + // Absolute path + flowsFullPath = flowsFile; + } else if (flowsFile.substring(0,2) === "./") { + // Relative to cwd + flowsFullPath = fspath.join(process.cwd(),flowsFile); + } else { + try { + fs.statSync(fspath.join(process.cwd(),flowsFile)); + // Found in cwd + flowsFullPath = fspath.join(process.cwd(),flowsFile); + } catch(err) { + // Use userDir + flowsFullPath = fspath.join(settings.userDir,flowsFile); + } + } + + } else { + flowsFile = 'flows_'+require('os').hostname()+'.json'; + flowsFullPath = fspath.join(settings.userDir,flowsFile); + } + var ffExt = fspath.extname(flowsFullPath); + var ffBase = fspath.basename(flowsFullPath,ffExt); + + flowsFileBackup = getBackupFilename(flowsFullPath); + credentialsFile = fspath.join(settings.userDir,ffBase+"_cred"+ffExt); + credentialsFileBackup = getBackupFilename(credentialsFile) + + if (!settings.readOnly) { + return util.promiseDir(projectsDir) + //TODO: this is accessing settings from storage directly as settings + // has not yet been initialised. That isn't ideal - can this be deferred? + .then(storageSettings.getSettings) + .then(function(globalSettings) { + if (!globalSettings.projects) { + // TODO: Migration Case + console.log("TODO: Migration from single file to project"); + globalSettings.projects = { + activeProject: "", + projects: {} + } + return storageSettings.saveSettings(globalSettings); + } else { + activeProject = globalSettings.projects.activeProject; + var projectPath = fspath.join(projectsDir,activeProject); + flowsFullPath = fspath.join(projectPath,"flow.json"); + flowsFileBackup = getBackupFilename(flowsFullPath); + credentialsFile = fspath.join(projectPath,"flow_cred.json"); + credentialsFileBackup = getBackupFilename(credentialsFile); + } + }); + } else { + return when.resolve(); + } +} + +function getBackupFilename(filename) { + var ffName = fspath.basename(filename); + var ffDir = fspath.dirname(filename); + return fspath.join(ffDir,"."+ffName+".backup"); +} + +function listProjects() { + return nodeFn.call(fs.readdir, projectsDir).then(function(fns) { + var dirs = []; + fns.sort().filter(function(fn) { + var fullPath = fspath.join(projectsDir,fn); + if (fn[0] != ".") { + var stats = fs.lstatSync(fullPath); + if (stats.isDirectory()) { + dirs.push(fn); + } + } + }); + return dirs; + }); +} + +function getProject(project) { + + return when.promise(function(resolve,reject) { + if (project === "") { + return reject(new Error("NLS: No active project set")); + } + var projectPath = fspath.join(projectsDir,project); + var globalProjectSettings = settings.get("projects"); + var projectSettings = {}; + if (globalProjectSettings.projects) { + projectSettings = globalProjectSettings.projects[project]||{}; + } + + // console.log(projectSettings); + var projectData = { + name: project + }; + var promises = []; + checkProjectFiles(project).then(function(missingFiles) { + if (missingFiles.length > 0) { + projectData.missingFiles = missingFiles; + } + if (missingFiles.indexOf('package.json') === -1) { + promises.push(nodeFn.call(fs.readFile,fspath.join(projectPath,"package.json"),"utf8").then(function(content) { + var package = util.parseJSON(content); + projectData.dependencies = package.dependencies||{}; + })); + } + if (missingFiles.indexOf('README.md') === -1) { + promises.push(nodeFn.call(fs.readFile,fspath.join(projectPath,"README.md"),"utf8").then(function(content) { + projectData.description = content; + })); + } else { + projectData.description = ""; + } + + when.settle(promises).then(function() { + resolve(projectData); + }) + // if (missingFiles.indexOf('flow_cred.json') === -1) { + // promises.push(nodeFn.call(fs.readFile,fspath.join(projectPath,"flow_cred.json"),"utf8").then(function(creds) { + // var credentials = util.parseJSON(creds); + // if (credentials.hasOwnProperty('$')) { + // // try { + // // decryptCredentials + // // } + // } + // })); + // } + + }); + + // fs.stat(projectPath,function(err,stat) { + // if (err) { + // return resolve(null); + // } + // resolve(nodeFn.call(fs.readFile,projectPackage,'utf8').then(util.parseJSON)); + // }) + }).otherwise(function(err) { + console.log(err); + var e = new Error("NLD: project not found"); + e.code = "project_not_found"; + throw e; + }); +} + +var encryptionAlgorithm = "aes-256-ctr"; +function decryptCredentials(key,credentials) { + var creds = credentials["$"]; + var initVector = new Buffer(creds.substring(0, 32),'hex'); + creds = creds.substring(32); + var decipher = crypto.createDecipheriv(encryptionAlgorithm, key, initVector); + var decrypted = decipher.update(creds, 'base64', 'utf8') + decipher.final('utf8'); + return JSON.parse(decrypted); +} + +function setCredentialSecret(project,secret) { + var globalProjectSettings = settings.get("projects"); + globalProjectSettings.projects = globalProjectSettings.projects || {}; + globalProjectSettings.projects[project] = globalProjectSettings.projects[project] || {}; + globalProjectSettings.projects[project].credentialSecret = secret; + return settings.set("projects",globalProjectSettings); +} + +function createProject(metadata) { + var project = metadata.name; + return when.promise(function(resolve,reject) { + if (project === "") { + return reject(new Error("NLS: No project set")); + } + var projectPath = fspath.join(projectsDir,project); + fs.stat(projectPath, function(err,stat) { + if (!err) { + var e = new Error("NLS: Project already exists"); + e.code = "project_exists"; + return reject(e); + } + createProjectDirectory(project).then(function() { + if (metadata.credentialSecret) { + return setCredentialSecret(project,metadata.credentialSecret); + } + return when.resolve(); + }).then(function() { + if (metadata.remote) { + return gitTools.pull(metadata.remote,projectPath).then(function(result) { + // Check this is a valid project + // If it is empty + // - if 'populate' flag is set, call populateProject + // - otherwise reject with suitable error to allow UI to confirm population + // If it is missing package.json/flow.json/flow_cred.json + // - reject as invalid project + + checkProjectFiles(project).then(function(results) { + console.log("checkProjectFiles"); + console.log(results); + }); + + resolve(project); + }).otherwise(function(error) { + fs.remove(projectPath,function() { + reject(error); + }); + }) + } else { + createDefaultProject(metadata).then(function() { resolve(project)}).otherwise(reject); + } + }).otherwise(reject); + }) + }) +} + +function createProjectDirectory(project) { + var projectPath = fspath.join(projectsDir,project); + return util.promiseDir(projectPath).then(function() { + return gitTools.initRepo(projectPath) + }); +} + +var defaultFileSet = { + "package.json": function(project) { + return JSON.stringify({ + "name": project.name, + "description": project.summary||"A Node-RED Project", + "version": "0.0.1", + "dependencies": {} + },"",4); + }, + "README.md": function(project) { + return project.name+"\n"+("=".repeat(project.name.length))+"\n\n"+(project.summary||"A Node-RED Project")+"\n\n"; + }, + "settings.json": function() { return "{}" }, + "flow.json": function() { return "[]" }, + "flow_cred.json": function() { return "{}" } +} + +function createDefaultProject(project) { + var projectPath = fspath.join(projectsDir,project.name); + // Create a basic skeleton of a project + var promises = []; + for (var file in defaultFileSet) { + if (defaultFileSet.hasOwnProperty(file)) { + promises.push(util.writeFile(fspath.join(projectPath,file),defaultFileSet[file](project))); + } + } + return when.all(promises); +} +function checkProjectExists(project) { + var projectPath = fspath.join(projectsDir,project); + return nodeFn.call(fs.stat,projectPath).otherwise(function(err) { + var e = new Error("NLD: project not found"); + e.code = "project_not_found"; + throw e; + }); +} +function checkProjectFiles(project) { + var projectPath = fspath.join(projectsDir,project); + var promises = []; + var paths = []; + for (var file in defaultFileSet) { + if (defaultFileSet.hasOwnProperty(file)) { + paths.push(file); + promises.push(nodeFn.call(fs.stat,fspath.join(projectPath,file))); + } + } + return when.settle(promises).then(function(results) { + var missing = []; + results.forEach(function(result,i) { + if (result.state === 'rejected') { + missing.push(paths[i]); + } + }); + return missing; + }).then(function(missing) { + // if (createMissing) { + // var promises = []; + // missing.forEach(function(file) { + // promises.push(util.writeFile(fspath.join(projectPath,file),defaultFileSet[file](project))); + // }); + // return promises; + // } else { + return missing; + // } + }); +} + + +function getFiles(project) { + var projectPath = fspath.join(projectsDir,project); + return nodeFn.call(listFiles,projectPath,"/"); +} +function getFile(project,path) { + +} + +function listFiles(root,path,done) { + var entries = []; + var fullPath = fspath.join(root,path); + fs.readdir(fullPath, function(err,fns) { + var childCount = fns.length; + fns.sort().forEach(function(fn) { + if (fn === ".git") { + childCount--; + return; + } + var child = { + path: fspath.join(path,fn), + name: fn + }; + entries.push(child); + var childFullPath = fspath.join(fullPath,fn); + fs.lstat(childFullPath, function(err, stats) { + if (stats.isDirectory()) { + child.type = 'd'; + listFiles(root,child.path,function(err,children) { + child.children = children; + childCount--; + if (childCount === 0) { + done(null,entries); + } + }) + } else { + child.type = 'f'; + childCount--; + console.log(child,childCount) + if (childCount === 0) { + done(null,entries); + } + } + }); + }); + }); +} + +var activeProject +function getActiveProject() { + return activeProject; +} + +function reloadActiveProject(project) { + return runtime.nodes.stopFlows().then(function() { + return runtime.nodes.loadFlows(true).then(function() { + runtime.events.emit("runtime-event",{id:"project-change",payload:{ project: project}}); + }).otherwise(function(err) { + // We're committed to the project change now, so notify editors + // that it has changed. + runtime.events.emit("runtime-event",{id:"project-change",payload:{ project: project}}); + throw err; + }); + }); +} + +function setActiveProject(project) { + return checkProjectExists(project).then(function() { + activeProject = project; + var globalProjectSettings = settings.get("projects"); + globalProjectSettings.activeProject = project; + return settings.set("projects",globalProjectSettings).then(function() { + var projectPath = fspath.join(projectsDir,project); + flowsFullPath = fspath.join(projectPath,"flow.json"); + flowsFileBackup = getBackupFilename(flowsFullPath); + credentialsFile = fspath.join(projectPath,"flow_cred.json"); + credentialsFileBackup = getBackupFilename(credentialsFile); + + log.info(log._("storage.localfilesystem.changing-project",{project:activeProject||"none"})); + log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath})); + + console.log("Updated file targets to"); + console.log(flowsFullPath) + console.log(credentialsFile) + + return reloadActiveProject(project); + + }) + // return when.promise(function(resolve,reject) { + // console.log("Activating project"); + // resolve(); + // }); + }); +} +function updateProject(project,data) { + return checkProjectExists(project).then(function() { + if (data.credentialSecret) { + // TODO: this path assumes we aren't trying to migrate the secret + return setCredentialSecret(project,data.credentialSecret).then(function() { + return reloadActiveProject(project); + }) + } else if (data.description) { + var projectPath = fspath.join(projectsDir,project); + var readmeFile = fspath.join(projectPath,"README.md"); + return util.writeFile(readmeFile, data.description); + } else if (data.dependencies) { + var projectPath = fspath.join(projectsDir,project); + var packageJSON = fspath.join(projectPath,"package.json"); + return nodeFn.call(fs.readFile,packageJSON,"utf8").then(function(content) { + var package = util.parseJSON(content); + package.dependencies = data.dependencies; + return util.writeFile(packageJSON,JSON.stringify(package,"",4)); + }); + } + }); +} + +var initialFlowLoadComplete = false; + +var flowsFile; +var flowsFullPath; +var flowsFileBackup; +var credentialsFile; +var credentialsFileBackup; + +function getFlows() { + if (!initialFlowLoadComplete) { + initialFlowLoadComplete = true; + log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir})); + log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath})); + log.info(log._("storage.localfilesystem.active-project",{project:activeProject||"none"})); + } + return util.readFile(flowsFullPath,flowsFileBackup,[],'flow'); +} + +function saveFlows(flows) { + if (settings.readOnly) { + return when.resolve(); + } + + try { + fs.renameSync(flowsFullPath,flowsFileBackup); + } catch(err) { + } + + var flowData; + + if (settings.flowFilePretty) { + flowData = JSON.stringify(flows,null,4); + } else { + flowData = JSON.stringify(flows); + } + return util.writeFile(flowsFullPath, flowData); +} + +function getCredentials() { + return util.readFile(credentialsFile,credentialsFileBackup,{},'credentials'); +} + +function saveCredentials(credentials) { + if (settings.readOnly) { + return when.resolve(); + } + + try { + fs.renameSync(credentialsFile,credentialsFileBackup); + } catch(err) { + } + var credentialData; + if (settings.flowFilePretty) { + credentialData = JSON.stringify(credentials,null,4); + } else { + credentialData = JSON.stringify(credentials); + } + return util.writeFile(credentialsFile, credentialData); +} + + +module.exports = { + init: init, + listProjects: listProjects, + getActiveProject: getActiveProject, + setActiveProject: setActiveProject, + getProject: getProject, + createProject: createProject, + updateProject: updateProject, + getFiles: getFiles, + + getFlows: getFlows, + saveFlows: saveFlows, + getCredentials: getCredentials, + saveCredentials: saveCredentials + +}; diff --git a/test/red/api/admin/flow_spec.js b/test/red/api/admin/flow_spec.js index 9e7f69fa2..e39b92da8 100644 --- a/test/red/api/admin/flow_spec.js +++ b/test/red/api/admin/flow_spec.js @@ -23,7 +23,7 @@ var when = require('when'); var flow = require("../../../../red/api/admin/flow"); -describe("flow api", function() { +describe("api/admin/flow", function() { var app; diff --git a/test/red/api/admin/flows_spec.js b/test/red/api/admin/flows_spec.js index d073bca88..57b8de99c 100644 --- a/test/red/api/admin/flows_spec.js +++ b/test/red/api/admin/flows_spec.js @@ -23,7 +23,7 @@ var when = require('when'); var flows = require("../../../../red/api/admin/flows"); -describe("flows api", function() { +describe("api/admin/flows", function() { var app; diff --git a/test/red/api/admin/info_spec.js b/test/red/api/admin/info_spec.js index c9a01683a..1fdb0ba6b 100644 --- a/test/red/api/admin/info_spec.js +++ b/test/red/api/admin/info_spec.js @@ -24,7 +24,7 @@ var app = express(); var info = require("../../../../red/api/admin/info"); var theme = require("../../../../red/api/editor/theme"); -describe("info api", function() { +describe("api/admin/info", function() { describe("settings handler", function() { before(function() { sinon.stub(theme,"settings",function() { return { test: 456 };}); diff --git a/test/red/api/admin/nodes_spec.js b/test/red/api/admin/nodes_spec.js index 67a60090c..a2c9d3924 100644 --- a/test/red/api/admin/nodes_spec.js +++ b/test/red/api/admin/nodes_spec.js @@ -24,7 +24,7 @@ var when = require('when'); var nodes = require("../../../../red/api/admin/nodes"); var apiUtil = require("../../../../red/api/util"); -describe("nodes api", function() { +describe("api/admin/nodes", function() { var app; function initNodes(runtime) { diff --git a/test/red/api/auth/clients_spec.js b/test/red/api/auth/clients_spec.js index 706a98232..9aafc716b 100644 --- a/test/red/api/auth/clients_spec.js +++ b/test/red/api/auth/clients_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var Clients = require("../../../../red/api/auth/clients"); -describe("Clients", function() { +describe("api/auth/clients", function() { it('finds the known editor client',function(done) { Clients.get("node-red-editor").then(function(client) { client.should.have.property("id","node-red-editor"); @@ -41,7 +41,6 @@ describe("Clients", function() { }).catch(function(err) { done(err); }); - + }); }); - \ No newline at end of file diff --git a/test/red/api/auth/index_spec.js b/test/red/api/auth/index_spec.js index 3abfa4185..9849b602c 100644 --- a/test/red/api/auth/index_spec.js +++ b/test/red/api/auth/index_spec.js @@ -24,7 +24,7 @@ var auth = require("../../../../red/api/auth"); var Users = require("../../../../red/api/auth/users"); var Tokens = require("../../../../red/api/auth/tokens"); -describe("api auth middleware",function() { +describe("api/auth/index",function() { diff --git a/test/red/api/auth/permissions_spec.js b/test/red/api/auth/permissions_spec.js index 35e3b74f9..52960096f 100644 --- a/test/red/api/auth/permissions_spec.js +++ b/test/red/api/auth/permissions_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var permissions = require("../../../../red/api/auth/permissions"); -describe("Auth permissions", function() { +describe("api/auth/permissions", function() { describe("hasPermission", function() { it('a user with no permissions',function() { permissions.hasPermission([],"*").should.be.false(); diff --git a/test/red/api/auth/strategies_spec.js b/test/red/api/auth/strategies_spec.js index 1fb246d1e..db8e290bf 100644 --- a/test/red/api/auth/strategies_spec.js +++ b/test/red/api/auth/strategies_spec.js @@ -23,7 +23,7 @@ var Users = require("../../../../red/api/auth/users"); var Tokens = require("../../../../red/api/auth/tokens"); var Clients = require("../../../../red/api/auth/clients"); -describe("Auth strategies", function() { +describe("api/auth/strategies", function() { before(function() { strategies.init({log:{audit:function(){}}}) }); diff --git a/test/red/api/auth/tokens_spec.js b/test/red/api/auth/tokens_spec.js index 2d11c4f93..a9c5a7831 100644 --- a/test/red/api/auth/tokens_spec.js +++ b/test/red/api/auth/tokens_spec.js @@ -21,7 +21,7 @@ var sinon = require("sinon"); var Tokens = require("../../../../red/api/auth/tokens"); -describe("Tokens", function() { +describe("api/auth/tokens", function() { describe("#init",function() { it('loads sessions', function(done) { Tokens.init({}).then(done); diff --git a/test/red/api/auth/users_spec.js b/test/red/api/auth/users_spec.js index 5bd8001f8..23c34001d 100644 --- a/test/red/api/auth/users_spec.js +++ b/test/red/api/auth/users_spec.js @@ -20,7 +20,7 @@ var sinon = require('sinon'); var Users = require("../../../../red/api/auth/users"); -describe("Users", function() { +describe("api/auth/users", function() { describe('Initalised with a credentials object, no anon',function() { before(function() { Users.init({ diff --git a/test/red/api/editor/projects/index_spec.js b/test/red/api/editor/projects/index_spec.js new file mode 100644 index 000000000..de22781ae --- /dev/null +++ b/test/red/api/editor/projects/index_spec.js @@ -0,0 +1,19 @@ +/** + * 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. + **/ + +describe("api/editor/projects", function() { + it.skip("NEEDS TESTS WRITING",function() {}); +}); diff --git a/test/red/runtime/nodes/credentials_spec.js b/test/red/runtime/nodes/credentials_spec.js index 3226c288a..c5b4b3941 100644 --- a/test/red/runtime/nodes/credentials_spec.js +++ b/test/red/runtime/nodes/credentials_spec.js @@ -424,8 +424,11 @@ describe('red/runtime/nodes/credentials', function() { var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { - credentials.dirty().should.be.true(); - should.not.exist(credentials.get("node")); + // credentials.dirty().should.be.true(); + // should.not.exist(credentials.get("node")); + done(); + }).otherwise(function(err) { + err.should.have.property('code','credentials_load_failed'); done(); }); }); @@ -437,8 +440,11 @@ describe('red/runtime/nodes/credentials', function() { var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { - credentials.dirty().should.be.true(); - should.not.exist(credentials.get("node")); + // credentials.dirty().should.be.true(); + // should.not.exist(credentials.get("node")); + done(); + }).otherwise(function(err) { + err.should.have.property('code','credentials_load_failed'); done(); }); }); diff --git a/test/red/runtime/storage/localfilesystem/index_spec.js b/test/red/runtime/storage/localfilesystem/index_spec.js index ac77a213c..92d949c9c 100644 --- a/test/red/runtime/storage/localfilesystem/index_spec.js +++ b/test/red/runtime/storage/localfilesystem/index_spec.js @@ -21,6 +21,12 @@ var path = require('path'); var localfilesystem = require("../../../../../red/runtime/storage/localfilesystem"); describe('storage/localfilesystem', function() { + var mockRuntime = { + log:{ + _:function() { return "placeholder message"}, + info: function() { } + } + }; var userDir = path.join(__dirname,".testUserHome"); var testFlow = [{"type":"tab","id":"d8be2a6d.2741d8","label":"Sheet 1"}]; beforeEach(function(done) { @@ -33,7 +39,7 @@ describe('storage/localfilesystem', function() { }); it('should initialise the user directory',function(done) { - localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.init({userDir:userDir}, mockRuntime).then(function() { fs.existsSync(path.join(userDir,"lib")).should.be.true(); fs.existsSync(path.join(userDir,"lib",'flows')).should.be.true(); done(); @@ -49,7 +55,7 @@ describe('storage/localfilesystem', function() { fs.mkdirSync(process.env.NODE_RED_HOME); fs.writeFileSync(path.join(process.env.NODE_RED_HOME,".config.json"),"{}","utf8"); var settings = {}; - localfilesystem.init(settings).then(function() { + localfilesystem.init(settings, mockRuntime).then(function() { try { fs.existsSync(path.join(process.env.NODE_RED_HOME,"lib")).should.be.true(); fs.existsSync(path.join(process.env.NODE_RED_HOME,"lib",'flows')).should.be.true(); @@ -74,7 +80,7 @@ describe('storage/localfilesystem', function() { fs.mkdirSync(path.join(process.env.HOMEPATH,".node-red")); fs.writeFileSync(path.join(process.env.HOMEPATH,".node-red",".config.json"),"{}","utf8"); var settings = {}; - localfilesystem.init(settings).then(function() { + localfilesystem.init(settings, mockRuntime).then(function() { try { fs.existsSync(path.join(process.env.HOMEPATH,".node-red","lib")).should.be.true(); fs.existsSync(path.join(process.env.HOMEPATH,".node-red","lib",'flows')).should.be.true(); @@ -101,7 +107,7 @@ describe('storage/localfilesystem', function() { fs.mkdirSync(process.env.HOME); var settings = {}; - localfilesystem.init(settings).then(function() { + localfilesystem.init(settings, mockRuntime).then(function() { try { fs.existsSync(path.join(process.env.HOME,".node-red","lib")).should.be.true(); fs.existsSync(path.join(process.env.HOME,".node-red","lib",'flows')).should.be.true(); @@ -131,7 +137,7 @@ describe('storage/localfilesystem', function() { fs.mkdirSync(process.env.USERPROFILE); var settings = {}; - localfilesystem.init(settings).then(function() { + localfilesystem.init(settings, mockRuntime).then(function() { try { fs.existsSync(path.join(process.env.USERPROFILE,".node-red","lib")).should.be.true(); fs.existsSync(path.join(process.env.USERPROFILE,".node-red","lib",'flows')).should.be.true(); @@ -151,7 +157,7 @@ describe('storage/localfilesystem', function() { }); it('should handle missing flow file',function(done) { - localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.init({userDir:userDir}, mockRuntime).then(function() { var flowFile = 'flows_'+require('os').hostname()+'.json'; var flowFilePath = path.join(userDir,flowFile); fs.existsSync(flowFilePath).should.be.false(); @@ -167,7 +173,7 @@ describe('storage/localfilesystem', function() { }); it('should handle empty flow file, no backup',function(done) { - localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.init({userDir:userDir}, mockRuntime).then(function() { var flowFile = 'flows_'+require('os').hostname()+'.json'; var flowFilePath = path.join(userDir,flowFile); var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup"); @@ -185,7 +191,7 @@ describe('storage/localfilesystem', function() { }); it('should handle empty flow file, restores backup',function(done) { - localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.init({userDir:userDir}, mockRuntime).then(function() { var flowFile = 'flows_'+require('os').hostname()+'.json'; var flowFilePath = path.join(userDir,flowFile); var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup"); @@ -208,7 +214,7 @@ describe('storage/localfilesystem', function() { }); it('should save flows to the default file',function(done) { - localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.init({userDir:userDir}, mockRuntime).then(function() { var flowFile = 'flows_'+require('os').hostname()+'.json'; var flowFilePath = path.join(userDir,flowFile); var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup"); @@ -237,7 +243,7 @@ describe('storage/localfilesystem', function() { var flowFile = 'test.json'; var flowFilePath = path.join(userDir,flowFile); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}, mockRuntime).then(function() { fs.existsSync(defaultFlowFilePath).should.be.false(); fs.existsSync(flowFilePath).should.be.false(); @@ -261,7 +267,7 @@ describe('storage/localfilesystem', function() { it('should format the flows file when flowFilePretty specified',function(done) { var flowFile = 'test.json'; var flowFilePath = path.join(userDir,flowFile); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath,flowFilePretty:true}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath,flowFilePretty:true}, mockRuntime).then(function() { localfilesystem.saveFlows(testFlow).then(function() { var content = fs.readFileSync(flowFilePath,"utf8"); content.split("\n").length.should.be.above(1); @@ -286,7 +292,7 @@ describe('storage/localfilesystem', function() { var flowFilePath = path.join(userDir,flowFile); var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup"); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}, mockRuntime).then(function() { fs.existsSync(defaultFlowFilePath).should.be.false(); fs.existsSync(flowFilePath).should.be.false(); fs.existsSync(flowFileBackupPath).should.be.false(); @@ -326,7 +332,7 @@ describe('storage/localfilesystem', function() { var flowFile = 'test.json'; var flowFilePath = path.join(userDir,flowFile); var credFile = path.join(userDir,"test_cred.json"); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}, mockRuntime).then(function() { fs.existsSync(credFile).should.be.false(); localfilesystem.getCredentials().then(function(creds) { @@ -345,7 +351,7 @@ describe('storage/localfilesystem', function() { var flowFilePath = path.join(userDir,flowFile); var credFile = path.join(userDir,"test_cred.json"); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}, mockRuntime).then(function() { fs.existsSync(credFile).should.be.false(); @@ -374,7 +380,7 @@ describe('storage/localfilesystem', function() { var credFile = path.join(userDir,"test_cred.json"); var credFileBackup = path.join(userDir,".test_cred.json.backup"); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}, mockRuntime).then(function() { fs.writeFileSync(credFile,"{}","utf8"); @@ -400,7 +406,7 @@ describe('storage/localfilesystem', function() { var flowFilePath = path.join(userDir,flowFile); var credFile = path.join(userDir,"test_cred.json"); - localfilesystem.init({userDir:userDir, flowFile:flowFilePath, flowFilePretty:true}).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFilePath, flowFilePretty:true}, mockRuntime).then(function() { fs.existsSync(credFile).should.be.false(); diff --git a/red/runtime/storage/projects/index.js b/test/red/runtime/storage/localfilesystem/projects/git/index_spec.js similarity index 100% rename from red/runtime/storage/projects/index.js rename to test/red/runtime/storage/localfilesystem/projects/git/index_spec.js diff --git a/test/red/runtime/storage/localfilesystem/projects/index_spec.js b/test/red/runtime/storage/localfilesystem/projects/index_spec.js new file mode 100644 index 000000000..e69de29bb