Support for job templates

v0.1.7
Dan Krieger 9 years ago
parent 3a10b1f8a0
commit 70ebf96e3e

@ -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'))
})

@ -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<docs.length; i++){
if(docs[i].schedule == "@reboot")
docs[i].next = "Next Reboot"
else
docs[i].next = cron_parser.parseExpression(docs[i].schedule).next().toString();
}
callback(docs);
db.crontabs.remove({_id: _id}, {});
}
exports.crontabs = function(callback) {
exports.templates(function(templates) {
db.crontabs.find({}).sort({ created: -1 }).exec(function(err, docs){
for(var i=0; i<docs.length; i++){
var doc = docs[i];
if(doc.command_template) {
var template = _.findWhere(templates, { _id: doc.command_template });
if(template) {
doc.command = exports.renderTemplateCommand(template, doc);
doc.schedule = template.schedule;
}
}
if(doc.schedule == "@reboot")
doc.next = "Next Reboot"
else if(doc.schedule)
doc.next = cron_parser.parseExpression(doc.schedule).next().toString();
}
callback(docs);
});
})
}
exports.templates = function(callback){
db.templates.find({}).sort({ name: 1 }).exec(function(err, templates){
callback(templates);
});
}
exports.renderTemplateCommand = function(template, cronjob) {
var vars = cronjob.vars || {};
return template.command.replace(/{[a-zA-Z]+}/g, function(s) {
return vars[s.substring(1, s.length - 1)] || 'undefined';
});
};
exports.create_new_template = function(name, command, schedule){
var tab = crontab_template(name, command, schedule);
tab.created = new Date().valueOf();
db.templates.insert(tab);
}
exports.update_template = function(data){
db.templates.update({_id: data._id}, crontab_template(data.name, data.command, data.schedule));
}
exports.remove_template = function(_id) {
db.templates.remove({_id: _id}, {});
}
exports.set_crontab = function(env_vars){
exports.crontabs( function(tabs){
var crontab_string = "";
@ -115,11 +173,11 @@ exports.backup = function(){
exports.restore = function(db_name){
fs.createReadStream( __dirname + '/crontabs/' + db_name).pipe(fs.createWriteStream( __dirname + '/crontabs/crontab.db'));
db.loadDatabase(); // reload the database
db.crontabs.loadDatabase(); // reload the database
}
exports.reload_db= function(){
db.loadDatabase();
exports.reload_db = function(){
db.crontabs.loadDatabase();
}
exports.get_env = function(){

@ -7,19 +7,21 @@
"start": "node app.js"
},
"dependencies": {
"express": "latest",
"ejs": "latest",
"nedb": "latest",
"body-parser": "latest",
"mime": "latest",
"connect-busboy": "latest",
"cron-parser": "latest",
"connect-busboy": "latest"
"ejs": "latest",
"express": "latest",
"mime": "latest",
"moment": "^2.13.0",
"nedb": "latest",
"underscore": "^1.8.3"
},
"engines": {
"node": "latest"
},
"bin": {
"crontab-ui": "bin/crontab-ui.js"
"bin": {
"crontab-ui": "bin/crontab-ui.js"
},
"repository": {
"type": "git",

@ -60,9 +60,11 @@ function editJob(_id){
job = crontab;
});
if(job){
current_job = job;
$("#job").modal("show");
$("#job-name").val(job.name);
$("#job-command").val(job.command);
$("#job-command-template").val(job.command_template);
// if macro not used
if(job.schedule.indexOf("@") != 0){
var components = job.schedule.split(" ");
@ -74,22 +76,87 @@ function editJob(_id){
}
schedule = job.schedule;
job_command = job.command;
console.log(job.logging)
if (job.logging && job.logging != "false")
$("#job-logging").prop("checked", true);
job_string();
setTemplateVariables(job.command_template);
} else {
setTemplateVariables(null);
}
$("#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: _id, logging: $("#job-logging").prop("checked")}, function(){
$.post(routes.save, {name: $("#job-name").val(), command: job_command , schedule: schedule, _id: _id, logging: $("#job-logging").prop("checked"), command_template: $('#job-command-template').val(), vars: current_job.vars }, function(){
location.reload();
})
});
}
function findTemplate(templateId) {
var template = null;
templates.forEach(function(tmpl) {
if(tmpl._id == templateId) {
template = tmpl;
}
});
return template;
}
function setTemplateVariables(templateId) {
var template = findTemplate(templateId);
$('#job-command-variables-wrapper').toggle(!!template);
$('#job-command-wrapper').toggle(!template);
if(template) {
job_command = renderTemplateCommand(templateId);
schedule = template.schedule;
var $jobVariables = $('#job-variables-list').empty(),
vars = current_job.vars || {};
findCommandVariables(template.command).forEach(function(varName) {
var $input = $('<input type="text" class="form-control job-variables" />').val(vars[varName]);
$jobVariables.append('<label>'+varName+'</label>');
$jobVariables.append($input);
$jobVariables.append('<br />');
$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;
}

@ -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",
}

@ -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 @@
<table class="table">
<tr>
<th>Id</th>
<th></th>
<th>Name</th>
<th>Job</th>
<th>Time</th>
<th>Last Modified</th>
@ -57,17 +57,29 @@
<td>
<%= index %>.
<% index += 1 %>
<%= crontab._id %>
</td>
<td>
<% if (crontab.name) { %>
<a class="btn" data-toggle="tooltip" data-placement="right" title="<%= crontab.name %>"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> </a>
<%= crontab.name %>
<a class="btn" data-toggle="tooltip" data-placement="right" title="<%= crontab._id %>"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> </a>
<% } else { %>
<%= crontab._id %>
<% } %>
</td>
<td>
<% if(crontab.command_template && templatesById[crontab.command_template]) { %>
<%= templatesById[crontab.command_template].name %>
<a class="btn" data-toggle="tooltip" data-placement="right" title="<%= crontab.command %>"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> </a>
<% } else { %>
<%= crontab.command %>
<% } %>
</td>
<td><%= crontab.command %></td>
<td><span style="cursor:pointer" data-toggle="tooltip" data-placement="bottom" title="<%= crontab.next %>"><%= crontab.schedule %></span></td>
<td style="width:20%"><%= crontab.timestamp %></td>
<td style="width:20%" title="<%= crontab.timestamp %>"><%= moment(new Date(crontab.timestamp)).fromNow() %></td>
<td>
<!-- controls based on crontab state -->

@ -14,16 +14,30 @@
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Templates <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<% if (templates.length > 0){ %>
<% templates.forEach(function(template){ %>
<li><a onclick="editTemplate('<%= template._id %>')"><%= template.name %></a></li>
<% }) %>
<li class="divider"></li>
<% } %>
<li><a onclick="newTemplate();"><i class="fa fa-plus"></i> Add Template</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Backups <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<% if (backups.length == 0){ %>
<li><a href="#">None</a></li>
<% } else { %>
<% backups.forEach(function(file){ %>
<li><a href="<%= JSON.parse(routes).restore %>?db=<%= file %>"><%= file %></a></li>
<% }) %>
<% } %>
<% if (backups.length == 0){ %>
<li><a href="#">None</a></li>
<% } else { %>
<% backups.forEach(function(file){ %>
<li><a href="<%= JSON.parse(routes).restore %>?db=<%= file %>"><%= file %></a></li>
<% }) %>
<% } %>
</ul>
</li>
</ul>

@ -42,34 +42,55 @@
<div class="modal-body" id="job-body">
<label>Name (Optional)</label>
<input type='text' class='form-control' id='job-name'/><br />
<label>Command</label>
<input type='text' class='form-control' id='job-command' onkeyup="job_command = $(this).val(); job_string();"/><br />
<label>Quick Schedule</label><br />
<a class="btn btn-primary" onclick="schedule = '@reboot'; job_string();">Startup</a>
<a class="btn btn-primary" onclick="schedule = '@hourly'; job_string();">Hourly</a>
<a class="btn btn-primary" onclick="schedule = '@daily'; job_string();">Daily</a>
<a class="btn btn-primary" onclick="schedule = '@weekly'; job_string();">Weekly</a>
<a class="btn btn-primary" onclick="schedule = '@monthly'; job_string();">Monthly</a>
<a class="btn btn-primary" onclick="schedule = '@yearly'; job_string();">Yearly</a><br /><br />
<label>Command Template</label>
<select id="job-command-template" class="form-control" onchange="setTemplateVariables(this.value)">
<option value="">No Template</option>
<% templates.forEach(function(template) { %>
<option value="<%= template._id %>"><%= template.name %></option>
<% }) %>
</select><br />
<div id="job-command-wrapper">
<label>Command</label>
<input type='text' class='form-control' id='job-command' onkeyup="job_command = $(this).val(); job_string();"/><br />
<label>Quick Schedule</label><br />
<a class="btn btn-primary" onclick="schedule = '@reboot'; job_string();">Startup</a>
<a class="btn btn-primary" onclick="schedule = '@hourly'; job_string();">Hourly</a>
<a class="btn btn-primary" onclick="schedule = '@daily'; job_string();">Daily</a>
<a class="btn btn-primary" onclick="schedule = '@weekly'; job_string();">Weekly</a>
<a class="btn btn-primary" onclick="schedule = '@monthly'; job_string();">Monthly</a>
<a class="btn btn-primary" onclick="schedule = '@yearly'; job_string();">Yearly</a><br /><br />
<div class="row">
<div class="col-md-2">Minute</div>
<div class="col-md-2">Hour</div>
<div class="col-md-2">Day</div>
<div class="col-md-2">Month</div>
<div class="col-md-2">Week</div>
</div>
<div class="row">
<div class="col-md-2"><input type="text" class="form-control" id="job-minute" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-hour" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-day" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-month" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-week" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><a class="btn btn-primary" onclick="set_schedule();">Set</a></div>
</div>
<br />
<br />
</div>
<div id="job-command-variables-wrapper" style="display: none">
<label>Command Variables</label>
<div id="job-variables-list">
</div>
</div>
<div class="row">
<div class="col-md-2">Minute</div>
<div class="col-md-2">Hour</div>
<div class="col-md-2">Day</div>
<div class="col-md-2">Month</div>
<div class="col-md-2">Week</div>
</div>
<div class="row">
<div class="col-md-2"><input type="text" class="form-control" id="job-minute" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-hour" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-day" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-month" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="job-week" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><a class="btn btn-primary" onclick="set_schedule();">Set</a></div>
</div>
<br />
<br />
<label>Job</label>
<input type='text' class='form-control' id='job-string' disabled='disabled'/><br />
<label><input type="checkbox" id="job-logging" style="position:relative;top:2px"/> Enable error logging.</label>
@ -81,3 +102,55 @@
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Template -->
<div class="modal fade" id="template-popup">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="template-title">Template</h4>
</div>
<div class="modal-body" id="template-body">
<label>Name</label>
<input type='text' class='form-control' id='template-name'/><br />
<label>Command</label>
<input type='text' class='form-control' id='template-command' onkeyup="template_command = $(this).val(); template_string();"/><br />
<label>Quick Schedule</label><br />
<a class="btn btn-primary" onclick="schedule = '@reboot'; template_string();">Startup</a>
<a class="btn btn-primary" onclick="schedule = '@hourly'; template_string();">Hourly</a>
<a class="btn btn-primary" onclick="schedule = '@daily'; template_string();">Daily</a>
<a class="btn btn-primary" onclick="schedule = '@weekly'; template_string();">Weekly</a>
<a class="btn btn-primary" onclick="schedule = '@monthly'; template_string();">Monthly</a>
<a class="btn btn-primary" onclick="schedule = '@yearly'; template_string();">Yearly</a><br /><br />
<div class="row">
<div class="col-md-2">Minute</div>
<div class="col-md-2">Hour</div>
<div class="col-md-2">Day</div>
<div class="col-md-2">Month</div>
<div class="col-md-2">Week</div>
</div>
<div class="row">
<div class="col-md-2"><input type="text" class="form-control" id="template-minute" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="template-hour" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="template-day" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="template-month" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><input type="text" class="form-control" id="template-week" value="*" onclick="this.select();"/></div>
<div class="col-md-2"><a class="btn btn-primary" onclick="set_template_schedule();">Set</a></div>
</div>
<br />
<br />
<label>Job</label>
<input type='text' class='form-control' id='template-string' disabled='disabled'/><br />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" id="template-remove">Delete Template</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" id="template-save">Save</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

Loading…
Cancel
Save