From 70ebf96e3e75def5fa2023bb2d8d14ca331faebf Mon Sep 17 00:00:00 2001 From: Dan Krieger Date: Mon, 9 May 2016 13:25:24 -0700 Subject: [PATCH 1/5] Support for job templates --- app.js | 41 ++++++++++-- crontab.js | 104 ++++++++++++++++++++++------- package.json | 16 +++-- public/js/script.js | 157 +++++++++++++++++++++++++++++++++++++++++++- routes.js | 2 + views/index.ejs | 28 +++++--- views/navbar.ejs | 28 ++++++-- views/popup.ejs | 125 +++++++++++++++++++++++++++-------- 8 files changed, 421 insertions(+), 80 deletions(-) mode change 100755 => 100644 package.json diff --git a/app.js b/app.js index c496138..b8b580f 100755 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ var app = express(); var crontab = require("./crontab"); var restore = require("./restore"); +var moment = require('moment'); var path = require('path'); var mime = require('mime'); var fs = require('fs'); @@ -34,11 +35,19 @@ app.set('port', (process.env.PORT || 8000)); app.get(routes.root, function(req, res) { // get all the crontabs crontab.crontabs( function(docs){ - res.render('index', { - routes : JSON.stringify(routes), - crontabs : JSON.stringify(docs), - backups : crontab.get_backup_names(), - env : crontab.get_env() + crontab.templates(function(templates) { + res.render('index', { + routes : JSON.stringify(routes), + crontabs : JSON.stringify(docs), + templates: templates, + templatesById: templates.reduce(function(memo, t) { + memo[t._id] = t; + return memo; + }, {}), + backups : crontab.get_backup_names(), + env : crontab.get_env(), + moment: moment, + }); }); }); }) @@ -46,15 +55,33 @@ app.get(routes.root, function(req, res) { app.post(routes.save, function(req, res) { // new job if(req.body._id == -1){ - crontab.create_new(req.body.name, req.body.command, req.body.schedule, req.body.logging); + crontab.create_new(req.body.name, req.body.command, req.body.command_template, req.body.vars, req.body.schedule, req.body.logging); } // edit job else{ + crontab.update(req.body); } res.end(); }) +app.post(routes.save_template, function(req, res) { + // new job + if(req.body._id == -1){ + crontab.create_new_template(req.body.name, req.body.command, req.body.schedule); + } + // edit job + else{ + crontab.update_template(req.body); + } + res.end(); +}); + +app.post(routes.remove_template, function(req, res) { + crontab.remove_template(req.body._id); + res.end(); +}) + app.post(routes.stop, function(req, res) { crontab.status(req.body._id, true); res.end(); @@ -142,6 +169,8 @@ app.get(routes.logger, function(req, res) { res.end("No errors logged yet"); }) +// app.use('/scripts/moment.js', express.static(__dirname + '/node_modules/moment/min/moment.min.js')); + app.listen(app.get('port'), function() { console.log("Crontab UI is running at localhost:" + app.get('port')) }) diff --git a/crontab.js b/crontab.js index 5401850..143dfd6 100644 --- a/crontab.js +++ b/crontab.js @@ -1,21 +1,25 @@ //load database var Datastore = require('nedb'); -var db = new Datastore({ filename: __dirname + '/crontabs/crontab.db' }); -db.loadDatabase(function (err) { -}); +var db = { + crontabs: new Datastore({ filename: __dirname + '/crontabs/crontab.db', autoload: true }), + templates: new Datastore({ filename: __dirname + '/crontabs/templates.db', autoload: true }), +}; var exec = require('child_process').exec; var fs = require('fs'); var cron_parser = require("cron-parser") var os = require("os") +var _ = require('underscore'); exports.log_folder = __dirname + '/crontabs/logs'; exports.env_file = __dirname + '/crontabs/env.db'; -crontab = function(name, command, schedule, stopped, logging){ +crontab = function(name, command, command_template, vars, schedule, stopped, logging){ var data = {}; data.name = name; data.command = command; + data.command_template = command_template; data.schedule = schedule; + data.vars = vars || {}; if(stopped != null) { data.stopped = stopped; } @@ -24,34 +28,88 @@ crontab = function(name, command, schedule, stopped, logging){ return data; } -exports.create_new = function(name, command, schedule, logging){ - var tab = crontab(name, command, schedule, false, logging); +crontab_template = function(name, command, schedule){ + var data = {}; + data.name = name; + data.command = command; + data.schedule = schedule; + data.timestamp = (new Date()).toString(); + return data; +} + +exports.create_new = function(name, command, command_template, vars, schedule, logging){ + var tab = crontab(name, command, command_template, vars, schedule, false, logging); tab.created = new Date().valueOf(); - db.insert(tab); + db.crontabs.insert(tab); } exports.update = function(data){ - db.update({_id: data._id}, crontab(data.name, data.command, data.schedule, null, data.logging)); + db.crontabs.update({_id: data._id}, crontab(data.name, data.command, data.command_template, data.vars, data.schedule, null, data.logging)); } exports.status = function(_id, stopped){ - db.update({_id: _id},{$set: {stopped: stopped}}); + db.crontabs.update({_id: _id},{$set: {stopped: stopped}}); } exports.remove = function(_id){ - db.remove({_id: _id}, {}); -} -exports.crontabs = function(callback){ - db.find({}).sort({ created: -1 }).exec(function(err, docs){ - for(var i=0; i').val(vars[varName]); + + $jobVariables.append(''); + $jobVariables.append($input); + $jobVariables.append('
'); + + $input.change(function() { + current_job.vars = current_job.vars || {}; + current_job.vars[varName] = $(this).val(); + }) + }) + + } else { + job_command = $('#job-command').val(); + } + + job_string(); +} + +function findCommandVariables (command) { + return command.match(/{[a-zA-Z]+}/g).map(function(cmd) { + return cmd.substring(1, cmd.length - 1); + }); +}; + +function renderTemplateCommand(templateId) { + var template = findTemplate(templateId); + + if(template) { + return template.command; + } + + return ''; +} + function newJob(){ + current_job = {}; schedule = "" job_command = "" $("#job-minute").val("*"); @@ -101,14 +168,17 @@ function newJob(){ $("#job").modal("show"); $("#job-name").val(""); $("#job-command").val(""); + $("#job-command-template").val(""); job_string(); $("#job-save").unbind("click"); // remove existing events attached to this $("#job-save").click(function(){ // TODO good old boring validations - $.post(routes.save, {name: $("#job-name").val(), command: job_command , schedule: schedule, _id: -1, logging: $("#job-logging").prop("checked")}, function(){ + $.post(routes.save, {name: $("#job-name").val(), command: job_command , schedule: schedule, _id: -1, logging: $("#job-logging").prop("checked"), command_template: $('#job-command-template').val(), vars: current_job.vars }, function(){ location.reload(); }) }); + + setTemplateVariables(null); } function doBackup(){ @@ -145,6 +215,7 @@ function import_db(){ // script corresponding to job popup management var schedule = ""; var job_command = ""; +var current_job = null; function job_string(){ $("#job-string").val(schedule + " " + job_command); return schedule + " " + job_command; @@ -155,3 +226,83 @@ function set_schedule(){ job_string(); } // popup management ends + + +/**************** Template Action s***************/ + +function newTemplate(){ + schedule = "" + template_command = "" + $("#template-minute").val("*"); + $("#template-hour").val("*"); + $("#template-day").val("*"); + $("#template-month").val("*"); + $("#template-week").val("*"); + + $("#template-popup").modal("show"); + $("#template-name").val(""); + $("#template-command").val(""); + template_string(); + $("#template-save").unbind("click"); // remove existing events attached to this + $("#template-save").click(function(){ + // TODO good old boring validations + $.post(routes.save_template, {name: $("#template-name").val(), command: template_command , schedule: schedule, _id: -1}, function(){ + location.reload(); + }) + }); + + $('#template-remove').unbind('click').hide(); +} + +function editTemplate(_id){ + var template = findTemplate(_id); + if(template){ + $("#template-popup").modal("show"); + $("#template-name").val(template.name); + $("#template-command").val(template.command); + + // if macro not used + if(template.schedule && template.schedule.indexOf("@") != 0){ + var components = template.schedule.split(" "); + $("#template-minute").val(components[0]); + $("#template-hour").val(components[1]); + $("#template-day").val(components[2]); + $("#template-month").val(components[3]); + $("#template-week").val(components[4]); + } + schedule = template.schedule; + template_command = template.command; + + template_string(); + } + + $("#template-save").unbind("click"); // remove existing events attached to this + $("#template-save").click(function(){ + // TODO good old boring validations + $.post(routes.save_template, {name: $("#template-name").val(), command: template_command , schedule: schedule, _id: _id}, function(){ + location.reload(); + }) + }); + + $('#template-remove') + .show() + .unbind('click') + .click(function() { + if(window.confirm('Are you sure you want to delete this template?')) { + $.post(routes.remove_template, {_id:_id}, function() { + location.reload(); + }); + } + }); + +} + +function set_template_schedule(){ + schedule = $("#template-minute").val() + " " +$("#template-hour").val() + " " +$("#template-day").val() + " " +$("#template-month").val() + " " +$("#template-week").val(); + template_string(); +} + +function template_string(){ + $("#template-string").val(schedule + " " + template_command); + return schedule + " " + template_command; +} diff --git a/routes.js b/routes.js index 15a629c..3dd9a56 100644 --- a/routes.js +++ b/routes.js @@ -13,4 +13,6 @@ exports.routes = { "import": "/import", // this is import from database "import_crontab": "/import_crontab", // this is from existing crontab "logger": "/logger", + "save_template" : "/save_template", + "remove_template" : "/remove_template", } diff --git a/views/index.ejs b/views/index.ejs index 6073ac7..adc4f8b 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -12,6 +12,7 @@ // initialize tooltips $('[data-toggle="tooltip"]').tooltip(); crontabs = JSON.parse('<%- crontabs.replace(/\\\\/g, "\\\\\\\\").replace(/\\\"/g,"\\\\\"") %>'); + templates = <%- JSON.stringify(templates) %>; routes = JSON.parse('<%- routes %>'); $("#env_vars").val(`<%- env %>`); }) @@ -39,8 +40,7 @@ - - + @@ -57,17 +57,29 @@ - + - - +
IdName Job Time Last Modified <%= index %>. <% index += 1 %> - <%= crontab._id %> - <% if (crontab.name) { %> - + <%= crontab.name %> + + <% } else { %> + <%= crontab._id %> <% } %> + + + + <% if(crontab.command_template && templatesById[crontab.command_template]) { %> + <%= templatesById[crontab.command_template].name %> + + + <% } else { %> + <%= crontab.command %> + <% } %> + + <%= crontab.command %> <%= crontab.schedule %><%= crontab.timestamp %><%= moment(new Date(crontab.timestamp)).fromNow() %> diff --git a/views/navbar.ejs b/views/navbar.ejs index e653059..7818824 100644 --- a/views/navbar.ejs +++ b/views/navbar.ejs @@ -14,16 +14,30 @@ + + + From e28c9b54b940e6cbd1359536ccacec1e30678434 Mon Sep 17 00:00:00 2001 From: Dan Krieger Date: Mon, 9 May 2016 15:42:36 -0700 Subject: [PATCH 2/5] Support for backup, import, export of templates --- app.js | 40 +++++++++++++++++++++++----------------- crontab.js | 35 ++++++++++++++++++++++++++++++----- package.json | 1 + restore.js | 35 +++++++++++++++++++++++++++-------- 4 files changed, 81 insertions(+), 30 deletions(-) diff --git a/app.js b/app.js index b8b580f..2ccfa64 100755 --- a/app.js +++ b/app.js @@ -108,11 +108,18 @@ app.get(routes.backup, function(req, res) { app.get(routes.restore, function(req, res) { // get all the crontabs - restore.crontabs(req.query.db, function(docs){ + restore.loadBackupFile(req.query.db, function(docs, templates) { res.render('restore', { routes : JSON.stringify(routes), crontabs : JSON.stringify(docs), + templates: templates, + templatesById: templates.reduce(function(memo, t) { + memo[t._id] = t; + return memo; + }, {}), backups : crontab.get_backup_names(), + env : crontab.get_env(), + moment: moment, db: req.query.db }); }); @@ -129,31 +136,30 @@ app.get(routes.restore_backup, function(req, res) { }) app.get(routes.export, function(req, res) { - var file = __dirname + '/crontabs/crontab.db'; + var backupData = crontab.backup_data(); - var filename = path.basename(file); - var mimetype = mime.lookup(file); + res.setHeader('Content-disposition', 'attachment; filename=crontab_ui_backup.json'); + res.setHeader('Content-type', 'application/json'); - res.setHeader('Content-disposition', 'attachment; filename=' + filename); - res.setHeader('Content-type', mimetype); - - var filestream = fs.createReadStream(file); - filestream.pipe(res); -}) + res.end(JSON.stringify(backupData)); +}); app.post(routes.import, function(req, res) { var fstream; req.pipe(req.busboy); req.busboy.on('file', function (fieldname, file, filename) { - fstream = fs.createWriteStream(__dirname + '/crontabs/crontab.db'); - file.pipe(fstream); - fstream.on('close', function () { + + file.on('data', function(data) { + crontab.restore_data(JSON.parse(data.toString('utf8'))); crontab.reload_db(); - res.redirect(routes.root); - }); - }); -}) + }); + }); + + req.busboy.on('finish', function() { + res.redirect(routes.root); + }) +}); app.get(routes.import_crontab, function(req, res) { crontab.import_crontab() diff --git a/crontab.js b/crontab.js index 143dfd6..243807d 100644 --- a/crontab.js +++ b/crontab.js @@ -1,8 +1,12 @@ //load database + +exports.crontab_db_file = __dirname + '/crontabs/crontab.db'; +exports.templates_db_file = __dirname + '/crontabs/templates.db'; + var Datastore = require('nedb'); var db = { - crontabs: new Datastore({ filename: __dirname + '/crontabs/crontab.db', autoload: true }), - templates: new Datastore({ filename: __dirname + '/crontabs/templates.db', autoload: true }), + crontabs: new Datastore({ filename: exports.crontab_db_file, autoload: true }), + templates: new Datastore({ filename: exports.templates_db_file, autoload: true }), }; var exec = require('child_process').exec; var fs = require('fs'); @@ -166,18 +170,39 @@ exports.get_backup_names = function(){ return backups; } -exports.backup = function(){ +exports.backup_data = function() { //TODO check if it failed - fs.createReadStream( __dirname + '/crontabs/crontab.db').pipe(fs.createWriteStream( __dirname + '/crontabs/backup ' + (new Date()).toString().replace("+", " ") + '.db')); + var crontabData = fs.readFileSync( exports.crontab_db_file ).toString('utf8') + var templateData = fs.readFileSync( exports.templates_db_file ).toString('utf8') + + return { + version: 1, + crontabs: crontabData, + templates: templateData, + }; +} + +exports.backup = function(){ + var backupFile = __dirname + '/crontabs/backup ' + (new Date()).toString().replace("+", " ") + '.db'; + + fs.writeFileSync(backupFile, JSON.stringify(exports.backup_data())); } exports.restore = function(db_name){ - fs.createReadStream( __dirname + '/crontabs/' + db_name).pipe(fs.createWriteStream( __dirname + '/crontabs/crontab.db')); + exports.restore_data(JSON.parse(fs.readFileSync( __dirname + '/crontabs/' + db_name ))) +} + +exports.restore_data = function(backupData) { + fs.writeFileSync(exports.crontab_db_file, backupData.crontabs); + fs.writeFileSync(exports.templates_db_file, backupData.templates); + db.crontabs.loadDatabase(); // reload the database + db.templates.loadDatabase(); } exports.reload_db = function(){ db.crontabs.loadDatabase(); + db.templates.loadDatabase(); } exports.get_env = function(){ diff --git a/package.json b/package.json index 425f9f4..cee0e0e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "mime": "latest", "moment": "^2.13.0", "nedb": "latest", + "tmp": "0.0.28", "underscore": "^1.8.3" }, "engines": { diff --git a/restore.js b/restore.js index 637fe41..91348cd 100644 --- a/restore.js +++ b/restore.js @@ -3,15 +3,34 @@ var Datastore = require('nedb'); var exec = require('child_process').exec; var fs = require('fs'); +var tmp = require('tmp'); -exports.crontabs = function(db_name, callback){ - var db = new Datastore({ filename: __dirname + '/crontabs/' + db_name }); - db.loadDatabase(function (err) { - }); - db.find({}).sort({ created: -1 }).exec(function(err, docs){ - callback(docs); - }); -} +exports.loadBackupFile = function(db_name, callback) { + + console.log(__dirname + '/crontabs/' + db_name); + + var backupFileData = fs.readFileSync(__dirname + '/crontabs/' + db_name).toString('utf8'); + console.log(backupFileData); + var data = JSON.parse(backupFileData); + + + var crontabFile = tmp.fileSync(); + var templateFile = tmp.fileSync(); + + fs.writeFileSync(crontabFile.name, data.crontabs); + fs.writeFileSync(templateFile.name, data.templates); + + var crontabDB = new Datastore({ filename: crontabFile.name , autoload: true }); + var templateDB = new Datastore({ filename: templateFile.name , autoload: true }); + + crontabDB.find({}).sort({ created: -1 }).exec(function(err, docs){ + templateDB.find({}).sort({ name: 1 }).exec(function(err, templates) { + + callback(docs, templates); + }) + }); + +}; exports.delete = function(db_name){ fs.unlink(__dirname + '/crontabs/' + db_name); From 591cbd001284abb7bc6ea6cd1cc4dde33a25e823 Mon Sep 17 00:00:00 2001 From: Dan Krieger Date: Tue, 10 May 2016 12:10:50 -0700 Subject: [PATCH 3/5] Add listener setting, sort by name --- app.js | 5 +++-- crontab.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 2ccfa64..e6c6a7d 100755 --- a/app.js +++ b/app.js @@ -31,6 +31,7 @@ app.set('views', __dirname + '/views'); //set port app.set('port', (process.env.PORT || 8000)); +app.set('listen', (process.env.LISTEN || '0.0.0.0')); app.get(routes.root, function(req, res) { // get all the crontabs @@ -177,6 +178,6 @@ app.get(routes.logger, function(req, res) { // app.use('/scripts/moment.js', express.static(__dirname + '/node_modules/moment/min/moment.min.js')); -app.listen(app.get('port'), function() { - console.log("Crontab UI is running at localhost:" + app.get('port')) +app.listen(app.get('port'), app.get('listen'), function() { + console.log("Crontab UI is running at " + app.get('listen') + ":" + app.get('port')) }) diff --git a/crontab.js b/crontab.js index 243807d..6edda46 100644 --- a/crontab.js +++ b/crontab.js @@ -60,7 +60,7 @@ exports.remove = function(_id){ } exports.crontabs = function(callback) { exports.templates(function(templates) { - db.crontabs.find({}).sort({ created: -1 }).exec(function(err, docs){ + db.crontabs.find({}).sort({ name: 1 }).exec(function(err, docs){ for(var i=0; i Date: Tue, 19 Jul 2016 11:10:12 -0700 Subject: [PATCH 4/5] Adding datatables --- views/index.ejs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/views/index.ejs b/views/index.ejs index adc4f8b..a6c5b16 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -3,8 +3,11 @@ Crontab UI - + + + + From 04c3264dd650c04e06b006908069fb3c54bcce6b Mon Sep 17 00:00:00 2001 From: Dan Krieger Date: Tue, 19 Jul 2016 11:16:50 -0700 Subject: [PATCH 5/5] Better datatable ordering options --- views/index.ejs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/views/index.ejs b/views/index.ejs index a6c5b16..cdac035 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -44,6 +44,7 @@ + @@ -63,6 +64,10 @@ + +
# Name Job Time <%= index %>. <% index += 1 %> + + <% if (crontab.name) { %> <%= crontab.name %> @@ -111,7 +116,17 @@ <% include popup.ejs %>