',{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');
}