diff --git a/editor/js/main.js b/editor/js/main.js
index e9eea4b58..09750b85e 100644
--- a/editor/js/main.js
+++ b/editor/js/main.js
@@ -97,17 +97,50 @@
}
if (msg.text) {
var text = RED._(msg.text,{default:msg.text});
+ var options = {
+ type: msg.type,
+ fixed: msg.timeout === undefined,
+ timeout: msg.timeout
+ }
if (notificationId === "runtime-state") {
if (msg.error === "credentials_load_failed") {
- // TODO: NLS
- text += '
'+'Setup credentials'+'
';
+ options.buttons = [
+ {
+ text: "Setup credentials",
+ click: function() {
+ RED.projects.showCredentialsPrompt();
+ }
+ }
+ ]
} else if (msg.error === "missing_flow_file") {
- // TODO: NLS
- text += ''+'Setup project files'+'
';
+ options.buttons = [
+ {
+ text: "Setup project files",
+ click: function() {
+ persistentNotifications[notificationId].close();
+ RED.projects.showFilesPrompt();
+ }
+ }
+ ]
+ } else if (msg.error === "project_empty") {
+ options.buttons = [
+ {
+ text: "No thanks",
+ click: function() {
+ persistentNotifications[notificationId].close();
+ }
+ }, {
+ text: "Create default project files",
+ click: function() {
+ persistentNotifications[notificationId].close();
+ RED.projects.createDefaultFileSet();
+ }
+ }
+ ]
}
}
if (!persistentNotifications.hasOwnProperty(notificationId)) {
- persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout);
+ persistentNotifications[notificationId] = RED.notify(text,options);
} else {
persistentNotifications[notificationId].update(text,msg.timeout);
}
diff --git a/editor/js/ui/notifications.js b/editor/js/ui/notifications.js
index 995ce1ce5..1e2a50d53 100644
--- a/editor/js/ui/notifications.js
+++ b/editor/js/ui/notifications.js
@@ -69,6 +69,10 @@ RED.notify = (function() {
n.close = (function() {
var nn = n;
return function() {
+ if (nn.closed) {
+ return;
+ }
+ nn.closed = true;
currentNotifications.splice(currentNotifications.indexOf(nn),1);
$(nn).slideUp(300, function() {
nn.parentNode.removeChild(nn);
diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js
index 04571fa55..a535507c1 100644
--- a/editor/js/ui/projects.js
+++ b/editor/js/ui/projects.js
@@ -83,7 +83,7 @@ RED.projects = (function() {
migrateProjectHeader.appendTo(container);
var body = $('
').appendTo(container);
- $('').text("Step 1 : Setup your version control client").appendTo(body);
+ $('
').text("Setup your version control client").appendTo(body);
$('
').text("Node-RED uses the open source tool Git for version control. It tracks changes to your project files and lets you push them to remote repositories.").appendTo(body);
$('
').text("When you commit a set of changes, Git records who made the changes with a username and email address. The Username can be anything you want - it does not need to be your real name.").appendTo(body);
$('
').text("If your Git client is already configured, you can skip this step.").appendTo(body);
@@ -139,25 +139,24 @@ RED.projects = (function() {
migrateProjectHeader.appendTo(container);
var body = $('
').appendTo(container);
- $('').text("Step 2 : Create your project").appendTo(body);
- $('
').text("A project contains your flow files, a README file, a package.json file and a settings file. It makes it much easier to share your flows with others and to collaborate on them.").appendTo(body);
+ $('
').text("Create your project").appendTo(body);
+ $('
').text("A project is maintained as a Git repository. It makes it much easier to share your flows with others and to collaborate on them.").appendTo(body);
$('
').text("You can create multiple projects and quickly switch between them from the editor.").appendTo(body);
$('
').text("To begin, your project needs a name and an optional description.").appendTo(body);
var validateForm = function() {
var projectName = projectNameInput.val();
var valid = true;
+ var projectNameValid = /^[a-zA-Z0-9\-_]+$/.test(projectName);
if (projectNameInputChanged) {
projectNameStatus.empty();
- if (!/^[a-zA-Z0-9\-_]+$/.test(projectName)) {
+ if (!projectNameValid) {
projectNameInput.addClass("input-error");
$(' ').appendTo(projectNameStatus);
- projectNameValid = false;
valid = false;
} else {
projectNameInput.removeClass("input-error");
$(' ').appendTo(projectNameStatus);
- projectNameValid = true;
}
projectNameLastChecked = projectName;
}
@@ -206,6 +205,7 @@ RED.projects = (function() {
setTimeout(function() {
projectNameInput.focus();
+ validateForm();
},50);
return container;
},
@@ -224,14 +224,112 @@ RED.projects = (function() {
click: function() {
createProjectOptions.name = projectNameInput.val();
createProjectOptions.summary = projectSummaryInput.val();
- createProjectOptions.files = { migrateFiles: true };
- show('encryption-config');
+ show('default-files');
}
}
]
};
})(),
+ 'default-files': (function() {
+ var projectFlowFileInput;
+ var projectCredentialFileInput;
+ return {
+ content: function(options) {
+ var container = $('
');
+ migrateProjectHeader.appendTo(container);
+ var body = $('
').appendTo(container);
+ $('').text("Create your project files").appendTo(body);
+ $('
').text("A project contains your flow files, a README file, a package.json file and a settings file.").appendTo(body);
+ $('
').text("It can contain any other files you want to maintain in the Git repository.").appendTo(body);
+ if (!options.existingProject) {
+ $('
').text("Your existing flow and credential files will be copied into the project.").appendTo(body);
+ }
+
+ var validateForm = function() {
+ var valid = true;
+ var flowFile = projectFlowFileInput.val();
+ if (flowFile === "" || !/\.json$/.test(flowFile)) {
+ valid = false;
+ if (!projectFlowFileInput.hasClass("input-error")) {
+ projectFlowFileInput.addClass("input-error");
+ projectFlowFileInput.next().empty().append(' ');
+ }
+ projectCredentialFileInput.text("");
+ if (!projectCredentialFileInput.hasClass("input-error")) {
+ projectCredentialFileInput.addClass("input-error");
+ projectCredentialFileInput.next().empty().append(' ');
+ }
+ } else {
+ if (projectFlowFileInput.hasClass("input-error")) {
+ projectFlowFileInput.removeClass("input-error");
+ projectFlowFileInput.next().empty();
+ }
+ if (projectCredentialFileInput.hasClass("input-error")) {
+ projectCredentialFileInput.removeClass("input-error");
+ projectCredentialFileInput.next().empty();
+ }
+ projectCredentialFileInput.text(flowFile.substring(0,flowFile.length-5)+"_cred.json");
+ }
+ $("#projects-dialog-create-default-files").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
+ }
+ var row = $('
').appendTo(body);
+ $('Flow file ').appendTo(row);
+ var subrow = $('
').appendTo(row);
+ var defaultFlowFile = (createProjectOptions.files &&createProjectOptions.files.flow) || RED.settings.files.flow||"flow.json";
+ projectFlowFileInput = $(' ').val(defaultFlowFile)
+ .on("change keyup paste",validateForm)
+ .appendTo(subrow);
+ $('
').appendTo(subrow);
+ $('*.json ').appendTo(row);
+
+ var defaultCredentialsFile = (createProjectOptions.files &&createProjectOptions.files.credentials) || RED.settings.files.credentials||"flow_cred.json";
+ row = $('
').appendTo(body);
+ $('Credentials file ').appendTo(row);
+ subrow = $('
').appendTo(row);
+ projectCredentialFileInput = $('').text(defaultCredentialsFile)
+ .appendTo(subrow);
+ $('
').appendTo(subrow);
+
+ setTimeout(function() {
+ projectFlowFileInput.focus();
+ validateForm();
+ },50);
+
+ return container;
+ },
+ buttons: function(options) {
+ return [
+ {
+ // id: "clipboard-dialog-cancel",
+ text: options.existingProject?"Cancel":"Back",
+ click: function() {
+ if (options.existingProject) {
+ $(this).dialog('close');
+ } else {
+ show('project-details',options);
+ }
+ }
+ },
+ {
+ id: "projects-dialog-create-default-files",
+ text: "Next", // TODO: nls
+ class: "primary",
+ click: function() {
+ createProjectOptions.files = {
+ flow: projectFlowFileInput.val(),
+ credentials: projectCredentialFileInput.text()
+ }
+ if (!options.existingProject) {
+ createProjectOptions.migrateFiles = true;
+ }
+ show('encryption-config',options);
+ }
+ }
+ ]
+ }
+ }
+ })(),
'encryption-config': (function() {
var emptyProjectCredentialInput;
return {
@@ -241,20 +339,23 @@ RED.projects = (function() {
migrateProjectHeader.appendTo(container);
var body = $('
').appendTo(container);
- $('
').text("Step 3 : Setup encryption of your credentials file").appendTo(body);
- // $('
').text("Your flow credentials file can be encrypted to keep its contents secure.").appendTo(body);
-
- if (RED.settings.flowEncryptionType === 'disabled') {
- $('
').text("Your flow credentials file is not currently encrypted.").appendTo(body);
- $('
').text("That means its contents, such as passwords and access tokens, can be read by anyone with access to the file.").appendTo(body);
+ $('
').text("Setup encryption of your credentials file").appendTo(body);
+ if (options.existingProject) {
+ $('
').text("Your flow credentials file can be encrypted to keep its contents secure.").appendTo(body);
$('
').text("If you want to store these credentials in a public Git repository, you must encrypt them by providing a secret key phrase.").appendTo(body);
} else {
- if (RED.settings.flowEncryptionType === 'user') {
- $('
').text("Your flow credentials file is currently encrypted using the credentialSecret property from your settings file as the key.").appendTo(body);
- } else if (RED.settings.flowEncryptionType === 'system') {
- $('
').text("Your flow credentials file is currently encrypted using a system-generated secret as the key. You should provide a new secret key for this project.").appendTo(body);
+ if (RED.settings.flowEncryptionType === 'disabled') {
+ $('
').text("Your flow credentials file is not currently encrypted.").appendTo(body);
+ $('
').text("That means its contents, such as passwords and access tokens, can be read by anyone with access to the file.").appendTo(body);
+ $('
').text("If you want to store these credentials in a public Git repository, you must encrypt them by providing a secret key phrase.").appendTo(body);
+ } else {
+ if (RED.settings.flowEncryptionType === 'user') {
+ $('
').text("Your flow credentials file is currently encrypted using the credentialSecret property from your settings file as the key.").appendTo(body);
+ } else if (RED.settings.flowEncryptionType === 'system') {
+ $('
').text("Your flow credentials file is currently encrypted using a system-generated secret as the key. You should provide a new secret key for this project.").appendTo(body);
+ }
+ $('
').text("The secret will be copied into the settings for your new project. You can then manage the secret within the editor.").appendTo(body);
}
- $('
').text("The secret will be copied into the settings for your new project. You can then manage the secret within the editor.").appendTo(body);
}
// var row = $('
').appendTo(body);
@@ -356,75 +457,89 @@ RED.projects = (function() {
return container;
},
- buttons: [
- {
- // id: "clipboard-dialog-cancel",
- text: "Back",
- click: function() {
- show('project-details');
- }
- },
- {
- id: "projects-dialog-create-encryption",
- text: "Create project", // TODO: nls
- class: "primary disabled",
- disabled: true,
- click: function() {
- var encryptionState = $("input[name=projects-encryption-type]:checked").val();
- if (encryptionState === 'enabled') {
- var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val();
- if (encryptionKeyType === 'custom') {
- createProjectOptions.credentialSecret = emptyProjectCredentialInput.val();
- } else {
- // If 'use existing', leave createProjectOptions.credentialSecret blank
- // - that will trigger it to use the existing key
- // TODO: this option should be disabled if encryption is disabled
- }
- } else {
- // Disabled encryption by explicitly setting credSec to false
- createProjectOptions.credentialSecret = false;
+ buttons: function(options) {
+ return [
+ {
+ // id: "clipboard-dialog-cancel",
+ text: "Back",
+ click: function() {
+ show('default-files',options);
}
+ },
+ {
+ id: "projects-dialog-create-encryption",
+ text: options.existingProject?"Create project files":"Create project", // TODO: nls
+ class: "primary disabled",
+ disabled: true,
+ click: function() {
+ var encryptionState = $("input[name=projects-encryption-type]:checked").val();
+ if (encryptionState === 'enabled') {
+ var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val();
+ if (encryptionKeyType === 'custom') {
+ createProjectOptions.credentialSecret = emptyProjectCredentialInput.val();
+ } else {
+ // If 'use existing', leave createProjectOptions.credentialSecret blank
+ // - that will trigger it to use the existing key
+ // TODO: this option should be disabled if encryption is disabled
+ }
+ } else {
+ // Disabled encryption by explicitly setting credSec to false
+ createProjectOptions.credentialSecret = false;
+ }
+ RED.deploy.setDeployInflight(true);
+ RED.projects.settings.switchProject(createProjectOptions.name);
- RED.deploy.setDeployInflight(true);
- RED.projects.settings.switchProject(createProjectOptions.name);
+ var method = "POST";
+ var url = "projects";
- sendRequest({
- url: "projects",
- type: "POST",
- requireCleanWorkspace: true,
- handleAuthFail: false,
- responses: {
- 200: function(data) {
- createProjectOptions = {};
- show('create-success');
- },
- 400: {
- 'project_exists': function(error) {
- console.log("already exists");
+ if (options.existingProject) {
+ createProjectOptions.initialise = true;
+ method = "PUT";
+ url = "projects/"+activeProject.name;
+ }
+ var self = this;
+ sendRequest({
+ url: url,
+ type: method,
+ requireCleanWorkspace: true,
+ handleAuthFail: false,
+ responses: {
+ 200: function(data) {
+ createProjectOptions = {};
+ if (options.existingProject) {
+ $( self ).dialog( "close" );
+ } else {
+ show('create-success');
+ }
},
- 'git_error': function(error) {
- console.log("git error",error);
- },
- 'git_connection_failed': function(error) {
- projectRepoInput.addClass("input-error");
- },
- 'git_auth_failed': function(error) {
- projectRepoUserInput.addClass("input-error");
- projectRepoPasswordInput.addClass("input-error");
- // getRepoAuthDetails(req);
- console.log("git auth error",error);
- },
- 'unexpected_error': function(error) {
- console.log("unexpected_error",error)
+ 400: {
+ 'project_exists': function(error) {
+ console.log("already exists");
+ },
+ 'git_error': function(error) {
+ console.log("git error",error);
+ },
+ 'git_connection_failed': function(error) {
+ projectRepoInput.addClass("input-error");
+ },
+ 'git_auth_failed': function(error) {
+ projectRepoUserInput.addClass("input-error");
+ projectRepoPasswordInput.addClass("input-error");
+ // getRepoAuthDetails(req);
+ console.log("git auth error",error);
+ },
+ 'unexpected_error': function(error) {
+ console.log("unexpected_error",error)
+ }
}
}
- }
- },createProjectOptions).always(function() {
- RED.deploy.setDeployInflight(false);
- })
+ },createProjectOptions).always(function() {
+ RED.deploy.setDeployInflight(false);
+ })
+ }
}
- }
- ]
+ ];
+ }
}
})(),
'create-success': {
@@ -437,10 +552,10 @@ RED.projects = (function() {
$('
').text("You have successfully created your first project!").appendTo(body);
$('
').text("You can now continue to use Node-RED just as you always have.").appendTo(body);
$('
').text("The 'info' tab in the sidebar shows you what your current active project is. "+
- "The button next to the name can be used to access the project settings view.").appendTo(body);
- $('
').text("The new 'history' tab in the sidebar can be used to view files that have changed "+
- "in your project and to commit them. It shows you a complete history of your commits and "+
- "allows you to push your changes to a remote repository.").appendTo(body);
+ "The button next to the name can be used to access the project settings view.").appendTo(body);
+ $('
').text("The 'history' tab in the sidebar can be used to view files that have changed "+
+ "in your project and to commit them. It shows you a complete history of your commits and "+
+ "allows you to push your changes to a remote repository.").appendTo(body);
return container;
},
@@ -537,18 +652,15 @@ RED.projects = (function() {
}
if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repo)) {
$(".projects-dialog-screen-create-row-creds").hide();
- $(".projects-dialog-screen-create-row-passphrase").show();
$(".projects-dialog-screen-create-row-sshkey").show();
// if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) {
// valid = false;
// }
} else if (/^https?:\/\//.test(repo)) {
$(".projects-dialog-screen-create-row-creds").show();
- $(".projects-dialog-screen-create-row-passphrase").hide();
$(".projects-dialog-screen-create-row-sshkey").hide();
} else {
$(".projects-dialog-screen-create-row-creds").show();
- $(".projects-dialog-screen-create-row-passphrase").hide();
$(".projects-dialog-screen-create-row-sshkey").hide();
}
@@ -757,9 +869,9 @@ RED.projects = (function() {
projectRepoPasswordInput = $(' ').appendTo(subrow);
row = $('
').hide().appendTo(container);
-
- $('
SSH Key ').appendTo(row);
- projectRepoSSHKeySelect = $("
").appendTo(row);
+ subrow = $('
').appendTo(row);
+ $('SSH Key ').appendTo(subrow);
+ projectRepoSSHKeySelect = $("",{style:"width: 100%"}).appendTo(subrow);
$.getJSON("settings/user/keys", function(data) {
var count = 0;
@@ -773,20 +885,9 @@ RED.projects = (function() {
});
- row = $('
').hide().appendTo(container);
- $('SSH key passphrase ').appendTo(row);
- projectRepoPassphrase = $(' ').appendTo(row);
-
- // row = $('
').hide().appendTo(container);
- // $('Remote name ').appendTo(row);
- // projectRepoRemoteName = $(' ').val("origin").appendTo(row);
- //
- // row = $('
').hide().appendTo(container);
- // $('Branch ').appendTo(row);
- // projectRepoBranch = $(' ').val('master').appendTo(row);
-
-
-
+ subrow = $('
').appendTo(row);
+ $('Passphrase ').appendTo(subrow);
+ projectRepoPassphrase = $(' ').appendTo(subrow);
// // Secret - clone
// row = $('
').appendTo(container);
@@ -905,6 +1006,10 @@ RED.projects = (function() {
projectRepoPassphrase.addClass("input-error");
console.log("git auth error",error);
},
+ 'project_empty': function(error) {
+ // This is handled via a runtime notification.
+ dialog.dialog("close");
+ },
'unexpected_error': function(error) {
console.log("unexpected_error",error)
}
@@ -996,15 +1101,12 @@ RED.projects = (function() {
class: "primary disabled",
disabled: true,
click: function() {
+ dialog.dialog( "close" );
switchProject(selectedProject.name,function(err,data) {
if (err) {
- if (err.error === 'credentials_load_failed') {
- dialog.dialog( "close" );
- } else {
+ if (err.error !== 'credentials_load_failed') {
console.log("unexpected_error",err)
}
- } else {
- dialog.dialog( "close" );
}
})
}
@@ -1109,7 +1211,11 @@ RED.projects = (function() {
var container = screen.content(options);
dialogBody.empty();
- dialog.dialog('option','buttons',screen.buttons);
+ var buttons = screen.buttons;
+ if (typeof buttons === 'function') {
+ buttons = buttons(options);
+ }
+ dialog.dialog('option','buttons',buttons);
dialogBody.append(container);
dialog.dialog('option','title',screen.title||"");
dialog.dialog("open");
@@ -1258,7 +1364,7 @@ RED.projects = (function() {
count++;
});
if (count === 0) {
- // projectRepoSSHKeySelect
+ //TODO: handle no keys yet setup
}
});
row = $('
').appendTo(message);
@@ -1520,6 +1626,38 @@ RED.projects = (function() {
// initSidebar();
}
+ function createDefaultFileSet() {
+ if (!activeProject) {
+ throw new Error("Cannot create default file set without an active project");
+ } else if (!activeProject.empty) {
+ throw new Error("Cannot create default file set on a non-empty project");
+ }
+ createProjectOptions = {};
+ show('default-files',{existingProject: true});
+ // var payload = {
+ //
+ // }
+ // RED.deploy.setDeployInflight(true);
+ // utils.sendRequest({
+ // url: "projects/"+activeProject.name,
+ // type: "PUT",
+ // responses: {
+ // 0: function(error) {},
+ // 200: function(data) {
+ // activeProject = data;
+ // RED.sidebar.versionControl.refresh(true);
+ // },
+ // 400: {
+ // 'unexpected_error': function(error) {
+ // console.log(error);
+ // }
+ // },
+ // }
+ // },payload).always(function() {
+ // RED.deploy.setDeployInflight(false);
+ // });
+
+ }
function refresh(done) {
$.getJSON("projects",function(data) {
@@ -1546,6 +1684,7 @@ RED.projects = (function() {
return {
init: init,
+ _show: show,
showStartup: function() {
show('welcome');
},
@@ -1568,6 +1707,7 @@ RED.projects = (function() {
showFilesPrompt: function() { //TODO: rename this function
RED.projects.settings.show('settings');
},
+ createDefaultFileSet: createDefaultFileSet,
// showSidebar: showSidebar,
refresh: refresh,
editProject: function() {
diff --git a/editor/js/ui/tab-versionControl.js b/editor/js/ui/tab-versionControl.js
index 8c03e0004..89c577677 100644
--- a/editor/js/ui/tab-versionControl.js
+++ b/editor/js/ui/tab-versionControl.js
@@ -1191,7 +1191,7 @@ RED.sidebar.versionControl = (function() {
var commitsBehind = result.commits.behind || 0;
if (activeProject.git.hasOwnProperty('remotes')) {
- if (result.branches.hasOwnProperty("remoteError")) {
+ if (result.branches.hasOwnProperty("remoteError") && result.branches.remoteError.code !== 'git_remote_gone') {
$("#sidebar-version-control-repo-status-auth-issue").show();
$("#sidebar-version-control-repo-status-stats").hide();
$('#sidebar-version-control-repo-branch').attr('disabled',true);
diff --git a/editor/sass/editor.scss b/editor/sass/editor.scss
index 734d8cb94..9d62de15a 100644
--- a/editor/sass/editor.scss
+++ b/editor/sass/editor.scss
@@ -160,7 +160,7 @@
}
#full-shade {
@include shade;
- z-index: 101;
+ z-index: 15;
}
.dialog-form,#dialog-form, #node-config-dialog-edit-form {
diff --git a/editor/sass/notifications.scss b/editor/sass/notifications.scss
index d8c8f853b..7f1fe4f43 100644
--- a/editor/sass/notifications.scss
+++ b/editor/sass/notifications.scss
@@ -15,7 +15,7 @@
**/
#notifications {
- z-index: 101;
+ z-index: 100;
width: 500px;
margin-left: -250px;
left: 50%;
diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json
index 249afb37f..187aba33c 100644
--- a/red/api/editor/locales/en-US/editor.json
+++ b/red/api/editor/locales/en-US/editor.json
@@ -86,7 +86,8 @@
"missing-types": "Flows stopped due to missing node types. Check logs for details.",
"restartRequired": "Node-RED must be restarted to enable upgraded modules",
"credentials_load_failed": "Flows stopped due to missing or invalid credentialSecret",
- "missing_flow_file": "Could not find the project flow file"
+ "missing_flow_file": "Could not find the project flow file",
+ "project_empty": "The project repository is empty.
Do you want to create a default set of project files? Otherwise, you will have to manually add files to the project outside of the editor.
"
},
"error": "Error : __message__",
diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js
index 072abc8d5..4860ded3d 100644
--- a/red/api/editor/projects/index.js
+++ b/red/api/editor/projects/index.js
@@ -90,6 +90,17 @@ module.exports = {
} else {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}
+ } else if (req.body.initialise) {
+ // Initialised set when creating default files for an empty repo
+ runtime.storage.projects.initialiseProject(req.user, req.params.id, req.body).then(function() {
+ res.redirect(303,req.baseUrl + '/'+ req.params.id);
+ }).catch(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 if (req.body.hasOwnProperty('credentialSecret') ||
req.body.hasOwnProperty('description') ||
req.body.hasOwnProperty('dependencies')||
@@ -145,8 +156,11 @@ module.exports = {
res.status(404).end();
}
}).catch(function(err) {
- console.log(err.stack);
- res.status(400).json({error:"unexpected_error", message:err.toString()});
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
})
});
@@ -154,12 +168,15 @@ module.exports = {
// Project file listing
app.get("/:id/files", needsPermission("projects.read"), function(req,res) {
runtime.storage.projects.getFiles(req.user, req.params.id).then(function(data) {
- console.log("TODO: REMOVE /:id/files as /:id/status is better!")
+ // console.log("TODO: REMOVE /:id/files as /:id/status is better!")
res.json(data);
})
.catch(function(err) {
- console.log(err.stack);
- res.status(400).json({error:"unexpected_error", message:err.toString()});
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
})
});
diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js
index 840534011..0668f3631 100644
--- a/red/api/editor/settings.js
+++ b/red/api/editor/settings.js
@@ -48,6 +48,16 @@ module.exports = {
safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {};
safeSettings.editorTheme.palette.editable = false;
}
+ if (runtime.storage.projects) {
+ var activeProject = runtime.storage.projects.getActiveProject();
+ if (activeProject) {
+ safeSettings.project = activeProject;
+ }
+ safeSettings.files = {
+ flow: runtime.storage.projects.getFlowFilename(),
+ credentials: runtime.storage.projects.getCredentialsFilename()
+ }
+ }
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js
index 934b97ca5..65f2e8aec 100644
--- a/red/runtime/storage/localfilesystem/projects/Project.js
+++ b/red/runtime/storage/localfilesystem/projects/Project.js
@@ -36,6 +36,7 @@ function Project(name) {
this.name = name;
this.path = fspath.join(projectsDir,name);
this.paths = {};
+ this.files = {};
this.auth = {origin:{}};
this.missingFiles = [];
@@ -111,6 +112,44 @@ Project.prototype.load = function () {
});
};
+Project.prototype.initialise = function(user,data) {
+ var project = this;
+ if (!this.empty) {
+ throw new Error("Cannot initialise non-empty project");
+ }
+ var files = Object.keys(defaultFileSet);
+ var promises = [];
+
+ if (data.hasOwnProperty('credentialSecret')) {
+ var projects = settings.get('projects');
+ projects.projects[project.name] = projects.projects[project.name] || {};
+ projects.projects[project.name].credentialSecret = data.credentialSecret;
+ promises.push(settings.set('projects',projects));
+ }
+
+ project.files.flow = data.files.flow;
+ project.files.credentials = data.files.credentials;
+ var flowFilePath = fspath.join(project.path,project.files.flow);
+ var credsFilePath = getCredentialsFilename(flowFilePath);
+ promises.push(util.writeFile(flowFilePath,"[]"));
+ promises.push(util.writeFile(credsFilePath,"{}"));
+ files.push(project.files.flow);
+ files.push(project.files.credentials);
+ for (var file in defaultFileSet) {
+ if (defaultFileSet.hasOwnProperty(file)) {
+ promises.push(util.writeFile(fspath.join(project.path,file),defaultFileSet[file](project)));
+ }
+ }
+
+ return when.all(promises).then(function() {
+ return gitTools.stageFile(project.path,files);
+ }).then(function() {
+ return gitTools.commit(project.path,"Create project files");
+ }).then(function() {
+ return project.load()
+ })
+}
+
Project.prototype.loadRemotes = function() {
var project = this;
return gitTools.getRemotes(project.path).then(function(remotes) {
@@ -154,8 +193,15 @@ Project.prototype.loadBranches = function() {
var project = this;
return gitTools.getBranchInfo(project.path).then(function(branches) {
project.branches = branches;
+ project.empty = project.branches.empty;
+ delete project.branches.empty;
});
}
+
+Project.prototype.isEmpty = function () {
+ return this.empty;
+};
+
Project.prototype.update = function (user, data) {
var username;
if (!user) {
@@ -297,7 +343,12 @@ Project.prototype.update = function (user, data) {
};
Project.prototype.getFiles = function () {
- return gitTools.getFiles(this.path);
+ return gitTools.getFiles(this.path).catch(function(err) {
+ if (/ambiguous argument/.test(err.message)) {
+ return {};
+ }
+ throw err;
+ });
};
Project.prototype.stageFile = function(file) {
return gitTools.stageFile(this.path,file);
@@ -319,7 +370,16 @@ Project.prototype.getFileDiff = function(file,type) {
return gitTools.getFileDiff(this.path,file,type);
}
Project.prototype.getCommits = function(options) {
- return gitTools.getCommits(this.path,options);
+ return gitTools.getCommits(this.path,options).catch(function(err) {
+ if (/ambiguous argument/.test(err.message) || /does not have any commits yet/.test(err.message)) {
+ return {
+ count:0,
+ commits:[],
+ total: 0
+ }
+ }
+ throw err;
+ })
}
Project.prototype.getCommit = function(sha) {
return gitTools.getCommit(this.path,sha);
@@ -373,20 +433,29 @@ Project.prototype.status = function(user) {
}
self.branches.local = result.branches.local;
self.branches.remote = result.branches.remote;
- if (fetchError) {
+ if (fetchError && !/ambiguous argument/.test(fetchError.message)) {
result.branches.remoteError = {
remote: fetchError.remote,
code: fetchError.code
}
}
return result;
+ }).catch(function(err) {
+ if (/ambiguous argument/.test(err.message)) {
+ return {
+ files:{},
+ commits:{total:0},
+ branches:{}
+ };
+ }
+ throw err;
});
}
return fetchPromise.then(completeStatus).catch(function(e) {
- if (e.code !== 'git_auth_failed') {
- console.log("Fetch failed");
- console.log(e);
- }
+ // if (e.code !== 'git_auth_failed') {
+ // console.log("Fetch failed");
+ // console.log(e);
+ // }
return completeStatus(e);
})
};
@@ -600,6 +669,7 @@ Project.prototype.toJSON = function () {
summary: this.package.description,
description: this.description,
dependencies: this.package.dependencies||{},
+ empty: this.empty,
settings: {
credentialsEncrypted: (typeof this.credentialSecret === "string"),
credentialSecretInvalid: this.credentialSecretInvalid
@@ -663,15 +733,15 @@ function createDefaultProject(user, project) {
var credsFilePath;
if (project.files.migrateFiles) {
- var baseFlowFileName = fspath.basename(project.files.flow);
- var baseCredentialFileName = fspath.basename(project.files.credentials);
+ var baseFlowFileName = project.files.flow || fspath.basename(project.files.oldFlow);
+ var baseCredentialFileName = project.files.credentials || fspath.basename(project.files.oldCredentials);
files.push(baseFlowFileName);
files.push(baseCredentialFileName);
flowFilePath = fspath.join(projectPath,baseFlowFileName);
credsFilePath = fspath.join(projectPath,baseCredentialFileName);
- log.trace("Migrating "+project.files.flow+" to "+flowFilePath);
- log.trace("Migrating "+project.files.credentials+" to "+credsFilePath);
- promises.push(fs.copy(project.files.flow,flowFilePath));
+ log.trace("Migrating "+project.files.oldFlow+" to "+flowFilePath);
+ log.trace("Migrating "+project.files.oldCredentials+" to "+credsFilePath);
+ promises.push(fs.copy(project.files.oldFlow,flowFilePath));
runtime.nodes.setCredentialSecret(project.credentialSecret);
promises.push(runtime.nodes.exportCredentials().then(function(creds) {
var credentialData;
@@ -769,7 +839,6 @@ function createProject(user, metadata) {
if (metadata.git && metadata.git.remotes && metadata.git.remotes.origin) {
var originRemote = metadata.git.remotes.origin;
var auth;
- console.log('originRemote:', originRemote);
if (originRemote.hasOwnProperty("username") && originRemote.hasOwnProperty("password")) {
authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
username: originRemote.username,
@@ -799,7 +868,18 @@ function createProject(user, metadata) {
// console.log("checkProjectFiles");
// console.log(results);
// });
-
+ // return gitTools.getFiles(projectPath).then(function() {
+ // // It wasn't an empty repository.
+ // // TODO: check for required files - checkProjectFiles
+ //
+ // }).catch(function(err) {
+ // if (/ambiguous argument/.test(err.message)) {
+ // // Empty repository
+ // err.code = "project_empty";
+ // err.message = "Project is empty";
+ // }
+ // throw err;
+ // });
resolve(getProject(project));
}).catch(function(error) {
fs.remove(projectPath,function() {
diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js
index 8bce2ce64..d710a0131 100644
--- a/red/runtime/storage/localfilesystem/projects/git/index.js
+++ b/red/runtime/storage/localfilesystem/projects/git/index.js
@@ -121,20 +121,17 @@ function getBranchInfo(localRepo) {
return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
var lines = output.split("\n");
var unknownDirs = [];
- var branchLineRE = /^## (.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m;
+ var branchLineRE = /^## (No commits yet on )?(.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m;
var m = branchLineRE.exec(output);
var result = {}; //commits:{}};
if (m) {
- result.local = m[1];
- if (m[3]) {
- result.remote = m[3];
+ if (m[1]) {
+ result.empty = true;
+ }
+ result.local = m[2];
+ if (m[4]) {
+ result.remote = m[4];
}
- // if (m[6] !== undefined) {
- // result.commits.ahead = parseInt(m[6]);
- // }
- // if (m[8] !== undefined) {
- // result.commits.behind = parseInt(m[8]);
- // }
}
return result;
});
@@ -177,7 +174,7 @@ function getStatus(localRepo) {
return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
var lines = output.split("\n");
var unknownDirs = [];
- var branchLineRE = /^## (.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/;
+ var branchLineRE = /^## (.+?)(?:$|\.\.\.(.+?)(?:$| \[(?:(?:ahead (\d+)(?:,\s*)?)?(?:behind (\d+))?|(gone))\]))/;
lines.forEach(function(line) {
if (line==="") {
return;
@@ -186,16 +183,22 @@ function getStatus(localRepo) {
var m = branchLineRE.exec(line);
if (m) {
result.branches.local = m[1];
- if (m[3]) {
- result.branches.remote = m[3];
+ if (m[2]) {
+ result.branches.remote = m[2];
result.commits.ahead = 0;
result.commits.behind = 0;
}
- if (m[6] !== undefined) {
- result.commits.ahead = parseInt(m[6]);
+ if (m[3] !== undefined) {
+ result.commits.ahead = parseInt(m[3]);
}
- if (m[8] !== undefined) {
- result.commits.behind = parseInt(m[8]);
+ if (m[4] !== undefined) {
+ result.commits.behind = parseInt(m[4]);
+ }
+ if (m[5] !== undefined) {
+ result.commits.ahead = result.commits.total;
+ result.branches.remoteError = {
+ code: "git_remote_gone"
+ }
}
}
return;
diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js
index a6802eeb1..e124675b9 100644
--- a/red/runtime/storage/localfilesystem/projects/index.js
+++ b/red/runtime/storage/localfilesystem/projects/index.js
@@ -303,8 +303,15 @@ function createProject(user, metadata) {
if (!metadata.hasOwnProperty('credentialSecret')) {
metadata.credentialSecret = currentEncryptionKey;
}
- metadata.files.flow = flowsFullPath;
- metadata.files.credentials = credentialsFile;
+ if (!metadata.files.flow) {
+ metadata.files.flow = fspath.basename(flowsFullPath);
+ }
+ if (!metadata.files.credentials) {
+ metadata.files.credentials = fspath.basename(credentialsFile);
+ }
+
+ metadata.files.oldFlow = flowsFullPath;
+ metadata.files.oldCredentials = credentialsFile;
metadata.files.credentialSecret = currentEncryptionKey;
}
return Projects.create(null,metadata).then(function(p) {
@@ -327,6 +334,21 @@ function setActiveProject(user, projectName) {
})
});
}
+
+function initialiseProject(user, project, data) {
+ if (!activeProject || activeProject.name !== project) {
+ // TODO standardise
+ throw new Error("Cannot initialise inactive project");
+ }
+ return activeProject.initialise(user,data).then(function(result) {
+ flowsFullPath = activeProject.getFlowFile();
+ flowsFileBackup = activeProject.getFlowFileBackup();
+ credentialsFile = activeProject.getCredentialsFile();
+ credentialsFileBackup = activeProject.getCredentialsFileBackup();
+ runtime.nodes.setCredentialSecret(activeProject.credentialSecret);
+ return reloadActiveProject("updated");
+ });
+}
function updateProject(user, project, data) {
if (!activeProject || activeProject.name !== project) {
// TODO standardise
@@ -414,9 +436,16 @@ function getFlows() {
}
}
if (activeProject) {
+ var error;
+ if (activeProject.isEmpty()) {
+ log.warn("Project repository is empty");
+ error = new Error("Project repository is empty");
+ error.code = "project_empty";
+ return when.reject(error);
+ }
if (!activeProject.getFlowFile()) {
- log.warn("NLS: project has no flow file");
- var error = new Error("NLS: project has no flow file");
+ log.warn("Project has no flow file");
+ error = new Error("Project has no flow file");
error.code = "missing_flow_file";
return when.reject(error);
}
@@ -466,6 +495,16 @@ function saveCredentials(credentials) {
return util.writeFile(credentialsFile, credentialData);
}
+function getFlowFilename() {
+ if (flowsFullPath) {
+ return fspath.basename(flowsFullPath);
+ }
+}
+function getCredentialsFilename() {
+ if (flowsFullPath) {
+ return fspath.basename(credentialsFile);
+ }
+}
module.exports = {
init: init,
@@ -475,6 +514,7 @@ module.exports = {
getProject: getProject,
deleteProject: deleteProject,
createProject: createProject,
+ initialiseProject: initialiseProject,
updateProject: updateProject,
getFiles: getFiles,
getFile: getFile,
@@ -498,6 +538,8 @@ module.exports = {
addRemote: addRemote,
removeRemote: removeRemote,
updateRemote: updateRemote,
+ getFlowFilename: getFlowFilename,
+ getCredentialsFilename: getCredentialsFilename,
getFlows: getFlows,
saveFlows: saveFlows,