diff --git a/editor/js/main.js b/editor/js/main.js index 13d031312..344983375 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -69,10 +69,15 @@ return; } if (msg.text) { + console.log(msg); var text = RED._(msg.text,{default:msg.text}); if (notificationId === "runtime-state") { if (msg.error === "credentials_load_failed") { + // TODO: NLS text += '

'+'Setup credentials'+'

'; + } else if (msg.error === "missing_flow_file") { + // TODO: NLS + text += '

'+'Setup project files'+'

'; } } if (!persistentNotifications.hasOwnProperty(notificationId)) { diff --git a/editor/js/ui/common/popover.js b/editor/js/ui/common/popover.js index f35179bd4..6a0bc9fdf 100644 --- a/editor/js/ui/common/popover.js +++ b/editor/js/ui/common/popover.js @@ -33,6 +33,7 @@ RED.popover = (function() { var trigger = options.trigger; var content = options.content; var delay = options.delay; + var autoClose = options.autoClose; var width = options.width||"auto"; var size = options.size||"default"; if (!deltaSizes[size]) { @@ -92,7 +93,6 @@ RED.popover = (function() { } if (trigger === 'hover') { - target.on('mouseenter',function(e) { clearTimeout(timer); active = true; @@ -116,6 +116,11 @@ RED.popover = (function() { openPopup(); } }); + } else if (autoClose) { + setTimeout(function() { + active = false; + closePopup(); + },autoClose); } var res = { setContent: function(_content) { diff --git a/editor/js/ui/projectSettings.js b/editor/js/ui/projectSettings.js index 8dd0f770f..08015664e 100644 --- a/editor/js/ui/projectSettings.js +++ b/editor/js/ui/projectSettings.js @@ -170,7 +170,7 @@ RED.projects.settings = (function() { updateProjectSummary(activeProject.summary, container); editButton.show(); }); - $('') + $('') .appendTo(bg) .click(function(evt) { evt.preventDefault(); @@ -422,211 +422,517 @@ RED.projects.settings = (function() { } - function createSettingsPane(activeProject) { - var pane = $('
'); - $('

').text("Credentials").appendTo(pane); - var row = $('
').appendTo(pane); - if (activeProject.settings.credentialsEncrypted) { - $(' Credentials are encrypted').appendTo(row); - } else { - $(' Credentials are not encrypted').appendTo(row); - } - var resetButton; - var action; - var changeButton = $('') - .text(activeProject.settings.credentialsEncrypted?"Change key":"Enable encryption") - .appendTo(row) - .click(function(evt) { - evt.preventDefault(); - newKey.val(""); - if (currentKey) { - currentKey.val(""); - currentKey.removeClass("input-error"); - } - checkInputs(); - saveButton.text("Save"); + function showProjectFileListing(row,activeProject,current,done) { + var dialog; + var dialogBody; + var filesList; + var selected; + var container = $('
',{style:"position: relative; min-height: 175px; height: 175px;"}).appendTo(row); + var spinner = addSpinnerOverlay(container); - $(".project-settings-credentials-row").show(); - $(".project-settings-credentials-current-row").show(); - $(this).prop('disabled',true); - if (resetButton) { - resetButton.prop('disabled',true); - } - action = 'change'; + $.getJSON("/projects/"+activeProject.name+"/files",function(result) { + var fileNames = Object.keys(result); + var files = {}; + fileNames.sort(); + fileNames.forEach(function(file) { + file.split("/").reduce(function(r,v,i,arr) { if (v) { if (i') - .text("Reset key") - .appendTo(row) - .click(function(evt) { - evt.preventDefault(); - newKey.val(""); - if (currentKey) { - currentKey.val(""); - currentKey.removeClass("input-error"); + var sortFiles = function(key,value,fullPath) { + var result = { + name: key||"/", + path: fullPath+(fullPath?"/":"")+key, + }; + if (value === true) { + result.type = 'f'; + return result; + } + result.type = 'd'; + result.children = []; + result.path = result.path; + var files = Object.keys(value); + files.forEach(function(file) { + result.children.push(sortFiles(file,value[file],result.path)); + }) + result.children.sort(function(A,B) { + if (A.hasOwnProperty("children") && !B.hasOwnProperty("children")) { + return -1; + } else if (!A.hasOwnProperty("children") && B.hasOwnProperty("children")) { + return 1; } - checkInputs(); - saveButton.text("Reset key"); - - $(".project-settings-credentials-row").show(); - $(".project-settings-credentials-reset-row").show(); - - $(this).prop('disabled',true); - changeButton.prop('disabled',true); - action = 'reset'; - }); - } - - if (activeProject.settings.credentialSecretInvalid) { - row = $('
').appendTo(pane); - $('
The current key is not valid. Set the correct key or reset credentials.
').appendTo(row); - } - - var credentialsContainer = $('
',{style:"position:relative"}).appendTo(pane); - var currentKey; - var newKey; - - var checkInputs = function() { - var valid = true; - if (newKey.val().length === 0) { - valid = false; + return A.name.localeCompare(B.name); + }) + return result; } - if (currentKey && currentKey.val() === 0) { - valid = false; - } - saveButton.toggleClass('disabled',!valid); - } + var files = sortFiles("",files,""); + createFileSubList(container,files.children,current,done,"height: 175px"); + spinner.remove(); + }); + } + function createFileSubList(container, files, current, onselect, style) { + style = style || ""; + var list = $('
    ',{class:"projects-dialog-file-list", style:style}).appendTo(container).editableList({ + addButton: false, + scrollOnAdd: false, + addItem: function(row,index,entry) { + var header = $('
    ',{class:"projects-dialog-file-list-entry"}).appendTo(row); + if (entry.children) { + $(' ').appendTo(header); + if (entry.children.length > 0) { + var children = $('
    ',{style:"padding-left: 20px;"}).appendTo(row); + if (current.indexOf(entry.path+"/") === 0) { + header.addClass("expanded"); + } else { + children.hide(); + } + createFileSubList(children,entry.children,current,onselect); + header.addClass("selectable"); + header.click(function(e) { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + children.slideUp(200); + } else { + $(this).addClass("expanded"); + children.slideDown(200); + } + + }); - if (activeProject.settings.credentialsEncrypted) { - if (!activeProject.settings.credentialSecretInvalid) { - row = $('').appendTo(credentialsContainer); - $('').appendTo(row); - currentKey = $('').appendTo(row); - currentKey.on("change keyup paste",function() { - if (popover) { - popover.close(); - popover = null; - $(this).removeClass('input-error'); } - checkInputs(); - }); + } else { + var fileIcon = "fa-file-o"; + var fileClass = ""; + if (/\.json$/i.test(entry.name)) { + fileIcon = "fa-file-code-o" + } else if (/\.md$/i.test(entry.name)) { + fileIcon = "fa-book"; + } else if (/^\.git/i.test(entry.name)) { + fileIcon = "fa-code-fork"; + header.addClass("projects-dialog-file-list-entry-file-type-git"); + } + $(' ').appendTo(header); + header.addClass("selectable"); + if (entry.path === current) { + header.addClass("selected"); + } + header.click(function(e) { + $(".projects-dialog-file-list-entry.selected").removeClass("selected"); + $(this).addClass("selected"); + onselect(entry.path); + }) + header.dblclick(function(e) { + e.preventDefault(); + onselect(entry.path,true); + }) + } + $('').text(entry.name).appendTo(header); } - row = $('').appendTo(credentialsContainer); - $('
    Resetting the key will delete all existing credentials
    ').appendTo(row); - + }); + if (!style) { + list.parent().css("overflow-y",""); } - // $('').appendTo(row); + files.forEach(function(f) { + list.editableList('addItem',f); + }) + } - row = $('').appendTo(credentialsContainer); - $('').text((activeProject.settings.credentialsEncrypted&& !activeProject.settings.credentialSecretInvalid)?"New key":"Encryption key").appendTo(row); - newKey = $('').appendTo(row).on("change keyup paste",checkInputs); + // function editFiles(activeProject, container,flowFile, flowFileLabel) { + // var editButton = container.children().first(); + // editButton.hide(); + // + // var flowFileInput = $('').val(flowFile).insertAfter(flowFileLabel); + // + // var flowFileInputSearch = $('') + // .insertAfter(flowFileInput) + // .click(function(e) { + // showProjectFileListing(activeProject,'Select flow file',flowFileInput.val(),function(result) { + // flowFileInput.val(result); + // checkFiles(); + // }) + // }) + // + // var checkFiles = function() { + // saveButton.toggleClass('disabled',flowFileInput.val()===""); + // saveButton.prop('disabled',flowFileInput.val()===""); + // } + // flowFileInput.on("change keyup paste",checkFiles); + // flowFileLabel.hide(); + // + // var bg = $('').prependTo(container); + // $('') + // .appendTo(bg) + // .click(function(evt) { + // evt.preventDefault(); + // + // flowFileLabel.show(); + // flowFileInput.remove(); + // flowFileInputSearch.remove(); + // bg.remove(); + // editButton.show(); + // }); + // var saveButton = $('') + // .appendTo(bg) + // .click(function(evt) { + // evt.preventDefault(); + // var newFlowFile = flowFileInput.val(); + // var newCredsFile = credentialsFileInput.val(); + // var spinner = addSpinnerOverlay(container); + // var done = function(err,res) { + // if (err) { + // spinner.remove(); + // return; + // } + // activeProject.summary = v; + // spinner.remove(); + // flowFileLabel.text(newFlowFile); + // flowFileLabel.show(); + // flowFileInput.remove(); + // flowFileInputSearch.remove(); + // bg.remove(); + // editButton.show(); + // } + // // utils.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); + // // } + // // }, + // // } + // // },{summary:v}); + // }); + // + // + // checkFiles(); + // + // } - row = $('').appendTo(credentialsContainer); - var bg = $('
    ').appendTo(row); - $('') - .appendTo(bg) + function createFilesSection(activeProject,pane) { + var title = $('

    ').text("Files").appendTo(pane); + var filesContainer = $('').appendTo(pane); + var editButton = $('') + .appendTo(title) .click(function(evt) { evt.preventDefault(); + formButtons.show(); + editButton.hide(); + flowFileLabelText.hide(); + flowFileInput.show(); + flowFileInputSearch.show(); + credFileLabel.hide(); + credFileInput.show(); + flowFileInput.focus(); + // credentialStateLabel.parent().hide(); + credentialStateLabel.addClass("uneditable-input"); + $(".user-settings-row-credentials").show(); + credentialStateLabel.css('height','auto'); + credentialFormRows.hide(); + credentialSecretButtons.show(); + }); + + var row; + + // Flow files + row = $('').appendTo(filesContainer); + $('').text('Flow').appendTo(row); + var flowFileLabel = $('
    ').appendTo(row); + var flowFileLabelText = $('').text(activeProject.files.flow).appendTo(flowFileLabel); + + var flowFileInput = $('').val(activeProject.files.flow).hide().appendTo(flowFileLabel); + var flowFileInputSearch = $('') + .hide() + .appendTo(flowFileLabel) + .click(function(e) { + if ($(this).hasClass('selected')) { + $(this).removeClass('selected'); + flowFileLabel.find('.project-file-listing-container').remove(); + flowFileLabel.css('height',''); + flowFileLabel.css('color',''); + + } else { + $(this).addClass('selected'); + flowFileLabel.css('height','auto'); + flowFileLabel.css('color','inherit'); + showProjectFileListing(flowFileLabel,activeProject,flowFileInput.val(),function(result,isDblClick) { + if (result) { + flowFileInput.val(result); + } + if (isDblClick) { + $(flowFileInputSearch).click(); + } + checkFiles(); + }) + } + }) + + row = $('').appendTo(filesContainer); + $('').text('Credentials').appendTo(row); + var credFileLabel = $('
    ').text(activeProject.files.credentials).appendTo(row); + var credFileInput = $('
    ').text(activeProject.files.credentials).hide().insertAfter(credFileLabel); + + var checkFiles = function() { + var saveDisabled; + var currentFlowValue = flowFileInput.val(); + var m = /^(.+?)(\.[^.]*)?$/.exec(currentFlowValue); + if (m) { + credFileInput.text(m[1]+"_cred"+(m[2]||".json")); + } else if (currentFlowValue === "") { + credFileInput.text(""); + } + var isFlowInvalid = currentFlowValue==="" || + /\.\./.test(currentFlowValue) || + /\/$/.test(currentFlowValue); + + saveDisabled = isFlowInvalid || credFileInput.text()===""; + + if (credentialSecretExistingInput.is(":visible")) { + credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === ""); + saveDisabled = saveDisabled || credentialSecretExistingInput.val() === ""; + } + if (credentialSecretNewInput.is(":visible")) { + credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === ""); + saveDisabled = saveDisabled || credentialSecretNewInput.val() === ""; + } + + + flowFileInput.toggleClass("input-error", isFlowInvalid); + credFileInput.toggleClass("input-error",credFileInput.text()===""); + saveButton.toggleClass('disabled',saveDisabled); + saveButton.prop('disabled',saveDisabled); + } + flowFileInput.on("change keyup paste",checkFiles); + + + if (!activeProject.files.flow) { + $(' Missing').appendTo(flowFileLabelText); + } + if (!activeProject.files.credentials) { + $(' Missing').appendTo(credFileLabel); + } + + + row = $('').appendTo(filesContainer); + + $('').appendTo(row); + var credentialStateLabel = $(' ').appendTo(row); + var credentialSecretButtons = $('').hide().appendTo(row); + + credentialStateLabel.css('color','#666'); + credentialSecretButtons.css('vertical-align','top'); + var credentialSecretResetButton = $('') + .appendTo(credentialSecretButtons) + .click(function(e) { + e.preventDefault(); + if (!$(this).hasClass('selected')) { + credentialSecretNewInput.val(""); + credentialSecretExistingRow.hide(); + credentialSecretNewRow.show(); + $(this).addClass("selected"); + credentialSecretEditButton.removeClass("selected"); + credentialResetLabel.show(); + credentialResetWarning.show(); + credentialSetLabel.hide(); + credentialChangeLabel.hide(); + + credentialFormRows.show(); + } else { + $(this).removeClass("selected"); + credentialFormRows.hide(); + } + checkFiles(); + }); + var credentialSecretEditButton = $('') + .appendTo(credentialSecretButtons) + .click(function(e) { + e.preventDefault(); + if (!$(this).hasClass('selected')) { + credentialSecretExistingInput.val(""); + credentialSecretNewInput.val(""); + if (activeProject.settings.credentialSecretInvalid || !activeProject.settings.credentialsEncrypted) { + credentialSetLabel.show(); + credentialChangeLabel.hide(); + credentialSecretExistingRow.hide(); + } else { + credentialSecretExistingRow.show(); + credentialSetLabel.hide(); + credentialChangeLabel.show(); + } + credentialSecretNewRow.show(); + credentialSecretEditButton.addClass("selected"); + credentialSecretResetButton.removeClass("selected"); + + credentialResetLabel.hide(); + credentialResetWarning.hide(); + credentialFormRows.show(); + } else { + $(this).removeClass("selected"); + credentialFormRows.hide(); + } + checkFiles(); + }) + + + row = $('').hide().appendTo(filesContainer); + + + + var credentialFormRows = $('
    ',{style:"margin-top:10px"}).hide().appendTo(credentialStateLabel); + + var credentialSetLabel = $('
    Set the encryption key:
    ').hide().appendTo(credentialFormRows); + var credentialChangeLabel = $('
    Change the encryption key:
    ').hide().appendTo(credentialFormRows); + var credentialResetLabel = $('
    Reset the encryption key:
    ').hide().appendTo(credentialFormRows); + + var credentialSecretExistingRow = $('').appendTo(credentialFormRows); + $('').text('Current key').appendTo(credentialSecretExistingRow); + var credentialSecretExistingInput = $('').appendTo(credentialSecretExistingRow) + .on("change keyup paste",function() { if (popover) { popover.close(); popover = null; } - changeButton.prop('disabled',false); - if (resetButton) { - resetButton.prop('disabled',false); - } - $(".project-settings-credentials-row").hide(); - $(".project-settings-credentials-current-row").hide(); - $(".project-settings-credentials-reset-row").hide(); + checkFiles(); }); - var saveButton = $('') - .text("Save") - .appendTo(bg) + + var credentialSecretNewRow = $('').appendTo(credentialFormRows); + + + $('').text('New key').appendTo(credentialSecretNewRow); + var credentialSecretNewInput = $('').appendTo(credentialSecretNewRow).on("change keyup paste",checkFiles); + + var credentialResetWarning = $('
    This will delete all existing credentials
    ').hide().appendTo(credentialFormRows); + + + var hideEditForm = function() { + editButton.show(); + formButtons.hide(); + flowFileLabelText.show(); + flowFileInput.hide(); + flowFileInputSearch.hide(); + credFileLabel.show(); + credFileInput.hide(); + // credentialStateLabel.parent().show(); + credentialStateLabel.removeClass("uneditable-input"); + credentialStateLabel.css('height',''); + + $(".user-settings-row-credentials").hide(); + credentialFormRows.hide(); + credentialSecretButtons.hide(); + credentialSecretResetButton.removeClass("selected"); + credentialSecretEditButton.removeClass("selected"); + + + } + + var formButtons = $('').hide().appendTo(filesContainer); + var cancelButton = $('') + .appendTo(formButtons) .click(function(evt) { evt.preventDefault(); - if ($(this).hasClass('disabled')) { - return; - } - var spinner = addSpinnerOverlay(credentialsContainer); - var payload = { - credentialSecret: newKey.val() - }; - if (activeProject.settings.credentialSecretInvalid) { - RED.deploy.setDeployInflight(true); - } - - if (activeProject.settings.credentialsEncrypted) { - if (action === 'reset') { - payload.resetCredentialSecret = true; - } else if (!activeProject.settings.credentialSecretInvalid) { - payload.currentCredentialSecret = currentKey.val(); - } - } - var done = function(err,res) { + hideEditForm(); + }); + var saveButton = $('') + .appendTo(formButtons) + .click(function(evt) { + evt.preventDefault(); + var spinner = addSpinnerOverlay(filesContainer); + var done = function(err) { spinner.remove(); if (err) { console.log(err); return; } + flowFileLabelText.text(flowFileInput.val()); + credFileLabel.text(credFileInput.text()); + hideEditForm(); } + var payload = { + files: { + flow: flowFileInput.val(), + credentials: credFileInput.text() + } + } + + if (credentialSecretResetButton.hasClass('selected')) { + payload.resetCredentialSecret = true; + } + if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) { + payload.credentialSecret = credentialSecretNewInput.val(); + if (credentialSecretExistingInput.is(":visible")) { + payload.currentCredentialSecret = credentialSecretExistingInput.val(); + } + } + + // console.log(JSON.stringify(payload,null,4)); + RED.deploy.setDeployInflight(true); utils.sendRequest({ url: "projects/"+activeProject.name, type: "PUT", responses: { 0: function(error) { - done(error,null); + done(error); }, 200: function(data) { - if (popover) { - popover.close(); - popover = null; - } - changeButton.prop('disabled',false); - if (resetButton) { - resetButton.prop('disabled',false); - } - $(".project-settings-credentials-row").hide(); - $(".project-settings-credentials-current-row").hide(); - $(".project-settings-credentials-reset-row").hide(); + activeProject = data; + console.log("updating form"); + updateForm(); + done(); }, 400: { + 'credentials_load_failed': function(error) { + done(error); + }, 'unexpected_error': function(error) { - done(error,null); + console.log(error); + done(error); }, 'missing_current_credential_key': function(error) { - currentKey.addClass("input-error"); + credentialSecretExistingInput.addClass("input-error"); popover = RED.popover.create({ - target: currentKey, + target: credentialSecretExistingInput, direction: 'right', size: 'small', - content: "Incorrect key" + content: "Incorrect key", + autoClose: 3000 }).open(); - done(); + done(error); } }, } },payload).always(function() { - if (activeProject.settings.credentialSecretInvalid) { - RED.deploy.setDeployInflight(false); - } + RED.deploy.setDeployInflight(false); }); + + + }); + var updateForm = function() { + if (activeProject.settings.credentialSecretInvalid) { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-warning"); + credentialStateLabel.find(".user-settings-credentials-state").text("Invalid encryption key"); + } else if (activeProject.settings.credentialsEncrypted) { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-lock"); + credentialStateLabel.find(".user-settings-credentials-state").text("Encryption enabled"); + } else { + credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-unlock"); + credentialStateLabel.find(".user-settings-credentials-state").text("Encryption disabled"); + } + credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialsEncrypted); + credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialsEncrypted); + } - - - // $('

    ').text("Credentials").appendTo(pane); - // row = $('').appendTo(pane); - // $(' Credentials are not encrypted').appendTo(row); - // $('').appendTo(row); - - - $('

    ').text("Repository").appendTo(pane); - row = $('').appendTo(pane); - var input; - $('').appendTo(row); - $('').appendTo(row); - - + checkFiles(); + updateForm(); + } + function createSettingsPane(activeProject) { + var pane = $('
    '); + createFilesSection(activeProject,pane); return pane; } diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 73f8ee418..4e946c4db 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -933,6 +933,9 @@ function refresh() { showCredentialsPrompt: function() { //TODO: rename this function RED.projects.settings.show('settings'); }, + showFilesPrompt: function() { //TODO: rename this function + RED.projects.settings.show('settings'); + }, // showSidebar: showSidebar, refresh: refresh, editProject: function() { diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss index ef76f3333..d7467cca6 100644 --- a/editor/sass/projects.scss +++ b/editor/sass/projects.scss @@ -371,3 +371,76 @@ transition: all 0.2s ease-in-out; } } +.project-file-listing-container > .red-ui-editableList > .red-ui-editableList-border { + border-radius: 0; + border: none; + border-top: 1px solid $secondary-border-color; + +} +.red-ui-editableList-container .projects-dialog-file-list { + .red-ui-editableList-border { + border: none; + } + li { + padding: 0 !important; + border: none; + } + .red-ui-editableList-container { + padding: 0; + } +} +.projects-dialog-file-list-entry { + padding: 3px 0; + border-left: 2px solid #fff; + border-right: 2px solid #fff; + &.projects-list-entry-current { + &:not(.selectable) { + background: #f9f9f9; + } + i { + color: #999; + } + } + &.selectable { + cursor: pointer; + &:hover { + background: #f3f3f3; + border-left-color:#999; + border-right-color:#999; + } + } + + i { + color: #999; + width: 16px; + text-align: center; + } + &.selected { + background: #efefef; + border-left-color:#999; + border-right-color:#999; + } + span { + display: inline-block; + vertical-align:middle; + } + .projects-dialog-file-list-entry-folder { + margin: 0 10px 0 0px; + + .fa-angle-right { + color: #333; + transition: all 0.2s ease-in-out; + + } + } + .projects-dialog-file-list-entry-file { + margin: 0 10px 0 20px; + } + .projects-dialog-file-list-entry-name { + font-size: 1em; + } + &.expanded .fa-angle-right { + transform: rotate(90deg); + } +} +.projects-dialog-file-list-entry-file-type-git { color: #999 } diff --git a/editor/sass/userSettings.scss b/editor/sass/userSettings.scss index 0fe07409e..9a00fa81b 100644 --- a/editor/sass/userSettings.scss +++ b/editor/sass/userSettings.scss @@ -43,8 +43,11 @@ padding-bottom: 0; } } - input { - margin-bottom: 0; + input, div.uneditable-input { + //margin-bottom: 0; + } + div.uneditable-input { + position: relative; } input[type='number'] { width: 60px; @@ -57,3 +60,14 @@ .user-settings-row { padding: 5px 10px 2px; } +.user-settings-section { + position: relative; + &:after { + content: ""; + display: table; + clear: both; + } + .uneditable-input, input { + width: calc(100% - 150px); + } +} diff --git a/red/api/editor/comms.js b/red/api/editor/comms.js index 08c285788..a311e8fb5 100644 --- a/red/api/editor/comms.js +++ b/red/api/editor/comms.js @@ -33,6 +33,7 @@ function handleStatus(event) { publish("status/"+event.id,event.status,true); } function handleRuntimeEvent(event) { + log.trace("runtime event: "+JSON.stringify(event)); publish("notification/"+event.id,event.payload||{},event.retain); } function init(_server,runtime) { diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index 7ed7f574f..249afb37f 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -85,7 +85,8 @@ "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", - "invalid-credentials-secret": "Flows stopped due to missing or invalid credentialSecret" + "credentials_load_failed": "Flows stopped due to missing or invalid credentialSecret", + "missing_flow_file": "Could not find the project flow file" }, "error": "Error: __message__", diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js index 270278a76..59f83db9a 100644 --- a/red/api/editor/projects/index.js +++ b/red/api/editor/projects/index.js @@ -77,14 +77,15 @@ module.exports = { } }) } else { - res.redirect(303,req.baseUrl + '/'); + res.redirect(303,req.baseUrl + '/'+ req.params.id); } } else if (req.body.hasOwnProperty('credentialSecret') || req.body.hasOwnProperty('description') || req.body.hasOwnProperty('dependencies')|| - req.body.hasOwnProperty('summary')) { + req.body.hasOwnProperty('summary') || + req.body.hasOwnProperty('files')) { runtime.storage.projects.updateProject(req.params.id, req.body).then(function() { - res.redirect(303,req.baseUrl + '/'); + res.redirect(303,req.baseUrl + '/'+ req.params.id); }).catch(function(err) { if (err.code) { res.status(400).json({error:err.code, message: err.message}); @@ -92,6 +93,8 @@ module.exports = { res.status(400).json({error:"unexpected_error", message:err.toString()}); } }) + } else { + res.status(400).json({error:"unexpected_error", message:"invalid_request"}); } }); diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index e05ae56f7..26c76cd30 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -78,7 +78,7 @@ function loadFlows() { }); }).catch(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}); + events.emit("runtime-event",{id:"runtime-state",payload:{type:"warning",error:err.code,text:"notification.warnings."+err.code},retain:true}); log.warn(log._("nodes.flows.error",{message:err.toString()})); throw err; }); diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 973d51205..3c203ea4e 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -49,11 +49,11 @@ Project.prototype.load = function () { this.credentialSecret = projectSettings.credentialSecret; - this.paths.flowFile = fspath.join(this.path,"flow.json"); - this.paths.credentialsFile = fspath.join(this.path,"flow_cred.json"); + // this.paths.flowFile = fspath.join(this.path,"flow.json"); + // this.paths.credentialsFile = fspath.join(this.path,"flow_cred.json"); var promises = []; - return checkProjectFiles(project.name).then(function(missingFiles) { + return checkProjectFiles(project).then(function(missingFiles) { if (missingFiles.length > 0) { project.missingFiles = missingFiles; } @@ -61,7 +61,18 @@ Project.prototype.load = function () { project.paths['package.json'] = fspath.join(project.path,"package.json"); promises.push(fs.readFile(project.paths['package.json'],"utf8").then(function(content) { project.package = util.parseJSON(content); + if (project.package.hasOwnProperty('node-red')) { + if (project.package['node-red'].hasOwnProperty('settings')) { + project.paths.flowFile = project.package['node-red'].settings.flowFile; + project.paths.credentialsFile = project.package['node-red'].settings.credentialsFile; + } + } else { + // TODO: package.json doesn't have a node-red section + // is that a bad thing? + } })); + } else { + project.package = {}; } if (missingFiles.indexOf('README.md') === -1) { project.paths['README.md'] = fspath.join(project.path,"README.md"); @@ -71,6 +82,16 @@ Project.prototype.load = function () { } else { project.description = ""; } + // if (missingFiles.indexOf('flow.json') !== -1) { + // console.log("MISSING FLOW FILE"); + // } else { + // project.paths.flowFile = fspath.join(project.path,"flow.json"); + // } + // if (missingFiles.indexOf('flow_cred.json') !== -1) { + // console.log("MISSING CREDS FILE"); + // } else { + // project.paths.credentialsFile = fspath.join(project.path,"flow_cred.json"); + // } return when.settle(promises).then(function() { return project; @@ -79,11 +100,23 @@ Project.prototype.load = function () { }; Project.prototype.update = function (data) { - var filesToUpdate = {}; + var promises = []; var project = this; + var saveSettings = false; + var saveREADME = false; + var savePackage = false; + var flowFilesChanged = false; + var credentialSecretChanged = false; - if (data.credentialSecret) { + var globalProjectSettings = settings.get("projects"); + if (!globalProjectSettings.projects.hasOwnProperty(this.name)) { + globalProjectSettings.projects[this.name] = {}; + saveSettings = true; + } + + + if (data.credentialSecret && data.credentialSecret !== this.credentialSecret) { var existingSecret = data.currentCredentialSecret; var isReset = data.resetCredentialSecret; var secret = data.credentialSecret; @@ -100,42 +133,62 @@ Project.prototype.update = function (data) { this.credentialSecret !== existingSecret) { // key doesn't match provided existing key var e = new Error("Cannot change credentialSecret without current key"); e.code = "missing_current_credential_key"; - throw e; + return when.reject(e); } this.credentialSecret = secret; - var globalProjectSettings = settings.get("projects"); - globalProjectSettings.projects[this.name] = globalProjectSettings.projects[this.name]||{} globalProjectSettings.projects[this.name].credentialSecret = project.credentialSecret; delete this.credentialSecretInvalid; - - return settings.set("projects",globalProjectSettings); + saveSettings = true; + credentialSecretChanged = true; } if (data.hasOwnProperty('description')) { - filesToUpdate[this.paths['README.md']] = function() { - return data.description; - }; + saveREADME = true; this.description = data.description; } if (data.hasOwnProperty('dependencies')) { - filesToUpdate[this.paths['package.json']] = function() { - return JSON.stringify(project.package,"",4) - }; + savePackage = true; this.package.dependencies = data.dependencies; } if (data.hasOwnProperty('summary')) { - filesToUpdate[this.paths['package.json']] = function() { - return JSON.stringify(project.package,"",4) - }; + savePackage = true; this.package.description = data.summary; } - var files = Object.keys(filesToUpdate); - files.forEach(function(f) { - promises.push(util.writeFile(f,filesToUpdate[f]())); - }); - return when.settle(promises); + if (data.hasOwnProperty('files')) { + this.package['node-red'] = this.package['node-red'] || { settings: {}}; + if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow) { + this.paths.flowFile = data.files.flow; + this.package['node-red'].settings.flowFile = data.files.flow; + savePackage = true; + flowFilesChanged = true; + } + if (data.files.hasOwnProperty('credentials') && this.package['node-red'].settings.credentialsFile !== data.files.credentials) { + this.paths.credentialsFile = data.files.credentials; + this.package['node-red'].settings.credentialsFile = data.files.credentials; + // Don't know if the credSecret is invalid or not so clear the flag + delete this.credentialSecretInvalid; + + savePackage = true; + flowFilesChanged = true; + } + } + if (saveSettings) { + promises.push(settings.set("projects",globalProjectSettings)); + } + if (saveREADME) { + promises.push(util.writeFile(this.paths['README.md'], this.description)); + } + if (savePackage) { + promises.push(util.writeFile(this.paths['package.json'], JSON.stringify(this.package,"",4))); + } + return when.settle(promises).then(function(res) { + return { + flowFilesChanged: flowFilesChanged, + credentialSecretChanged: credentialSecretChanged + } + }) }; Project.prototype.getFiles = function () { @@ -161,13 +214,23 @@ Project.prototype.getCommit = function(sha) { } Project.prototype.getFlowFile = function() { - return this.paths.flowFile; + console.log("Project.getFlowFile = ",this.paths.flowFile); + if (this.paths.flowFile) { + return fspath.join(this.path,this.paths.flowFile); + } else { + return null; + } } Project.prototype.getFlowFileBackup = function() { return getBackupFilename(this.getFlowFile()); } Project.prototype.getCredentialsFile = function() { - return this.paths.credentialsFile; + console.log("Project.getCredentialsFile = ",this.paths.credentialsFile); + if (this.paths.credentialsFile) { + return fspath.join(this.path,this.paths.credentialsFile); + } else { + return this.paths.credentialsFile; + } } Project.prototype.getCredentialsFileBackup = function() { return getBackupFilename(this.getCredentialsFile()); @@ -186,8 +249,8 @@ Project.prototype.toJSON = function () { credentialSecretInvalid: this.credentialSecretInvalid }, files: { - flowFile: this.paths.flowFile&&this.paths.flowFile.substring(this.path.length), - credentialsFile: this.paths.credentialsFile&&this.paths.credentialsFile.substring(this.path.length) + flow: this.paths.flowFile, + credentials: this.paths.credentialsFile } } }; @@ -233,7 +296,7 @@ function createDefaultProject(project) { } function checkProjectFiles(project) { - var projectPath = fspath.join(projectsDir,project); + var projectPath = project.path; var promises = []; var paths = []; for (var file in defaultFileSet) { @@ -279,6 +342,9 @@ function createProject(metadata) { if (metadata.credentialSecret) { projects.projects[project].credentialSecret = metadata.credentialSecret; } + if (metadata.remote) { + projects.projects[project].remote = metadata.remote; + } return settings.set('projects',projects); }).then(function() { if (metadata.remote) { diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index 07d0a6d43..bdc69ca94 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -111,7 +111,7 @@ function getFiles(localRepo) { } } files[fullName] = { - type: "f" + type: /\/$/.test(fullName)?"d":"f" } }) return runCommand(gitCommand,["status","--porcelain"],localRepo).then(function(output) { diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index 782188b10..cb676393b 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -200,11 +200,36 @@ function updateProject(project,data) { // TODO standardise throw new Error("Cannot update inactive project"); } - if (data.hasOwnProperty('credentialSecret')) { - return setCredentialSecret(data); - } else { - return activeProject.update(data); - } + // In case this triggers a credential secret change + var isReset = data.resetCredentialSecret; + var wasInvalid = activeProject.credentialSecretInvalid; + + return activeProject.update(data).then(function(result) { + + if (result.flowFilesChanged) { + flowsFullPath = activeProject.getFlowFile(); + flowsFileBackup = activeProject.getFlowFileBackup(); + credentialsFile = activeProject.getCredentialsFile(); + credentialsFileBackup = activeProject.getCredentialsFileBackup(); + return reloadActiveProject(); + } else if (result.credentialSecretChanged) { + if (isReset || !wasInvalid) { + if (isReset) { + runtime.nodes.clearCredentials(); + } + runtime.nodes.setCredentialSecret(activeProject.credentialSecret); + return runtime.nodes.exportCredentials() + .then(runtime.storage.saveCredentials) + .then(function() { + if (wasInvalid) { + return reloadActiveProject(); + } + }); + } else if (wasInvalid) { + return reloadActiveProject(); + } + } + }); } function setCredentialSecret(data) { //existingSecret,secret) { var isReset = data.resetCredentialSecret; @@ -251,6 +276,14 @@ function getFlows() { log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath})); } } + if (activeProject) { + if (!activeProject.getFlowFile()) { + log.warn("NLS: project has no flow file"); + var error = new Error("NLS: project has no flow file"); + error.code = "missing_flow_file"; + return when.reject(error); + } + } return util.readFile(flowsFullPath,flowsFileBackup,[],'flow'); }