diff --git a/Duplicati/Server/webroot/app.js b/Duplicati/Server/webroot/app.js new file mode 100644 index 000000000..f8bdff582 --- /dev/null +++ b/Duplicati/Server/webroot/app.js @@ -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' + }] + }); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/ListUtility.js b/Duplicati/Server/webroot/backup-app/ListUtility.js new file mode 100644 index 000000000..afa88d26f --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/ListUtility.js @@ -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('Source files: ' + this.formatSizeString(mt['source-file-size']) + ''); + + if (mt['total-backup-size'] != null && parseInt(mt['total-backup-size']) > 0) + metadataItems.push('Backup size: ' + this.formatSizeString(mt['total-backup-size']) + ''); + + 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('Free space: ' + this.formatSizeString(spaceLeft) + ''); + } + + 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('' + str + ''); + } else if (mt['last-backup-date'] != null || mt['last-backup-completed-time'] != null) { + if (mt['last-backup-completed-time'] != null) + metadataItems.push('Last backup: ' + this.timeAgo(new Date(mt['last-backup-completed-time'])) + ''); + else + metadataItems.push('Last backup: ' + this.timeAgo(new Date(mt['last-backup-date'])) + ''); + } + + for(var ix = 0; ix < metadataItems.length; ix++) + { + /*if (ix != 0) + metadata += ", "; */ + + metadata += metadataItems[ix]; + } + } + + if (metadata == '') + metadata = 'No backup information was found, run a backup to obtain data' + + return metadata; + } + } +}) \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/Service.js b/Duplicati/Server/webroot/backup-app/Service.js new file mode 100644 index 000000000..20654d266 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/Service.js @@ -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) { + } + }); + + } + +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/Utility.js b/Duplicati/Server/webroot/backup-app/Utility.js new file mode 100644 index 000000000..c9bc01479 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/Utility.js @@ -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; + } + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/controller/Schedules.js b/Duplicati/Server/webroot/backup-app/controller/Schedules.js new file mode 100644 index 000000000..853a4172d --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/controller/Schedules.js @@ -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'); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js b/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js new file mode 100644 index 000000000..84e580a0f --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js @@ -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'); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js b/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js new file mode 100644 index 000000000..048d10d1a --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js @@ -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'); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/model/Schedule.js b/Duplicati/Server/webroot/backup-app/model/Schedule.js new file mode 100644 index 000000000..19e63bebf --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/model/Schedule.js @@ -0,0 +1,4 @@ +Ext.define('BackupApp.model.Schedule', { + extend: 'Ext.data.Model', + fields: ['ID', 'Name', 'Path', 'MetadataLookup', 'NextScheduledTime', 'When', 'ExistsInDb', 'AllowedWeekdays', 'Repeat', 'Weekdays'] +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/store/Schedules.js b/Duplicati/Server/webroot/backup-app/store/Schedules.js new file mode 100644 index 000000000..307f1a3cb --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/store/Schedules.js @@ -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' + } + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/view/AboutWindow.js b/Duplicati/Server/webroot/backup-app/view/AboutWindow.js new file mode 100644 index 000000000..ac9d12383 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/view/AboutWindow.js @@ -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(); + } + } +}); + diff --git a/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js b/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js new file mode 100644 index 000000000..4e5f58454 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js @@ -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 new version 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); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js b/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js new file mode 100644 index 000000000..43c500982 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js @@ -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); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/backup-app/view/schedule/List.js b/Duplicati/Server/webroot/backup-app/view/schedule/List.js new file mode 100644 index 000000000..c7983b8d9 --- /dev/null +++ b/Duplicati/Server/webroot/backup-app/view/schedule/List.js @@ -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: + '{Name}
' + + '' + + '' + + /* The template for this became un-readable, so now it is a function */ + '{[BackupApp.ListUtility.formatBackupScheduleDetails(values)]}' + + '' + + '' + + '
' + + '
Labels: {Path}' + + '
' + + '
' + + '
', + + + 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 '
In progress
Cancel
'; + } else if (schedulePos > 0) { + return '
No. ' + schedulePos + ' in queue
'; + } else if (value != null) { + var scdate = BackupApp.Utility.parseJsonDate(value); + if (scdate > new Date().getTime()) + return '
' + BackupApp.Utility.timeAgo(scdate) + '
'; + } + + 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); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/icons/add-backup.png b/Duplicati/Server/webroot/icons/add-backup.png new file mode 100644 index 000000000..0940ab07d Binary files /dev/null and b/Duplicati/Server/webroot/icons/add-backup.png differ diff --git a/Duplicati/Server/webroot/icons/hourglass.png b/Duplicati/Server/webroot/icons/hourglass.png new file mode 100644 index 000000000..5b36882bd Binary files /dev/null and b/Duplicati/Server/webroot/icons/hourglass.png differ diff --git a/Duplicati/Server/webroot/icons/hourglass2.png b/Duplicati/Server/webroot/icons/hourglass2.png new file mode 100644 index 000000000..f6964802d Binary files /dev/null and b/Duplicati/Server/webroot/icons/hourglass2.png differ diff --git a/Duplicati/Server/webroot/icons/main-logo.png b/Duplicati/Server/webroot/icons/main-logo.png new file mode 100644 index 000000000..1e291e564 Binary files /dev/null and b/Duplicati/Server/webroot/icons/main-logo.png differ diff --git a/Duplicati/Server/webroot/icons/schedule-active.png b/Duplicati/Server/webroot/icons/schedule-active.png new file mode 100644 index 000000000..f8c8ec683 Binary files /dev/null and b/Duplicati/Server/webroot/icons/schedule-active.png differ diff --git a/Duplicati/Server/webroot/icons/schedule-in-queue.png b/Duplicati/Server/webroot/icons/schedule-in-queue.png new file mode 100644 index 000000000..fb4ebc850 Binary files /dev/null and b/Duplicati/Server/webroot/icons/schedule-in-queue.png differ diff --git a/Duplicati/Server/webroot/icons/schedule-waiting.png b/Duplicati/Server/webroot/icons/schedule-waiting.png new file mode 100644 index 000000000..e2672c206 Binary files /dev/null and b/Duplicati/Server/webroot/icons/schedule-waiting.png differ diff --git a/Duplicati/Server/webroot/icons/status-pause.png b/Duplicati/Server/webroot/icons/status-pause.png new file mode 100644 index 000000000..755b2a9eb Binary files /dev/null and b/Duplicati/Server/webroot/icons/status-pause.png differ diff --git a/Duplicati/Server/webroot/icons/status-play.png b/Duplicati/Server/webroot/icons/status-play.png new file mode 100644 index 000000000..6941af7fa Binary files /dev/null and b/Duplicati/Server/webroot/icons/status-play.png differ diff --git a/Duplicati/Server/webroot/scripts/customized.js b/Duplicati/Server/webroot/scripts/customized.js new file mode 100644 index 000000000..47aa86d62 --- /dev/null +++ b/Duplicati/Server/webroot/scripts/customized.js @@ -0,0 +1 @@ +/* Placeholder for customization */ \ No newline at end of file diff --git a/Duplicati/Server/webroot/status-window.html b/Duplicati/Server/webroot/status-window.html new file mode 100644 index 000000000..668b911fc --- /dev/null +++ b/Duplicati/Server/webroot/status-window.html @@ -0,0 +1,21 @@ + + + + +Status Window + + + + + + + + + + + + + + + + diff --git a/Duplicati/Server/webroot/stylesheets/common.css b/Duplicati/Server/webroot/stylesheets/common.css new file mode 100644 index 000000000..47aa86d62 --- /dev/null +++ b/Duplicati/Server/webroot/stylesheets/common.css @@ -0,0 +1 @@ +/* Placeholder for customization */ \ No newline at end of file diff --git a/Duplicati/Server/webroot/stylesheets/customized.css b/Duplicati/Server/webroot/stylesheets/customized.css new file mode 100644 index 000000000..47aa86d62 --- /dev/null +++ b/Duplicati/Server/webroot/stylesheets/customized.css @@ -0,0 +1 @@ +/* Placeholder for customization */ \ No newline at end of file diff --git a/Duplicati/Server/webroot/stylesheets/status-window.css b/Duplicati/Server/webroot/stylesheets/status-window.css new file mode 100644 index 000000000..771f7de01 --- /dev/null +++ b/Duplicati/Server/webroot/stylesheets/status-window.css @@ -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; +} \ No newline at end of file