[pbs-devel] [PATCH v3 proxmox-backup 1/2] create prune simulator

Fabian Ebner f.ebner at proxmox.com
Tue Nov 3 15:28:41 CET 2020


A stand-alone ExtJS app that allows experimenting with different backup
schedules and prune parameters.

The HTML for the documentation was taken from the PBS docs and adapted to the
context of the simulator.

For performance reasons, the week table does not use
subcomponents, but raw HTML.

Signed-off-by: Fabian Ebner <f.ebner at proxmox.com>
---

Changes from v2:
    * add explanation/documentation for usage
    * add titles to components
    * improve layout
    * fix and extend schedule parsing: now '..' is supported and '/'
      behaves as it should
    * don't allow custom input in the weekdays combobox
    * add schedule example with range
    * make initial prune options more realistic
    * disable the sorting menu on the grid: trying to sort would cause
      everything to freeze when there were more than a few backups and
      backups are already sorted anyways
    * don't track link to extjs with git; add it with dh_link instead, which
      is how it's done for the PVE api-viewer as well (part of the next patch)

 docs/prune-simulator/documentation.html |  73 +++
 docs/prune-simulator/index.html         |  13 +
 docs/prune-simulator/prune-simulator.js | 755 ++++++++++++++++++++++++
 3 files changed, 841 insertions(+)
 create mode 100644 docs/prune-simulator/documentation.html
 create mode 100644 docs/prune-simulator/index.html
 create mode 100644 docs/prune-simulator/prune-simulator.js

diff --git a/docs/prune-simulator/documentation.html b/docs/prune-simulator/documentation.html
new file mode 100644
index 00000000..2bd90b94
--- /dev/null
+++ b/docs/prune-simulator/documentation.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+    tt, code {
+	background-color: #ecf0f3;
+	color: #222;
+    }
+    pre, tt, code {
+	font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+	font-size: 0.9em;
+    }
+    div.note {
+	background-color: #EEE;
+	border: 1px solid #CCC;
+    }
+</style>
+</head>
+<body>
+<p>A simulator to experiment with different backup schedules and prune
+options.</p>
+
+<h3>Schedule</h3>
+<p>Select weekdays with the combobox and input hour and minute
+specification separated by a colon, i.e. <code>HOUR:MINUTE</code>. Each of
+<code>HOUR</code> and <code>MINUTE</code> can be either a single value or
+one of the following:</p>
+<ul class="simple">
+<li>a comma-separated list: e.g., <code>01,02,03</code></li>
+<li>a range: e.g., <code>01..10</code></li>
+<li>a repetition: e.g, <code>05/10</code> (means starting at <code>5</code> every <code>10</code>)</li>
+<li>a combination of the above: e.g., <code>01,05..10,12/02</code></li>
+<li>a <code>*</code> for every possible value</li>
+</ul>
+
+<h3>Pruning</h3>
+<p>Prune lets you systematically delete older backups, retaining backups for
+the last given number of time intervals. The following retention options are
+available:</p>
+<dl class="docutils">
+<dt><code class="docutils literal notranslate"><span class="pre">keep-last</span> <span class="pre"><N></span></code></dt>
+<dd>Keep the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> backup snapshots.</dd>
+<dt><code class="docutils literal notranslate"><span class="pre">keep-hourly</span> <span class="pre"><N></span></code></dt>
+<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> hours. If there is more than one
+backup for a single hour, only the latest is kept.</dd>
+<dt><code class="docutils literal notranslate"><span class="pre">keep-daily</span> <span class="pre"><N></span></code></dt>
+<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> days. If there is more than one
+backup for a single day, only the latest is kept.</dd>
+<dt><code class="docutils literal notranslate"><span class="pre">keep-weekly</span> <span class="pre"><N></span></code></dt>
+<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> weeks. If there is more than one
+backup for a single week, only the latest is kept.
+<div class="last admonition note">
+<p class="last"><b>Note:</b> Weeks start on Monday and end on Sunday. The software
+uses the <a class="reference external" href="https://en.wikipedia.org/wiki/ISO_week_date">ISO week date</a> system and handles weeks at
+the end of the year correctly.</p>
+</div>
+</dd>
+<dt><code class="docutils literal notranslate"><span class="pre">keep-monthly</span> <span class="pre"><N></span></code></dt>
+<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> months. If there is more than one
+backup for a single month, only the latest is kept.</dd>
+<dt><code class="docutils literal notranslate"><span class="pre">keep-yearly</span> <span class="pre"><N></span></code></dt>
+<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre"><N></span></code> years. If there is more than one
+backup for a single year, only the latest is kept.</dd>
+</dl>
+<p>The retention options are processed in the order given above. Each option
+only covers backups within its time period. The next option does not take care
+of already covered backups. It will only consider older backups.</p>
+<p>For example, in a week covered by <code>keep-weekly</code>, one backup is
+kept while all others are removed; <code>keep-monthly</code> then does not
+consider backups from that week anymore, even if part of the week is part of
+an earlier month.</p>
+</body>
+</html>
diff --git a/docs/prune-simulator/index.html b/docs/prune-simulator/index.html
new file mode 100644
index 00000000..22fa66f6
--- /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>PBS Prune 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..505eb852
--- /dev/null
+++ b/docs/prune-simulator/prune-simulator.js
@@ -0,0 +1,755 @@
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+    var console = {
+        dir: function() {},
+        log: function() {},
+    };
+}
+
+Ext.onReady(function() {
+    const NOW = new Date();
+    const COLORS = {
+	'keep-last': 'orange',
+	'keep-hourly': 'purple',
+	'keep-daily': 'yellow',
+	'keep-weekly': 'green',
+	'keep-monthly': 'blue',
+	'keep-yearly': 'red',
+	'all zero': 'white',
+    };
+    const TEXT_COLORS = {
+	'keep-last': 'black',
+	'keep-hourly': 'white',
+	'keep-daily': 'black',
+	'keep-weekly': 'white',
+	'keep-monthly': 'white',
+	'keep-yearly': 'white',
+	'all zero': 'black',
+    };
+
+    Ext.define('PBS.prunesimulator.Documentation', {
+	extend: 'Ext.Panel',
+	alias: 'widget.prunesimulatorDocumentation',
+
+	html: '<iframe style="width:100%;height:100%" src="./documentation.html"/>',
+    });
+
+    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" },
+		{ value: '08..17:00/30', text: "From 08:00 to 17:30 every 30 minutes" },
+		{ value: 'HOUR:MINUTE', text: "Custom schedule" },
+	    ],
+	},
+
+	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',
+
+	editable: false,
+
+	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',
+	    },
+	    {
+		name: 'mark',
+		type: 'string',
+	    },
+	    {
+		name: 'keepName',
+		type: 'string',
+	    },
+	],
+    });
+
+    Ext.define('PBS.prunesimulator.PruneList', {
+	extend: 'Ext.panel.Panel',
+	alias: 'widget.prunesimulatorPruneList',
+
+	initComponent: function() {
+	    var me = this;
+
+	    if (!me.store) {
+		throw "no store specified";
+	    }
+
+	    me.items = [
+		{
+		    xtype: 'grid',
+		    store: me.store,
+		    columns: [
+			{
+			    header: 'Backup Time',
+			    dataIndex: 'backuptime',
+			    renderer: function(value, metaData, record) {
+				let text = Ext.Date.format(value, 'Y-m-d H:i:s');
+				if (record.data.mark === 'keep') {
+				    if (me.useColors) {
+					let bgColor = COLORS[record.data.keepName];
+					let textColor = TEXT_COLORS[record.data.keepName];
+					return '<div style="background-color: ' + bgColor + '; ' +
+							    'color: ' + textColor + ';">' + text + '</div>';
+				    } else {
+					return text;
+				    }
+				} else {
+				    return '<div style="text-decoration: line-through;">' + text + '</div>';
+				}
+			    },
+			    flex: 1,
+			    sortable: false,
+			},
+			{
+			    header: 'Keep (reason)',
+			    dataIndex: 'mark',
+			    renderer: function(value, metaData, record) {
+				if (record.data.mark === 'keep') {
+				    return 'keep (' + record.data.keepName + ')';
+				} else {
+				    return value;
+				}
+			    },
+			    width: 200,
+			    sortable: false,
+			},
+		    ],
+		},
+	    ];
+
+	    me.callParent();
+	},
+    });
+
+    Ext.define('PBS.prunesimulator.WeekTable', {
+	extend: 'Ext.panel.Panel',
+	alias: 'widget.prunesimulatorWeekTable',
+
+	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'), 10);
+	    let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);
+
+	    let bIndex = 0;
+
+	    for (let i = 0; bIndex < backups.length; i++) {
+		html += '<tr>';
+
+		for (let j = 0; j < 7; j++) {
+		    html += '<td style="vertical-align: top;' +
+				       'width: 150px;' +
+				       'border: black 1px solid;' +
+				       '">';
+
+		    let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
+		    let currentDay = Ext.Date.format(date, 'd/m/Y');
+
+		    let isBackupOnDay = function(backup, day) {
+			return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
+		    };
+
+		    let backup = backups[bIndex];
+
+		    html += '<table><tr><th style="border-bottom: black 1px solid;">' +
+			    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;">' + text + '</div>';
+			} else {
+			    text += ' (' + backup.data.keepName + ')';
+			    if (me.useColors) {
+				let bgColor = COLORS[backup.data.keepName];
+				let textColor = TEXT_COLORS[backup.data.keepName];
+				html += '<div style="background-color: ' + bgColor + '; ' +
+						    'color: ' + textColor + ';">' + text + '</div>';
+			    } else {
+				html += '<div>' + 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',
+	alias: 'widget.prunesimulatorPanel',
+
+	viewModel: {
+	    formulas: {
+		calendarHidden: function(get) {
+		    return !get('showCalendar.checked');
+		},
+	    },
+	},
+
+	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;
+	},
+
+	controller: {
+	    xclass: 'Ext.app.ViewController',
+
+	    init: function(view) {
+		this.reloadFull(); // initial load
+	    },
+
+	    control: {
+		'field[fieldGroup=keep]': { change: 'reloadPrune' },
+	    },
+
+	    reloadFull: function() {
+		let me = this;
+		let view = me.getView();
+
+		let params = view.getValues();
+
+		let [hourSpec, minuteSpec] = params['schedule-time'].split(':');
+
+		if (!hourSpec || !minuteSpec) {
+		    Ext.Msg.alert('Error', 'Invalid schedule');
+		    return;
+		}
+
+		let matchTimeSpec = function(timeSpec, rangeMin, rangeMax) {
+		    let specValues = timeSpec.split(',');
+		    let matches = {};
+
+		    let assertValid = function(value) {
+			let num = Number(value);
+			if (isNaN(num)) {
+			    throw value + " is not an integer";
+			} else if (value < rangeMin || value > rangeMax) {
+			    throw "number '" + value + "' is not in the range '" + rangeMin + ".." + rangeMax + "'";
+			}
+			return num;
+		    };
+
+		    specValues.forEach(function(value) {
+			if (value.includes('..')) {
+			    let [start, end] = value.split('..');
+			    start = assertValid(start);
+			    end = assertValid(end);
+			    if (start > end) {
+				throw "interval start is bigger then interval end '" + start + " > " + end + "'";
+			    }
+			    for (let i = start; i <= end; i++) {
+				matches[i] = 1;
+			    }
+			} else if (value.includes('/')) {
+			    let [start, step] = value.split('/');
+			    start = assertValid(start);
+			    step = assertValid(step);
+			    for (let i = start; i <= rangeMax; i += step) {
+				matches[i] = 1;
+			    }
+			} else if (value === '*') {
+			    for (let i = rangeMin; i <= rangeMax; i++) {
+				matches[i] = 1;
+			    }
+			} else {
+			    value = assertValid(value);
+			    matches[value] = 1;
+			}
+		    });
+
+		    return Object.keys(matches);
+		};
+
+		let hours, minutes;
+
+		try {
+		    hours = matchTimeSpec(hourSpec, 0, 23);
+		    minutes = matchTimeSpec(minuteSpec, 0, 59);
+		} catch (err) {
+		    Ext.Msg.alert('Error', err);
+		}
+
+		let backups = me.populateFromSchedule(
+		    params['schedule-weekdays'],
+		    hours,
+		    minutes,
+		    params.numberOfWeeks,
+		);
+
+		me.pruneSelect(backups, params);
+
+		view.pruneStore.setData(backups);
+	    },
+
+	    reloadPrune: function() {
+		let me = this;
+		let view = me.getView();
+
+		let params = view.getValues();
+
+		let backups = [];
+		view.pruneStore.getData().items.forEach(function(item) {
+		    backups.push({
+			backuptime: item.data.backuptime,
+		    });
+		});
+
+		me.pruneSelect(backups, params);
+
+		view.pruneStore.setData(backups);
+	    },
+
+	    // backups are sorted descending by date
+	    populateFromSchedule: function(weekdays, hours, minutes, weekCount) {
+		let weekdayFlags = [
+		    weekdays.includes('sun'),
+		    weekdays.includes('mon'),
+		    weekdays.includes('tue'),
+		    weekdays.includes('wed'),
+		    weekdays.includes('thu'),
+		    weekdays.includes('fri'),
+		    weekdays.includes('sat'),
+		];
+
+		let todaysDate = new Date(NOW.getTime());
+
+		let timesOnSingleDay = [];
+
+		hours.forEach(function(hour) {
+		    minutes.forEach(function(minute) {
+			todaysDate.setHours(hour);
+			todaysDate.setMinutes(minute);
+			timesOnSingleDay.push(todaysDate.getTime());
+		    });
+		});
+
+		// ordering here and iterating backwards through days
+		// ensures that everything is ordered
+		timesOnSingleDay.sort(function(a, b) {
+		    return a < b;
+		});
+
+		let backups = [];
+
+		for (let i = 0; i < 7 * weekCount; i++) {
+		    let daysDate = Ext.Date.subtract(todaysDate, Ext.Date.DAY, i);
+		    let weekday = parseInt(Ext.Date.format(daysDate, 'w'), 10);
+		    if (weekdayFlags[weekday]) {
+			timesOnSingleDay.forEach(function(time) {
+			    backups.push({
+				backuptime: Ext.Date.subtract(new Date(time), Ext.Date.DAY, i),
+			    });
+			});
+		    }
+		}
+
+		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;
+
+		if (Number(keepParams['keep-last']) +
+		    Number(keepParams['keep-hourly']) +
+		    Number(keepParams['keep-daily']) +
+		    Number(keepParams['keep-weekly']) +
+		    Number(keepParams['keep-monthly']) +
+		    Number(keepParams['keep-yearly']) === 0) {
+		    backups.forEach(function(backup) {
+			backup.mark = 'keep';
+			backup.keepName = 'all zero';
+		    });
+
+		    return;
+		}
+
+		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: 4,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'keep-hourly',
+		allowBlank: true,
+		fieldLabel: 'keep-hourly',
+		minValue: 0,
+		value: 0,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'keep-daily',
+		allowBlank: true,
+		fieldLabel: 'keep-daily',
+		minValue: 0,
+		value: 5,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'keep-weekly',
+		allowBlank: true,
+		fieldLabel: 'keep-weekly',
+		minValue: 0,
+		value: 2,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'keep-monthly',
+		allowBlank: true,
+		fieldLabel: 'keep-monthly',
+		minValue: 0,
+		value: 0,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'keep-yearly',
+		allowBlank: true,
+		fieldLabel: 'keep-yearly',
+		minValue: 0,
+		value: 0,
+		fieldGroup: 'keep',
+		padding: '0 0 0 10',
+	    },
+	],
+
+	initComponent: function() {
+	    var me = this;
+
+	    me.pruneStore = Ext.create('Ext.data.Store', {
+		model: 'pbs-prune-list',
+		sorters: { property: 'backuptime', direction: 'DESC' },
+	    });
+
+	    let scheduleItems = [
+		{
+		    xtype: 'prunesimulatorDayOfWeekSelector',
+		    name: 'schedule-weekdays',
+		    fieldLabel: 'Day of week',
+		    value: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
+		    allowBlank: false,
+		    multiSelect: true,
+		    padding: '0 0 0 10',
+		},
+		{
+		    xtype: 'prunesimulatorCalendarEvent',
+		    name: 'schedule-time',
+		    allowBlank: false,
+		    value: '0/6:00',
+		    fieldLabel: 'Backup schedule',
+		    padding: '0 0 0 10',
+		},
+		{
+		    xtype: 'numberfield',
+		    name: 'numberOfWeeks',
+		    allowBlank: false,
+		    fieldLabel: 'Number of weeks',
+		    minValue: 1,
+		    value: 15,
+		    maxValue: 200,
+		    padding: '0 0 0 10',
+		},
+		{
+		    xtype: 'button',
+		    name: 'schedule-button',
+		    text: 'Update Schedule',
+		    handler: function() {
+			me.controller.reloadFull();
+		    },
+		},
+	    ];
+
+	    me.items = [
+		{
+		    xtype: 'panel',
+		    layout: 'hbox',
+		    height: 180,
+		    items: [
+			{
+			    title: 'View',
+			    layout: 'anchor',
+			    flex: 1,
+			    items: [
+				{
+				    padding: '0 0 0 10',
+				    xtype: 'checkbox',
+				    name: 'showCalendar',
+				    reference: 'showCalendar',
+				    fieldLabel: 'Show Calendar:',
+				    checked: false,
+				},
+				{
+				    padding: '0 0 0 10',
+				    xtype: 'checkbox',
+				    name: 'showColors',
+				    reference: 'showColors',
+				    fieldLabel: 'Show Colors:',
+				    checked: false,
+				    handler: function(checkbox, checked) {
+					Ext.Array.each(me.query('[isFormField]'), function(field) {
+					    if (field.fieldGroup !== 'keep') {
+						return;
+					    }
+
+					    if (checked) {
+						field.setFieldStyle('background-color: ' + COLORS[field.name] + '; ' +
+						    'color: ' + TEXT_COLORS[field.name] + ';');
+					    } else {
+						field.setFieldStyle('background-color: white; color: black;');
+					    }
+					});
+
+					me.lookupReference('weekTable').useColors = checked;
+					me.lookupReference('pruneList').useColors = checked;
+
+					me.controller.reloadPrune();
+				    },
+				},
+			    ],
+			},
+			{
+			    layout: 'anchor',
+			    flex: 1,
+			    title: 'Backup Schedule',
+			    items: scheduleItems,
+			},
+		    ],
+		},
+		{
+		    xtype: 'panel',
+		    layout: 'hbox',
+		    flex: 1,
+		    items: [
+			{
+			    layout: 'anchor',
+			    title: 'Prune Options',
+			    items: me.keepItems,
+			    flex: 1,
+			},
+			{
+			    layout: 'fit',
+			    title: 'Backups',
+			    xtype: 'prunesimulatorPruneList',
+			    store: me.pruneStore,
+			    reference: 'pruneList',
+			    height: '100%',
+			    flex: 1,
+			},
+		    ],
+		},
+		{
+		    layout: 'anchor',
+		    title: 'Calendar',
+		    autoScroll: true,
+		    flex: 2,
+		    xtype: 'prunesimulatorWeekTable',
+		    reference: 'weekTable',
+		    store: me.pruneStore,
+		    bind: {
+			hidden: '{calendarHidden}',
+		    },
+		},
+	    ];
+
+	    me.callParent();
+	},
+    });
+
+    Ext.create('Ext.container.Viewport', {
+	layout: 'border',
+	renderTo: Ext.getBody(),
+	items: [
+	    {
+		xtype: 'prunesimulatorPanel',
+		title: 'PBS Prune Simulator',
+		region: 'west',
+		layout: {
+		    type: 'vbox',
+		    align: 'stretch',
+		    pack: 'start',
+		},
+		width: 1080,
+	    },
+	    {
+		xtype: 'prunesimulatorDocumentation',
+		title: 'Usage',
+		margins: '5 0 0 0',
+		region: 'center',
+	    },
+	],
+    });
+});
+
-- 
2.20.1






More information about the pbs-devel mailing list