Added new web-stuff
git-svn-id: https://duplicati.googlecode.com/svn/trunk@1322 59da171f-624f-0410-aa54-27559c288bec
@@ -0,0 +1,65 @@
|
||||
Ext.application({
|
||||
requires: [
|
||||
'Ext.container.Viewport',
|
||||
'Ext.layout.container.Border',
|
||||
'BackupApp.Service',
|
||||
'BackupApp.Utility',
|
||||
'BackupApp.ListUtility',
|
||||
'BackupApp.view.AboutWindow'
|
||||
],
|
||||
|
||||
name: 'BackupApp',
|
||||
appFolder: 'backup-app',
|
||||
|
||||
controllers: [
|
||||
'Schedules',
|
||||
'StatuswindowHeader',
|
||||
'StatuswindowFooter'
|
||||
],
|
||||
|
||||
//This object encapsulates all access to the web service
|
||||
service: null,
|
||||
|
||||
launch: function() {
|
||||
|
||||
Ext.log = function(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
//Pretend the app fires events too
|
||||
this.addEvents('current-state-updated', 'count-down-pause-timer');
|
||||
|
||||
//Make a service object
|
||||
this.service = new BackupApp.Service();
|
||||
//Make the service globally accesible
|
||||
BackupApp.service = this.service;
|
||||
BackupApp.instance = this;
|
||||
BackupApp.utility = this.utility;
|
||||
|
||||
//Pretend the app can fire the update event, this helps with initialization in controllers
|
||||
this.service.on('current-state-updated', function(e) {
|
||||
BackupApp.instance.fireEvent('current-state-updated', e)
|
||||
});
|
||||
|
||||
this.service.on('count-down-pause-timer', function(e) {
|
||||
BackupApp.instance.fireEvent('count-down-pause-timer', e)
|
||||
});
|
||||
|
||||
//Fetch the current status
|
||||
this.service.updateCurrentState();
|
||||
|
||||
Ext.create('Ext.container.Viewport', {
|
||||
layout: 'border',
|
||||
items: [{
|
||||
region: 'center',
|
||||
xtype: 'schedulelist'
|
||||
},{
|
||||
region: 'north',
|
||||
xtype: 'statuswindowheader'
|
||||
},{
|
||||
region: 'south',
|
||||
xtype: 'statuswindowfooter'
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
Ext.define('BackupApp.ListUtility', {
|
||||
statics: {
|
||||
|
||||
currentScheduleId: null,
|
||||
|
||||
showContextMenu: function(id, el) {
|
||||
var lst = Ext.getCmp('status-window-schedule-list');
|
||||
var mnu = lst.elementContextMenu;
|
||||
this.currentScheduleId = id;
|
||||
var elx = Ext.get(el);
|
||||
mnu.showBy(elx, 'tr-br?');
|
||||
},
|
||||
|
||||
formatSizeString: function(size) { return BackupApp.Utility.formatSizeString(size); },
|
||||
timeAgo: function(date) { return BackupApp.Utility.timeAgo(date); },
|
||||
|
||||
formatBackupScheduleDetails: function(values)
|
||||
{
|
||||
var metadata = '';
|
||||
|
||||
if (values.MetadataLookup != null)
|
||||
{
|
||||
var metadataItems = [];
|
||||
var mt = values.MetadataLookup;
|
||||
if (mt['source-file-size'] != null)
|
||||
metadataItems.push('<span class="backup-schedule-detail-source-size">Source files: ' + this.formatSizeString(mt['source-file-size']) + '</span>');
|
||||
|
||||
if (mt['total-backup-size'] != null && parseInt(mt['total-backup-size']) > 0)
|
||||
metadataItems.push('<span class="backup-schedule-detail-backup-size">Backup size: ' + this.formatSizeString(mt['total-backup-size']) + '</span>');
|
||||
|
||||
if (mt['assigned-quota-space'] != null || mt['free-quota-space'] != null)
|
||||
{
|
||||
var spaceLeft = null;
|
||||
if (mt['assigned-quota-space'] != null && mt['free-quota-space'] != null && mt['total-backup-size'] != null)
|
||||
spaceLeft = Math.min(parseInt(mt['assigned-quota-space']) - parseInt(mt['total-backup-size']), parseInt(mt['free-quota-space']));
|
||||
else if (mt['assigned-quota-space'] != null && mt['total-backup-size'] != null)
|
||||
spaceLeft = parseInt(mt['assigned-quota-space']) - parseInt(mt['total-backup-size']);
|
||||
else if (mt['free-quota-space'] != null)
|
||||
spaceLeft = parseInt(mt['free-quota-space']);
|
||||
|
||||
var extracls = '';
|
||||
if (mt['total-backup-size'] != null)
|
||||
{
|
||||
//We mark it as low if there is less than 10% free
|
||||
if (spaceLeft < (parseInt(mt['total-backup-size']) * 0.1))
|
||||
extracls = 'class="low-storage-space"';
|
||||
}
|
||||
|
||||
if (spaceLeft != null)
|
||||
metadataItems.push('<span class="backup-schedule-detail-free-size">Free space: <span ' + extracls + '>' + this.formatSizeString(spaceLeft) + '</span></span>');
|
||||
}
|
||||
|
||||
if (mt['changed-file-count'] != null)
|
||||
{
|
||||
var changedFiles = parseInt(mt['changed-file-count']);
|
||||
var str = changedFiles + ' ' + changedFiles == 1 ? "change" : "changes";
|
||||
|
||||
if (mt['last-backup-date'] != null)
|
||||
str += ' since ' + this.timeAgo(new Date(mt['last-backup-date']));
|
||||
|
||||
metadataItems.push('<span class="backup-schedule-detail-change-count">' + str + '</span>');
|
||||
} else if (mt['last-backup-date'] != null || mt['last-backup-completed-time'] != null) {
|
||||
if (mt['last-backup-completed-time'] != null)
|
||||
metadataItems.push('<span class="backup-schedule-detail-last-backup">Last backup: <span class="marker-time-ago" title="' + (new Date(mt['last-backup-completed-time'])).toUTCString() + '"></span>' + this.timeAgo(new Date(mt['last-backup-completed-time'])) + '</span>');
|
||||
else
|
||||
metadataItems.push('<span class="backup-schedule-detail-last-backup">Last backup: <span class="marker-time-ago" title="' + (new Date(mt['last-backup-date'])).toUTCString() + '"></span>' + this.timeAgo(new Date(mt['last-backup-date'])) + '</span>');
|
||||
}
|
||||
|
||||
for(var ix = 0; ix < metadataItems.length; ix++)
|
||||
{
|
||||
/*if (ix != 0)
|
||||
metadata += ", "; */
|
||||
|
||||
metadata += metadataItems[ix];
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata == '')
|
||||
metadata = '<span class="important-information">No backup information was found, <a title="Click here to run the backup now" href="#" onclick="BackupApp.service.runBackup(' + values.ID + ', false)">run a backup</a> to obtain data</span>'
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,228 @@
|
||||
Ext.define('BackupApp.Service', {
|
||||
mixins: {
|
||||
observable: 'Ext.util.Observable'
|
||||
},
|
||||
|
||||
controlUrl: '/control.cgi',
|
||||
pauseCountDownTimer: null,
|
||||
updateRequestParams: {
|
||||
action: 'get-current-state',
|
||||
longpoll: true,
|
||||
lasteventid: '-1',
|
||||
duration: '5m',
|
||||
},
|
||||
updateRequestRunning: false,
|
||||
|
||||
//Bugfix for extjs
|
||||
hasListeners: {},
|
||||
|
||||
//Cache for server responses
|
||||
serverdata: {
|
||||
'current-state': null,
|
||||
'installed-backends': null,
|
||||
'installed-compression-modules': null,
|
||||
'installed-encryption-modules': null,
|
||||
'installed-generic-modules': null
|
||||
},
|
||||
|
||||
constructor: function(config) {
|
||||
this.addEvents('current-state-updated', 'count-down-pause-timer');
|
||||
},
|
||||
|
||||
updateCurrentState: function(force) {
|
||||
if (this.updateRequestRunning && !force)
|
||||
return;
|
||||
|
||||
var params = force ? { action: 'get-current-state' } : this.updateRequestParams;
|
||||
var timeout = force ? 5000 : (1000 * ((60 * 5) + 5));
|
||||
|
||||
if (!force)
|
||||
this.updateRequestRunning = true;
|
||||
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: params,
|
||||
timeout: timeout,
|
||||
scope: this,
|
||||
success: function(response) {
|
||||
var res = eval('(' + response.responseText + ')');
|
||||
if (res != null)
|
||||
{
|
||||
Ext.log('Got update: ' + res.LastEventID + ', last: ' + this.updateRequestParams.lasteventid);
|
||||
var realUpdate = this.updateRequestParams.lasteventid != res.LastEventID;
|
||||
this.serverdata['current-state'] = res;
|
||||
this.updateRequestParams.lasteventid = res.LastEventID;
|
||||
|
||||
//Register next request
|
||||
if (!force)
|
||||
{
|
||||
this.updateRequestRunning = false;
|
||||
this.updateCurrentState();
|
||||
}
|
||||
|
||||
if (realUpdate)
|
||||
this.fireEvent('current-state-updated', this.serverdata['current-state']);
|
||||
if (this.getExpectedPauseEnd() != null)
|
||||
this.startPauseCountDownTimer();
|
||||
}
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
//TODO: Grey out or similar
|
||||
|
||||
|
||||
//Register next request
|
||||
if (!force)
|
||||
{
|
||||
Ext.log('Failure in longpoll: ' + response.status + ', timeout: ' + response.timedout);
|
||||
this.updateRequestRunning = false;
|
||||
this.updateCurrentState();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startPauseCountDownTimer: function() {
|
||||
if (this.pauseCountDownTimer == null)
|
||||
this.pauseCountDownTimer = window.setTimeout('BackupApp.service.onPauseCountDownTimer()', 500);
|
||||
},
|
||||
|
||||
onPauseCountDownTimer: function() {
|
||||
this.pauseCountDownTimer = null;
|
||||
var pauseEnd = this.getExpectedPauseEnd();
|
||||
if (pauseEnd == null) {
|
||||
this.fireEvent('count-down-pause-timer', 0);
|
||||
} else {
|
||||
this.fireEvent('count-down-pause-timer', parseInt((pauseEnd.getTime() - new Date().getTime()) / 1000));
|
||||
this.startPauseCountDownTimer();
|
||||
}
|
||||
},
|
||||
|
||||
getExpectedPauseEnd: function() {
|
||||
var pauseEnd = this.serverdata['current-state'].EstimatedPauseEnd;
|
||||
if (pauseEnd != null) {
|
||||
pauseEnd = BackupApp.Utility.parseJsonDate(pauseEnd);
|
||||
if (pauseEnd != null && pauseEnd.getTime() > new Date().getTime())
|
||||
return pauseEnd;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getSchedulePosition: function(id) {
|
||||
var state = this.serverdata['current-state'];
|
||||
if (state == null)
|
||||
return -1;
|
||||
|
||||
if (id == state.ActiveScheduleId)
|
||||
return 0;
|
||||
else if (state.SchedulerQueueIds != null)
|
||||
for(var i = 0; i < state.SchedulerQueueIds.length; i++)
|
||||
if (state.SchedulerQueueIds[i] == id)
|
||||
return i+1;
|
||||
|
||||
return -1;
|
||||
},
|
||||
|
||||
pause: function(duration) {
|
||||
var self = this;
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: {
|
||||
action: 'send-command',
|
||||
command: 'pause',
|
||||
duration: duration
|
||||
},
|
||||
success: function(response) { }
|
||||
});
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
var self = this;
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: {
|
||||
action: 'send-command',
|
||||
command: 'resume'
|
||||
},
|
||||
success: function(response) { }
|
||||
});
|
||||
},
|
||||
|
||||
isPaused: function() {
|
||||
var state = this.serverdata['current-state'];
|
||||
if (state != null)
|
||||
return state.ProgramState != 'Running';
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
togglePause: function() {
|
||||
if (this.isPaused())
|
||||
this.resume();
|
||||
else
|
||||
this.pause();
|
||||
},
|
||||
|
||||
getInstalledCompressionModules: function(callback) {
|
||||
if (this.serverdata['installed-compression-modules'] == null) {
|
||||
var self = this;
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: { action: 'list-installed-compression-modules' },
|
||||
success: function(response) {
|
||||
self.serverdata['installed-compression-modules'] = eval('(' + response.responseText + ')');
|
||||
callback(self.serverdata['installed-compression-modules']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(this.serverdata['installed-compression-modules']);
|
||||
}
|
||||
},
|
||||
|
||||
getInstalledGenericModules: function(callback) {
|
||||
if (this.serverdata['installed-generic-modules'] == null) {
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: { action: 'list-installed-generic-modules' },
|
||||
success: function(response) {
|
||||
self.serverdata['installed-generic-modules'] = eval('(' + response.responseText + ')');
|
||||
callback(self.serverdata['installed-generic-modules']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(this.serverdata['installed-generic-modules']);
|
||||
}
|
||||
},
|
||||
|
||||
getInstalledEncryptionModules: function(callback) {
|
||||
if (this.serverdata['installed-encryption-modules'] == null) {
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: { action: 'list-installed-encryption-modules' },
|
||||
success: function(response) {
|
||||
self.serverdata['installed-encryption-modules'] = eval('(' + response.responseText + ')');
|
||||
callback(self.serverdata['installed-encryption-modules']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(this.serverdata['installed-encryption-modules']);
|
||||
}
|
||||
},
|
||||
|
||||
runBackup: function(id, full) {
|
||||
var self = this;
|
||||
|
||||
Ext.Ajax.request({
|
||||
url: this.controlUrl,
|
||||
params: {
|
||||
action: 'send-command',
|
||||
command: 'run-backup',
|
||||
id: id,
|
||||
full: full
|
||||
},
|
||||
success: function(response) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
Ext.define('BackupApp.Utility', {
|
||||
statics: {
|
||||
|
||||
strings: {
|
||||
terabyteShort: 'TB',
|
||||
gigabyteShort: 'GB',
|
||||
megabyteShort: 'MB',
|
||||
kilobyteShort: 'KB',
|
||||
bytesShort: 'bytes',
|
||||
|
||||
prefixAgo: null,
|
||||
prefixFromNow: "in",
|
||||
suffixAgo: "ago",
|
||||
suffixFromNow: null,
|
||||
seconds: "less than a minute",
|
||||
minute: "about a minute",
|
||||
minutes: "%d minutes",
|
||||
hour: "about an hour",
|
||||
hours: "about %d hours",
|
||||
day: "a day",
|
||||
days: "%d days",
|
||||
month: "about a month",
|
||||
months: "%d months",
|
||||
year: "about a year",
|
||||
years: "%d years",
|
||||
wordSeparator: " "
|
||||
},
|
||||
|
||||
timeAgoTimer: window.setInterval('BackupApp.Utility.updateTimeAgoFields()', 1000 * 150),
|
||||
|
||||
//Simplified version of jquery.timeago
|
||||
timeAgo: function(date) {
|
||||
var distanceMillis = (new Date().getTime() - date.getTime());
|
||||
var strings = this.strings;
|
||||
|
||||
var prefix = strings.prefixAgo;
|
||||
var suffix = strings.suffixAgo;
|
||||
if (distanceMillis < 0) {
|
||||
prefix = strings.prefixFromNow;
|
||||
suffix = strings.suffixFromNow;
|
||||
}
|
||||
|
||||
var seconds = Math.abs(distanceMillis) / 1000;
|
||||
var minutes = seconds / 60;
|
||||
var hours = minutes / 60;
|
||||
var days = hours / 24;
|
||||
var years = days / 365;
|
||||
|
||||
var words = seconds < 45 && strings.seconds.replace(/%d/i, Math.round(seconds)) ||
|
||||
seconds < 90 && strings.minute ||
|
||||
minutes < 45 && strings.minutes.replace(/%d/i, Math.round(minutes)) ||
|
||||
minutes < 90 && strings.hour ||
|
||||
hours < 24 && strings.hours.replace(/%d/i, Math.round(hours)) ||
|
||||
hours < 42 && strings.day ||
|
||||
days < 30 && strings.days.replace(/%d/i, Math.round(days)) ||
|
||||
days < 45 && strings.month ||
|
||||
days < 365 && strings.months.replace(/%d/i, Math.round(days / 30)) ||
|
||||
years < 1.5 && strings.year ||
|
||||
strings.years.replace(/%d/i, Math.round(years));
|
||||
|
||||
var separator = strings.wordSeparator === undefined ? " " : strings.wordSeparator;
|
||||
return Ext.String.trim([prefix, words, suffix].join(separator));
|
||||
},
|
||||
|
||||
updateTimeAgoFields: function() {
|
||||
var parent = Ext.get(document);
|
||||
var els = parent.query('.marker-time-ago');
|
||||
for(var i = 0; i < els.length; i++) {
|
||||
var time = this.parseJsonDate(els[i].title);
|
||||
if (els[i].nextSibling.nodeType == 3)
|
||||
els[i].nextSibling.nodeValue = this.timeAgo(time);
|
||||
else if (els[i].nextSibling.innerText != null)
|
||||
els[i].nextSibling.innerText = this.timeAgo(time);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
parseJsonDate: function(str) {
|
||||
if (str == null)
|
||||
return null;
|
||||
|
||||
if (Ext.String.trim(str).length == 0)
|
||||
return null;
|
||||
|
||||
if (str.indexOf("\/") == 0)
|
||||
{
|
||||
var strtmp = str.replace(/\/Date\((-?\d+(((\+|\-)\d+)|Z|z)?)\)\//gi, "new Date($1)");
|
||||
return eval(strtmp);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Date(str);
|
||||
}
|
||||
},
|
||||
|
||||
formatSecondsAsTime: function(seconds) {
|
||||
seconds = parseInt(seconds);
|
||||
var hours = parseInt(Math.floor(seconds / (60 * 60)));
|
||||
seconds = seconds - (hours * 60 * 60);
|
||||
var minutes = parseInt(Math.floor(seconds / 60));
|
||||
seconds = seconds - (minutes * 60);
|
||||
var res = '';
|
||||
if (hours > 0) {
|
||||
res += hours + ':';
|
||||
if (minutes < 10)
|
||||
res += '0';
|
||||
}
|
||||
|
||||
res += minutes + ':' ;
|
||||
if (seconds < 10)
|
||||
res += '0';
|
||||
res += seconds;
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
formatSizeString: function(size) {
|
||||
size = parseInt(size);
|
||||
if (isNaN(size))
|
||||
return size;
|
||||
|
||||
var strings = this.strings;
|
||||
|
||||
if (size >= 1024 * 1024 * 1024 * 1024)
|
||||
return (size / (1024 * 1024 * 1024 * 1024.0)).toFixed(2) + ' ' + strings.terabyteShort;
|
||||
else if (size >= 1024 * 1024 * 1024)
|
||||
return (size / (1024 * 1024 * 1024.0)).toFixed(2) + ' ' + strings.gigabyteShort;
|
||||
else if (size >= 1024 * 1024)
|
||||
return (size / (1024 * 1024.0)).toFixed(2) + ' ' + strings.megabyteShort;
|
||||
else if (size >= 1024)
|
||||
return (size / (1024.0)).toFixed(2) + ' ' + strings.kilobyteShort;
|
||||
else
|
||||
return size + ' ' + strings.bytesShort;
|
||||
},
|
||||
|
||||
estimatedFreeSpace: function(assignedQuota, freeQuota, backupSize) {
|
||||
if (assignedQuota != null && freeQuota != null && backupSize != null)
|
||||
return Math.min(parseInt(assignedQuota) - parseInt(backupSize), parseInt(freeQuota));
|
||||
else if (assignedQuota != null && backupSize != null)
|
||||
return parseInt(assignedQuota) - parseInt(backupSize);
|
||||
else if (freeQuota != null)
|
||||
return parseInt(freeQuota);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
Ext.define('BackupApp.controller.Schedules', {
|
||||
extend: 'Ext.app.Controller',
|
||||
stores: [ 'Schedules' ],
|
||||
views: [ 'schedule.List' ],
|
||||
|
||||
refs: [{
|
||||
selector: 'viewport > panel',
|
||||
ref: 'basicPanel'
|
||||
}],
|
||||
|
||||
lastEventID: -1,
|
||||
|
||||
init: function() {
|
||||
this.control({
|
||||
'viewport > panel': {
|
||||
render: this.onPanelRendered
|
||||
}
|
||||
});
|
||||
|
||||
this.application.on({
|
||||
'current-state-updated': function(data) {
|
||||
|
||||
//Update the data store if there is new metadata
|
||||
if (data.LastEventID != this.lastEventID)
|
||||
this.getSchedulesStore().load();
|
||||
|
||||
this.lastEventID = data.LastEventID;
|
||||
},
|
||||
scope: this
|
||||
});
|
||||
},
|
||||
|
||||
onPanelRendered: function() {
|
||||
//console.log('The panel was rendered');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
Ext.define('BackupApp.controller.StatuswindowFooter', {
|
||||
extend: 'Ext.app.Controller',
|
||||
views: [ 'StatuswindowFooter' ],
|
||||
|
||||
init: function() {
|
||||
this.control({
|
||||
'viewport > panel': {
|
||||
render: this.onPanelRendered
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onPanelRendered: function() {
|
||||
//console.log('The footer panel was rendered');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
Ext.define('BackupApp.controller.StatuswindowHeader', {
|
||||
extend: 'Ext.app.Controller',
|
||||
views: [ 'StatuswindowHeader' ],
|
||||
|
||||
refs: [{
|
||||
selector: '#status-window-header-pause-button',
|
||||
ref: 'pauseButton'
|
||||
}, {
|
||||
selector: '#status-window-header-pause-menu',
|
||||
ref: 'pauseMenu',
|
||||
}],
|
||||
|
||||
init: function() {
|
||||
this.application.on({
|
||||
'current-state-updated': function() {
|
||||
var btn = this.getPauseButton();
|
||||
var mnu = this.getPauseMenu();
|
||||
var srv = this.application.service;
|
||||
var util = BackupApp.Utility;
|
||||
|
||||
if (srv.isPaused()) {
|
||||
if (srv.getExpectedPauseEnd() != null)
|
||||
btn.setText('Resume (' + util.formatSecondsAsTime((srv.getExpectedPauseEnd().getTime() - new Date().getTime()) / 1000) + ')');
|
||||
else
|
||||
btn.setText('Resume');
|
||||
btn.setIconCls('backups-paused');
|
||||
mnu.setText('Resume Duplicati');
|
||||
} else {
|
||||
btn.setText('Pause');
|
||||
btn.setIconCls('backups-running');
|
||||
mnu.setText('Pause Duplicati');
|
||||
}
|
||||
},
|
||||
scope: this
|
||||
});
|
||||
|
||||
this.application.on({
|
||||
'count-down-pause-timer': function(data) {
|
||||
var btn = this.getPauseButton();
|
||||
var srv = this.application.service;
|
||||
var util = BackupApp.Utility;
|
||||
if (data <= 0)
|
||||
btn.setText('Pause');
|
||||
else
|
||||
btn.setText('Resume (' + util.formatSecondsAsTime(data) + ')');
|
||||
},
|
||||
scope: this
|
||||
});
|
||||
|
||||
this.control({
|
||||
'viewport > panel': {
|
||||
render: this.onPanelRendered
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onPanelRendered: function() {
|
||||
//console.log('The header panel was rendered');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
Ext.define('BackupApp.model.Schedule', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: ['ID', 'Name', 'Path', 'MetadataLookup', 'NextScheduledTime', 'When', 'ExistsInDb', 'AllowedWeekdays', 'Repeat', 'Weekdays']
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
Ext.define('BackupApp.store.Schedules', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'BackupApp.model.Schedule',
|
||||
autoLoad: true,
|
||||
|
||||
/* Sort order,
|
||||
* first by queue position
|
||||
* second by next schedule time,
|
||||
* third by last backup time
|
||||
* */
|
||||
sorters: [{
|
||||
sorterFn: function(a, b) {
|
||||
var schedulePosA = BackupApp.service.getSchedulePosition(a.get("ID"));
|
||||
var schedulePosB = BackupApp.service.getSchedulePosition(b.get("ID"));
|
||||
|
||||
if(schedulePosA >= 0 && schedulePosB >= 0)
|
||||
{
|
||||
if (schedulePosA == schedulePosB)
|
||||
return 0;
|
||||
else
|
||||
return schedulePosA < schedulePosB ? -1 : 1;
|
||||
}
|
||||
else if (schedulePosA >= 0)
|
||||
return -1;
|
||||
else if (schedulePosB >= 0)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}, {
|
||||
sorterFn: function(a, b) {
|
||||
var nextTimeA = BackupApp.Utility.parseJsonDate(a.get('NextScheduledTime'));
|
||||
var nextTimeB = BackupApp.Utility.parseJsonDate(b.get('NextScheduledTime'));
|
||||
var now = new Date().getTime();
|
||||
|
||||
//Scheduled time in past, but not in queue, so it will not run
|
||||
if (nextTimeA != null && nextTimeA < now)
|
||||
nextTimeA = null;
|
||||
if (nextTimeB != null && nextTimeB < now)
|
||||
nextTimeB = null;
|
||||
|
||||
if (nextTimeA != null && nextTimeB != null)
|
||||
{
|
||||
nextTimeA = nextTimeA.getTime();
|
||||
nextTimeB = nextTimeB.getTime();
|
||||
if (nextTimeA == nextTimeB)
|
||||
return 0;
|
||||
else
|
||||
return nextTimeA < nextTimeB ? -1 : 1;
|
||||
}
|
||||
else if (nextTimeA != null)
|
||||
return -1;
|
||||
else if (nextTimeB != null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}, {
|
||||
sorterFn: function(a, b) {
|
||||
var lastTimeA = BackupApp.Utility.parseJsonDate(a.data.MetadataLookup['last-backup-completed-time']);
|
||||
var lastTimeB = BackupApp.Utility.parseJsonDate(b.data.MetadataLookup['last-backup-completed-time']);
|
||||
if (lastTimeA == null)
|
||||
lastTimeA = BackupApp.Utility.parseJsonDate(a.data.MetadataLookup['last-backup-date']);
|
||||
if (lastTimeB == null)
|
||||
lastTimeB = BackupApp.Utility.parseJsonDate(b.data.MetadataLookup['last-backup-date']);
|
||||
|
||||
if ((lastTimeA != null && lastTimeB != null))
|
||||
{
|
||||
lastTimeA = lastTimeA.getTime();
|
||||
lastTimeB = lastTimeB.getTime();
|
||||
|
||||
if (lastTimeA == lastTimeB)
|
||||
return 0;
|
||||
else
|
||||
return lastTimeA > lastTimeB ? -1 : 1;
|
||||
}
|
||||
else if (lastTimeA != null)
|
||||
return -1;
|
||||
else if (lastTimeB != null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}, {
|
||||
property: 'Name',
|
||||
}],
|
||||
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
url: 'control.cgi?action=list-schedules',
|
||||
reader: {
|
||||
type: 'json'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
Ext.define('BackupApp.view.AboutWindow', {
|
||||
statics: {
|
||||
dialogWindow: null,
|
||||
show: function() {
|
||||
if (this.dialogWindow == null)
|
||||
this.dialogWindow = Ext.create('Ext.window.Window', {
|
||||
title: 'About Duplicati',
|
||||
height: 200,
|
||||
width: 400,
|
||||
modal: true,
|
||||
resizable: false,
|
||||
buttonAlign: 'center',
|
||||
closeAction: 'hide',
|
||||
layout: 'auto',
|
||||
items: [{
|
||||
xtype: 'label',
|
||||
text: 'Duplicati 2.0'
|
||||
}],
|
||||
buttons: [{
|
||||
xtype: 'button',
|
||||
text: 'OK',
|
||||
handler: function() {
|
||||
BackupApp.view.AboutWindow.dialogWindow.hide();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
this.dialogWindow.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
Ext.define('BackupApp.view.StatuswindowFooter' ,{
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias : 'widget.statuswindowfooter',
|
||||
|
||||
title : 'Status line',
|
||||
header: false,
|
||||
border: 0,
|
||||
layout: 'column',
|
||||
cls: 'footer-panel',
|
||||
|
||||
items: [{
|
||||
xtype: 'label',
|
||||
html: 'A <a href="http://www.duplicati.com/news/duplicati132available" target="_blank">new version</a> is available',
|
||||
handler: function() { window.open('https://www.facebook.com/pages/Duplicati/105118456272281') },
|
||||
columnWidth: 1,
|
||||
margin: 10
|
||||
},{
|
||||
xtype: 'button',
|
||||
text: 'Facebook',
|
||||
handler: function() { window.open('https://www.facebook.com/pages/Duplicati/105118456272281') },
|
||||
margin: 10
|
||||
},{
|
||||
xtype: 'button',
|
||||
text: 'Google+',
|
||||
handler: function() { window.open('https://plus.google.com/105271984558189185842/posts') },
|
||||
margin: 10
|
||||
},{
|
||||
xtype: 'button',
|
||||
text: 'Donate!',
|
||||
handler: function() { window.open('https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=paypal%40hexad%2edk&item_name=Duplicati%20Donation&no_shipping=2&no_note=1&tax=0¤cy_code=EUR&bn=PP%2dDonationsBF&charset=UTF%2d8') },
|
||||
margin: 10
|
||||
}],
|
||||
|
||||
initComponent: function() {
|
||||
this.callParent(arguments);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
Ext.define('BackupApp.view.StatuswindowHeader' ,{
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias : 'widget.statuswindowheader',
|
||||
|
||||
title : 'Main App controls',
|
||||
header: false,
|
||||
border: 0,
|
||||
layout: 'column',
|
||||
cls: 'header-panel',
|
||||
|
||||
items: [{
|
||||
xtype: 'splitbutton',
|
||||
cls: 'main-button',
|
||||
text: 'Duplicati',
|
||||
id: 'status-window-header-main-button',
|
||||
margin: 10,
|
||||
|
||||
handler: function() { this.showMenu(); },
|
||||
menu: [{
|
||||
id: 'status-window-header-pause-menu',
|
||||
text: 'Pause Duplicati',
|
||||
handler: function() { BackupApp.service.togglePause(); }
|
||||
},{
|
||||
text: 'Pause for x minutes',
|
||||
menu: [{
|
||||
text: 'Pause for 5 minutes',
|
||||
handler: function() { BackupApp.service.pause('5m'); }
|
||||
},{
|
||||
text: 'Pause for 15 minutes',
|
||||
handler: function() { BackupApp.service.pause('15m'); }
|
||||
},{
|
||||
text: 'Pause for 30 minutes',
|
||||
handler: function() { BackupApp.service.pause('30m'); }
|
||||
},{
|
||||
text: 'Pause for 60 minutes',
|
||||
handler: function() { BackupApp.service.pause('60m'); }
|
||||
}]
|
||||
},{
|
||||
text: '*** Throttle ...'
|
||||
},{
|
||||
xtype: 'menuseparator'
|
||||
},{
|
||||
text: '*** Run backups',
|
||||
menu: [{
|
||||
text: 'All backups'
|
||||
},{
|
||||
text: 'Label: Work'
|
||||
},{
|
||||
text: 'Label: Home'
|
||||
},{
|
||||
text: 'Label: Amazon S3'
|
||||
}]
|
||||
},{
|
||||
xtype: 'menuseparator'
|
||||
},{
|
||||
text: '*** Settings'
|
||||
},{
|
||||
text: 'About',
|
||||
handler: function() { BackupApp.view.AboutWindow.show(); }
|
||||
}]
|
||||
},{
|
||||
xtype: 'label',
|
||||
html: ' ',
|
||||
columnWidth: 1
|
||||
},{
|
||||
xtype: 'panel',
|
||||
layout: 'vbox',
|
||||
align: 'center',
|
||||
margin: 5,
|
||||
border: 0,
|
||||
items: [{
|
||||
xtype: 'label',
|
||||
cls: 'throttle-label',
|
||||
text: '*** Throttle upload'
|
||||
},{
|
||||
xtype: 'slider',
|
||||
width: 200,
|
||||
value: 80,
|
||||
minValue: 0,
|
||||
maxValue: 100
|
||||
}]
|
||||
},{
|
||||
xtype: 'button',
|
||||
cls: 'add-new-backup-button',
|
||||
text: '*** Add new backup',
|
||||
margin: 10
|
||||
},{
|
||||
xtype: 'button',
|
||||
id: 'status-window-header-pause-button',
|
||||
cls: 'pause-backup-button',
|
||||
iconCls: 'backups-paused',
|
||||
text: 'Pause',
|
||||
margin: 10,
|
||||
handler: function() { BackupApp.service.togglePause(); }
|
||||
}],
|
||||
|
||||
initComponent: function() {
|
||||
|
||||
this.callParent(arguments);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
Ext.define('BackupApp.view.schedule.List' ,{
|
||||
extend: 'Ext.grid.Panel',
|
||||
alias : 'widget.schedulelist',
|
||||
|
||||
title : 'All Schedules',
|
||||
header: false,
|
||||
border: 0,
|
||||
id: 'status-window-schedule-list',
|
||||
|
||||
hideHeaders: true,
|
||||
selType: 'rowmodel',
|
||||
|
||||
store: 'Schedules',
|
||||
|
||||
elementTemplateStr:
|
||||
'<span class="backup-schedule-name" title="Click for actions - {Name}" onclick="BackupApp.ListUtility.showContextMenu({ID}, this);"><span class="backup-schedule-name-inner">{Name}<div class="backup-schedule-drop-arrow"></div></span></span>' +
|
||||
'<span class="backup-schedule-details">' +
|
||||
'<span class="backup-schedule-details-inner">' +
|
||||
/* The template for this became un-readable, so now it is a function */
|
||||
'{[BackupApp.ListUtility.formatBackupScheduleDetails(values)]}' +
|
||||
'</span>' +
|
||||
'<tpl if="Path != null && Path != \'\'">' +
|
||||
'<div class="backup-schedule-tags">' +
|
||||
'<div class="schedule-tags-icon"></div><span>Labels: <span>{Path}</span></span>' +
|
||||
'</div>' +
|
||||
'</tpl>' +
|
||||
'</span>',
|
||||
|
||||
|
||||
elementTemplate: null,
|
||||
|
||||
elementContextMenu: Ext.create('Ext.menu.Menu', {
|
||||
floating: true,
|
||||
items: [{
|
||||
text: '*** Show files'
|
||||
},{
|
||||
text: 'Run now',
|
||||
handler: function() {
|
||||
Ext.log('Running backup ' + BackupApp.ListUtility.currentScheduleId);
|
||||
BackupApp.service.runBackup(BackupApp.ListUtility.currentScheduleId, false);
|
||||
}
|
||||
},{
|
||||
text: 'Run a full backup now',
|
||||
handler: function() {
|
||||
Ext.log('Running full backup ' + BackupApp.ListUtility.currentScheduleId);
|
||||
BackupApp.service.runBackup(BackupApp.ListUtility.currentScheduleId, true);
|
||||
}
|
||||
},{
|
||||
text: '*** Cancel'
|
||||
},{
|
||||
xtype: 'menuseparator'
|
||||
},{
|
||||
text: '*** Edit settings'
|
||||
},{
|
||||
text: '*** Delete backup'
|
||||
},{
|
||||
xtype: 'menuseparator'
|
||||
},{
|
||||
text: '*** Create a copy'
|
||||
}]
|
||||
}),
|
||||
|
||||
columns:
|
||||
[
|
||||
{header: 'ScheduleTime', dataIndex: 'NextScheduledTime', flex: 0.8, align: 'center', renderer:
|
||||
function(value, metaData, record) {
|
||||
var schedulePos = BackupApp.service.getSchedulePosition(record.get("ID"));
|
||||
if (schedulePos == 0) {
|
||||
return '<div class="schedule-activity-text schedule-running-info"><div>In progress</div><div><a href="#" onclick="BackupApp.service.cancelJob('+ record.get("ID") +')">Cancel</a></div></div><div class="info-icon schedule-active"></div>';
|
||||
} else if (schedulePos > 0) {
|
||||
return '<div class="schedule-activity-text schedule-queue-info">No. ' + schedulePos + ' in queue</div><div class="info-icon schedule-in-queue"></div>';
|
||||
} else if (value != null) {
|
||||
var scdate = BackupApp.Utility.parseJsonDate(value);
|
||||
if (scdate > new Date().getTime())
|
||||
return '<div class="schedule-activity-text schedule-time-info" title="' + scdate.toLocaleString() + '"><span class="marker-time-ago" title="' + scdate.toUTCString() + '"></span>' + BackupApp.Utility.timeAgo(scdate) + '</div><div class="info-icon schedule-waiting"></div>';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
{header: 'Name', dataIndex: 'Name', flex: 1, align: 'center', renderer:
|
||||
function(value, metaData, record) {
|
||||
return this.elementTemplate.apply(record.data);
|
||||
}
|
||||
},
|
||||
{header: 'Path', dataIndex: 'ID', flex: 0.8, align: 'left', renderer:
|
||||
function(value) { return value; }
|
||||
}
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
this.elementTemplate = new Ext.XTemplate(this.elementTemplateStr, {formatSizeString: function(v) { return BackupApp.Utility.formatSizeString(v); } });
|
||||
this.callParent(arguments);
|
||||
}
|
||||
});
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 717 B |
|
After Width: | Height: | Size: 943 B |
|
After Width: | Height: | Size: 882 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1 @@
|
||||
/* Placeholder for customization */
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Status Window</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="extjs-4.1.0/resources/css/ext-all.css" />
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/common.css" />
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/status-window.css" />
|
||||
<link rel="stylesheet" type="text/css" href="stylesheets/customized.css" />
|
||||
|
||||
<script type="text/javascript" src="extjs-4.1.0/ext-debug.js"></script>
|
||||
<script type="text/javascript" src="app.js"></script>
|
||||
<script type="text/javascript" src="scripts/customized.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
/* Placeholder for customization */
|
||||
@@ -0,0 +1 @@
|
||||
/* Placeholder for customization */
|
||||
@@ -0,0 +1,193 @@
|
||||
.footer-panel .x-panel-body
|
||||
{
|
||||
background-color: #ccffcc;
|
||||
}
|
||||
|
||||
.header-panel .x-panel-body
|
||||
{
|
||||
background-color: #ccffcc;
|
||||
}
|
||||
|
||||
.main-button .x-btn-split, .add-new-backup-button .x-btn-center, .pause-backup-button .x-btn-center
|
||||
{
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.main-button .x-btn-inner, .add-new-backup-button .x-btn-inner, .pause-backup-button .x-btn-inner
|
||||
{
|
||||
font-size: 2em;
|
||||
height: 20px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.main-button .x-btn-icon, .add-new-backup-button .x-btn-icon, .pause-backup-button .x-btn-icon
|
||||
{
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.main-button .x-btn-icon
|
||||
{
|
||||
background-image: url(../icons/main-logo.png);
|
||||
}
|
||||
|
||||
.add-new-backup-button .x-btn-icon
|
||||
{
|
||||
background-image: url(../icons/add-backup.png);
|
||||
}
|
||||
|
||||
.pause-backup-button .backups-paused
|
||||
{
|
||||
background-image: url(../icons/status-play.png);
|
||||
}
|
||||
|
||||
.pause-backup-button .backups-running
|
||||
{
|
||||
background-image: url(../icons/status-pause.png);
|
||||
}
|
||||
|
||||
.throttle-label
|
||||
{
|
||||
font-size: 2em;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.low-storage-space
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
.important-information
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-icon
|
||||
{
|
||||
padding: 8px;
|
||||
margin-right: 4px;
|
||||
margin-top: 40px;
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: 4px;
|
||||
display: inline;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.schedule-activity-text
|
||||
{
|
||||
margin-top: 40px;
|
||||
margin-right: 20px;
|
||||
display: inline;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.schedule-waiting
|
||||
{
|
||||
background: url(../icons/schedule-waiting.png)
|
||||
|
||||
}
|
||||
|
||||
.schedule-active
|
||||
{
|
||||
background: url(../icons/schedule-active.png)
|
||||
|
||||
}
|
||||
|
||||
.schedule-in-queue
|
||||
{
|
||||
background: url(../icons/schedule-in-queue.png)
|
||||
|
||||
}
|
||||
|
||||
.backup-schedule-tags
|
||||
{
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.marker-time-ago
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.backup-schedule-detail-source-size,
|
||||
.backup-schedule-detail-backup-size,
|
||||
.backup-schedule-detail-free-size,
|
||||
.backup-schedule-detail-change-count,
|
||||
.backup-schedule-detail-last-backup,
|
||||
.backup-schedule-tags
|
||||
{
|
||||
display: block;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.backup-schedule-details-inner
|
||||
{
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.backup-schedule-name
|
||||
{
|
||||
display: block;
|
||||
border: 1px lightgrey solid;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
margin-top: 10px;
|
||||
background-color: #aaeeaa;
|
||||
|
||||
-moz-border-radius: 6px;
|
||||
-webkit-border-radius: 6px;
|
||||
-khtml-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.backup-schedule-drop-arrow
|
||||
{
|
||||
background-image: url(../extjs-4.1.0/resources/themes/images/default/button/arrow.gif);
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
background-position-x: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.backup-schedule-name-inner
|
||||
{
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
overflow: hidden;
|
||||
white-space: wrap;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backup-schedule-details
|
||||
{
|
||||
display: block;
|
||||
border: 1px lightgrey solid;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
overflow: auto;
|
||||
white-space: normal;
|
||||
margin-bottom: 10px;
|
||||
background-color: #ccffcc;
|
||||
margin-top: -2px;
|
||||
font-size: 1.2em;
|
||||
|
||||
-moz-border-radius: 6px;
|
||||
-webkit-border-radius: 6px;
|
||||
-khtml-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||