* [pbs-devel] [RFC proxmox-backup] create prune simulator
@ 2020-09-17 12:09 Fabian Ebner
0 siblings, 0 replies; only message in thread
From: Fabian Ebner @ 2020-09-17 12:09 UTC (permalink / raw)
To: pbs-devel
A stand-alone ExtJS app that allows experimenting with
different backup schedules and prune parameters.
For performance reasons, the week table does not use
subcomponents, but raw HTML.
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
---
Not sure where in our repos the best place to put this is.
I first tried having a panel for each day, each with a
filtered ChainedStore, but the performance was terrible (a few
seconds delay) even with only 10 weeks. The single panel
approach can handle a lot more.
Still missing/ideas:
* make interface prettier
* colors were just added quickly as a POC,
don't know if background-coloring is the best idea
* for the schedule, add a submit button instead of
continuously updating the view
* allow configuring number of weeks
docs/prune-simulator/extjs | 1 +
docs/prune-simulator/index.html | 13 +
docs/prune-simulator/prune-simulator.js | 500 ++++++++++++++++++++++++
3 files changed, 514 insertions(+)
create mode 120000 docs/prune-simulator/extjs
create mode 100644 docs/prune-simulator/index.html
create mode 100644 docs/prune-simulator/prune-simulator.js
diff --git a/docs/prune-simulator/extjs b/docs/prune-simulator/extjs
new file mode 120000
index 00000000..b71ec6ef
--- /dev/null
+++ b/docs/prune-simulator/extjs
@@ -0,0 +1 @@
+/usr/share/javascript/extjs
\ No newline at end of file
diff --git a/docs/prune-simulator/index.html b/docs/prune-simulator/index.html
new file mode 100644
index 00000000..deb26764
--- /dev/null
+++ b/docs/prune-simulator/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+ <title>Prune Backups Simulator</title>
+
+ <link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
+ <script type="text/javascript" src="extjs/ext-all.js"></script>
+ <script type="text/javascript" src="prune-simulator.js"></script>
+</head>
+<body></body>
+</html>
diff --git a/docs/prune-simulator/prune-simulator.js b/docs/prune-simulator/prune-simulator.js
new file mode 100644
index 00000000..53995930
--- /dev/null
+++ b/docs/prune-simulator/prune-simulator.js
@@ -0,0 +1,500 @@
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+ var console = {
+ dir: function() {},
+ log: function() {}
+ };
+}
+
+Ext.onReady(function() {
+
+ // TODO: allow configuring some of these from within the UI?
+ let NUMBER_OF_WEEKS = 15;
+ let NOW = new Date();
+ let COLORS = {
+ 'keep-last': 'green',
+ 'keep-hourly': 'orange',
+ 'keep-daily': 'yellow',
+ 'keep-weekly': 'red',
+ 'keep-monthly': 'blue',
+ 'keep-yearly': 'purple',
+ };
+ let TEXT_COLORS = {
+ 'keep-last': 'white',
+ 'keep-hourly': 'black',
+ 'keep-daily': 'black',
+ 'keep-weekly': 'white',
+ 'keep-monthly': 'white',
+ 'keep-yearly': 'white',
+ };
+
+ Ext.define('PBS.prunesimulator.CalendarEvent', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.prunesimulatorCalendarEvent',
+
+ editable: true,
+
+ displayField: 'text',
+ valueField: 'value',
+ queryMode: 'local',
+
+ store: {
+ field: [ 'value', 'text'],
+ data: [
+ { value: '0/2:00', text: "Every two hours" },
+ { value: '0/6:00', text: "Every six hours" },
+ { value: '2,22:30', text: "At 02:30 and 22:30" },
+ ],
+ },
+
+ tpl: [
+ '<ul class="x-list-plain"><tpl for=".">',
+ '<li role="option" class="x-boundlist-item">{text}</li>',
+ '</tpl></ul>',
+ ],
+
+ displayTpl: [
+ '<tpl for=".">',
+ '{value}',
+ '</tpl>',
+ ],
+ });
+
+ Ext.define('PBS.prunesimulator.DayOfWeekSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.prunesimulatorDayOfWeekSelector',
+
+ displayField: 'text',
+ valueField: 'value',
+ queryMode: 'local',
+
+ store: {
+ field: ['value', 'text'],
+ data: [
+ { value: 'mon', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[1]) },
+ { value: 'tue', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[2]) },
+ { value: 'wed', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[3]) },
+ { value: 'thu', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[4]) },
+ { value: 'fri', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[5]) },
+ { value: 'sat', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[6]) },
+ { value: 'sun', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[0]) },
+ ],
+ },
+ });
+
+ Ext.define('pbs-prune-list', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'backuptime',
+ type: 'date',
+ dateFormat: 'timestamp',
+ },
+ ]
+ });
+
+ Ext.define('PBS.prunesimulator.WeekTable', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.prunesimulatorWeekTable',
+
+ autoScroll: true,
+ height: 800,
+
+ reload: function() {
+ let me = this;
+ let backups = me.store.data.items;
+
+ let html = '<table>';
+
+ let now = new Date(NOW.getTime());
+ let skip = 7 - parseInt(Ext.Date.format(now, 'N'));
+ let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);
+
+ let bIndex = 0;
+
+ for (i = 0; i < NUMBER_OF_WEEKS; i++) {
+ html += '<tr>';
+
+ for (j = 0; j < 7; j++) {
+ html += '<td style="vertical-align: top;' +
+ 'width: 150px;' +
+ 'border:#000000 1px solid;' +
+ '">';
+
+ let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
+ let currentDay = Ext.Date.format(date, 'd/m/Y');
+
+ let backup = backups[bIndex];
+
+ let isBackupOnDay = function(backup, day) {
+ return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
+ };
+
+ html += '<table><tr><th>' + Ext.Date.format(date, 'D, d M Y') + '</th>';
+
+ while (isBackupOnDay(backup, currentDay)) {
+ html += '<tr><td>';
+
+ let text = Ext.Date.format(backup.data.backuptime, 'H:i');
+ if (backup.data.mark === 'remove') {
+ html += '<div style="text-decoration: line-through; ' +
+ 'font-weight: bold;">' + text + '</div>';
+ } else {
+ let bgColor = COLORS[backup.data.keepName];
+ let textColor = TEXT_COLORS[backup.data.keepName];
+ html += '<div style="background-color: ' + bgColor + '; ' +
+ 'color: ' + textColor + ';">' + text + '</div>';
+ }
+ html += '</td></tr>';
+ backup = backups[++bIndex];
+ }
+ html += '</table>';
+ html += '</div>';
+ html += '</td>';
+ }
+
+ html += '</tr>';
+ }
+
+ me.setHtml(html);
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.store) {
+ throw "no store specified";
+ }
+
+ let reload = function() {
+ me.reload();
+ };
+
+ me.store.on("datachanged", reload);
+
+ me.callParent();
+
+ me.reload();
+ },
+ });
+
+ Ext.define('PBS.PruneSimulatorPanel', {
+ extend: 'Ext.panel.Panel',
+
+ getValues: function() {
+ let me = this;
+
+ let values = {};
+
+ Ext.Array.each(me.query('[isFormField]'), function(field) {
+ let data = field.getSubmitData();
+ Ext.Object.each(data, function(name, val) {
+ values[name] = val;
+ });
+ });
+
+ return values;
+ },
+
+ layout: {
+ type: 'table',
+ columns: 2,
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ this.reload(); // initial load
+ },
+
+ control: {
+ field: { change: 'reload' }
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+
+ let params = view.getValues();
+
+ //TODO check format for schedule
+
+ let [ startTimesStr, interval ] = params['schedule-time'].split('/');
+
+ if (interval) {
+ let [ intervalBig, intervalSmall ] = interval.split(':');
+ if (intervalSmall) {
+ interval = parseInt(intervalBig) * 60 + parseInt(intervalSmall);
+ } else {
+ interval = parseInt(intervalBig); // no ':' means only minutes
+ }
+ }
+
+ let [ startTimesHours, startTimesMinute ] = startTimesStr.split(':');
+ let startTimesHoursArray = startTimesHours.split(',');
+
+ let startTimes = [];
+ startTimesHoursArray.forEach(function(hour) {
+ startTimesMinute = startTimesMinute || '0';
+ startTimes.push([hour, startTimesMinute]);
+ });
+
+
+ let backups = me.populateFromSchedule(params['schedule-weekdays'], startTimes, interval);
+ me.pruneSelect(backups, params);
+
+ view.pruneStore.setData(backups);
+ },
+
+ // backups are sorted descending by date
+ populateFromSchedule: function(weekdays, startTimes, interval) {
+
+ interval = interval || 60 * 60 * 24;
+
+ let weekdayFlags = [
+ weekdays.includes('sun'),
+ weekdays.includes('mon'),
+ weekdays.includes('tue'),
+ weekdays.includes('wed'),
+ weekdays.includes('thu'),
+ weekdays.includes('fri'),
+ weekdays.includes('sat'),
+ ];
+
+ let baseDate = new Date(NOW.getTime());
+
+ let backups = [];
+
+ startTimes.forEach(function(startTime) {
+ baseDate.setHours(startTime[0]);
+ baseDate.setMinutes(startTime[1]);
+ for (i = 0; i < 7 * NUMBER_OF_WEEKS; i++) {
+ let date = Ext.Date.subtract(baseDate, Ext.Date.DAY, i);
+ let weekday = parseInt(Ext.Date.format(date, 'w'));
+ if (weekdayFlags[weekday]) {
+ let backupsToday = [];
+ while (parseInt(Ext.Date.format(date, 'w')) === weekday) {
+ backupsToday.push({
+ backuptime: date,
+ });
+ date = Ext.Date.add(date, Ext.Date.MINUTE, interval);
+ }
+ while (backupsToday.length > 0) {
+ backups.push(backupsToday.pop());
+ }
+ }
+ }
+ });
+
+ return backups;
+ },
+
+ pruneMark: function(backups, keepCount, keepName, idFunc) {
+ if (!keepCount) {
+ return;
+ }
+
+ let alreadyIncluded = {};
+ let newlyIncluded = {};
+ let newlyIncludedCount = 0;
+
+ let finished = false;
+
+ backups.forEach(function(backup) {
+ let mark = backup.mark;
+ let id = idFunc(backup);
+
+ if (finished || alreadyIncluded[id]) {
+ return;
+ }
+
+ if (mark) {
+ if (mark === 'keep') {
+ alreadyIncluded[id] = true;
+ }
+ return;
+ }
+
+ if (!newlyIncluded[id]) {
+ if (newlyIncludedCount >= keepCount) {
+ finished = true;
+ return;
+ }
+ newlyIncluded[id] = true;
+ newlyIncludedCount++;
+ backup.mark = 'keep';
+ backup.keepName = keepName;
+ } else {
+ backup.mark = 'remove';
+ }
+ });
+ },
+
+ // backups need to be sorted descending by date
+ pruneSelect: function(backups, keepParams) {
+ let me = this;
+
+ me.pruneMark(backups, keepParams['keep-last'], 'keep-last', function(backup) {
+ return backup.backuptime;
+ });
+ me.pruneMark(backups, keepParams['keep-hourly'], 'keep-hourly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'H/d/m/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-daily'], 'keep-daily', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'd/m/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-weekly'], 'keep-weekly', function(backup) {
+ // ISO-8601 week and week-based year
+ return Ext.Date.format(backup.backuptime, 'W/o');
+ });
+ me.pruneMark(backups, keepParams['keep-monthly'], 'keep-monthly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'm/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-yearly'], 'keep-yearly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'Y');
+ });
+
+ backups.forEach(function(backup) {
+ backup.mark = backup.mark || 'remove';
+ });
+ },
+ },
+
+ keepItems: [
+ {
+ xtype: 'numberfield',
+ name: 'keep-last',
+ allowBlank: true,
+ fieldLabel: 'keep-last',
+ minValue: 0,
+ value: 5,
+ fieldStyle: 'background-color: ' + COLORS['keep-last'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-last'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-hourly',
+ allowBlank: true,
+ fieldLabel: 'keep-hourly',
+ minValue: 0,
+ value: 4,
+ fieldStyle: 'background-color: ' + COLORS['keep-hourly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-hourly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-daily',
+ allowBlank: true,
+ fieldLabel: 'keep-daily',
+ minValue: 0,
+ value: 3,
+ fieldStyle: 'background-color: ' + COLORS['keep-daily'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-daily'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-weekly',
+ allowBlank: true,
+ fieldLabel: 'keep-weekly',
+ minValue: 0,
+ value: 2,
+ fieldStyle: 'background-color: ' + COLORS['keep-weekly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-weekly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-monthly',
+ allowBlank: true,
+ fieldLabel: 'keep-monthly',
+ minValue: 0,
+ value: 1,
+ fieldStyle: 'background-color: ' + COLORS['keep-monthly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-monthly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-yearly',
+ allowBlank: true,
+ fieldLabel: 'keep-yearly',
+ minValue: 0,
+ value: 0,
+ fieldStyle: 'background-color: ' + COLORS['keep-yearly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-yearly'] + ';',
+ }
+ ],
+
+ scheduleItems: [
+ {
+ xtype: 'prunesimulatorDayOfWeekSelector',
+ name: 'schedule-weekdays',
+ fieldLabel: 'Day of week',
+ value: ['mon','tue','wed','thu','fri','sat','sun'],
+ allowBlank: false,
+ multiSelect: true,
+ },
+ {
+ xtype: 'prunesimulatorCalendarEvent',
+ name: 'schedule-time',
+ allowBlank: false,
+ value: '0/6:00',
+ fieldLabel: 'Backup schedule',
+ },
+/* TODO add button and only update schedule when clicked
+ {
+ xtype: 'button',
+ name: 'schedule-button',
+ text: 'Update Schedule',
+ },
+ */
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ me.pruneStore = Ext.create('Ext.data.Store', {
+ model: 'pbs-prune-list',
+ sorters: { property: 'backuptime', direction: 'DESC' },
+ });
+
+ me.column2 = [
+ {
+ xtype: 'prunesimulatorWeekTable',
+ store: me.pruneStore,
+ },
+ ];
+
+ me.items = [
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.keepItems,
+ },
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.scheduleItems,
+ },
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.column2,
+ colspan: 2,
+ },
+ ];
+
+ me.callParent();
+ }
+ });
+
+ let simulator = Ext.create('PBS.PruneSimulatorPanel', {});
+
+ Ext.create('Ext.container.Viewport', {
+ layout: 'border',
+ renderTo: Ext.getBody(),
+ items: [
+ simulator,
+ ],
+ });
+});
+
--
2.20.1
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2020-09-17 12:09 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-09-17 12:09 [pbs-devel] [RFC proxmox-backup] create prune simulator Fabian Ebner
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal