* [pbs-devel] [RFC v2 proxmox-backup] create prune simulator
@ 2020-10-28 13:12 Fabian Ebner
2020-10-29 16:11 ` Thomas Lamprecht
0 siblings, 1 reply; 2+ messages in thread
From: Fabian Ebner @ 2020-10-28 13:12 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>
---
Changes from v1:
* add list view of backups as we have in PBS
* make it possible to toggle the calendar and hide it by default
* make it possible to toggle the colors and turn them off by default
* fix backup sorting and uniqueness when there is more than one start time
* add a button for applying a new schedule instead of doing so continuosly
* make it more obvious that one can input a custom schedule
* error out on invalid schedule inputs
* make the number of weeks configurable
* show which rule keeps a specific backup
* keep all if all options are zero
* minor style improvements
docs/prune-simulator/extjs | 1 +
docs/prune-simulator/index.html | 13 +
docs/prune-simulator/prune-simulator.js | 699 ++++++++++++++++++++++++
3 files changed, 713 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..2ba89e74
--- /dev/null
+++ b/docs/prune-simulator/prune-simulator.js
@@ -0,0 +1,699 @@
+// 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.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: 'hour[:minute][/interval]', 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',
+
+ 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',
+ height: 200,
+ store: me.store,
+ width: 400,
+ 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,
+ },
+ {
+ 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,
+ },
+ ],
+ },
+ ];
+
+ me.callParent();
+ },
+ });
+
+ 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'), 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',
+
+ 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;
+ },
+
+ layout: {
+ type: 'table',
+ columns: 3,
+ },
+
+ 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 [startTimesStr, interval] = params['schedule-time'].split('/');
+
+ if (interval) {
+ let [intervalBig, intervalSmall] = interval.split(':');
+ if (intervalSmall) {
+ interval = Number(intervalBig) * 60 + Number(intervalSmall);
+ } else {
+ interval = Number(intervalBig); // no ':' means only minutes
+ }
+
+ if (isNaN(interval) || interval < 0 || interval >= 60 * 24) {
+ Ext.Msg.alert('Error', 'Invalid interval');
+ return;
+ }
+ }
+
+ let [startTimesHours, startTimesMinute] = startTimesStr.split(':');
+ let startTimesHoursArray = startTimesHours.split(',');
+
+ startTimesMinute = startTimesMinute ? Number(startTimesMinute) : 0;
+ if (isNaN(startTimesMinute) || startTimesMinute < 0 || startTimesMinute >= 60) {
+ Ext.Msg.alert('Error', 'Invalid start time');
+ return;
+ }
+
+ let startTimes = [];
+ startTimesHoursArray.forEach(function(hour) {
+ hour = Number(hour);
+ if (isNaN(hour) || hour < 0 || hour >= 24) {
+ Ext.Msg.alert('Error', 'Invalid start time');
+ return;
+ }
+ startTimes.push([hour, startTimesMinute]);
+ });
+
+ let backups = me.populateFromSchedule(
+ params['schedule-weekdays'],
+ startTimes,
+ interval,
+ 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, startTimes, interval, weekCount) {
+ interval = interval || 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 todaysDate = new Date(NOW.getTime());
+
+ let timesOnSingleDaySet = new Set();
+
+ startTimes.forEach(function(startTime) {
+ todaysDate.setHours(startTime[0]);
+ todaysDate.setMinutes(startTime[1]);
+ let date = new Date(todaysDate);
+ let weekday = parseInt(Ext.Date.format(date, 'w'), 10);
+ while (parseInt(Ext.Date.format(date, 'w'), 10) === weekday) {
+ timesOnSingleDaySet.add(date.getTime());
+ date = Ext.Date.add(date, Ext.Date.MINUTE, interval);
+ }
+ });
+
+ let timesOnSingleDayArray = [];
+
+ for (let time of timesOnSingleDaySet.keys()) {
+ timesOnSingleDayArray.push(time);
+ }
+ // ordering here and iterating backwards through days
+ // ensures that everything is ordered
+ timesOnSingleDayArray.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]) {
+ timesOnSingleDayArray.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: 5,
+ fieldGroup: 'keep',
+ padding: '0 0 0 10',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-hourly',
+ allowBlank: true,
+ fieldLabel: 'keep-hourly',
+ minValue: 0,
+ value: 4,
+ fieldGroup: 'keep',
+ padding: '0 0 0 10',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-daily',
+ allowBlank: true,
+ fieldLabel: 'keep-daily',
+ minValue: 0,
+ value: 3,
+ 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: 1,
+ 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 = [
+ {
+ padding: '10 5 0 10',
+ layout: 'anchor',
+ items: scheduleItems,
+ },
+ {
+ padding: '10 5 0 5',
+ layout: 'anchor',
+ items: me.keepItems,
+ },
+ {
+ padding: '10 10 0 5',
+ layout: 'anchor',
+ xtype: 'prunesimulatorPruneList',
+ store: me.pruneStore,
+ reference: 'pruneList',
+ },
+ {
+ padding: '0 0 0 10',
+ xtype: 'checkbox',
+ name: 'showCalendar',
+ reference: 'showCalendar',
+ fieldLabel: 'Show Calendar:',
+ checked: false,
+ colspan: 3,
+ },
+ {
+ 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();
+ },
+ colspan: 3,
+ },
+ {
+ padding: '10 10 10 10',
+ layout: 'anchor',
+ xtype: 'prunesimulatorWeekTable',
+ reference: 'weekTable',
+ store: me.pruneStore,
+ bind: {
+ hidden: '{calendarHidden}',
+ },
+ colspan: 3,
+ },
+ ];
+
+ 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] 2+ messages in thread
* Re: [pbs-devel] [RFC v2 proxmox-backup] create prune simulator
2020-10-28 13:12 [pbs-devel] [RFC v2 proxmox-backup] create prune simulator Fabian Ebner
@ 2020-10-29 16:11 ` Thomas Lamprecht
0 siblings, 0 replies; 2+ messages in thread
From: Thomas Lamprecht @ 2020-10-29 16:11 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Fabian Ebner
On 28.10.20 14:12, Fabian Ebner wrote:
> 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>
> ---
>
> Changes from v1:
> * add list view of backups as we have in PBS
> * make it possible to toggle the calendar and hide it by default
> * make it possible to toggle the colors and turn them off by default
> * fix backup sorting and uniqueness when there is more than one start time
> * add a button for applying a new schedule instead of doing so continuosly
> * make it more obvious that one can input a custom schedule
> * error out on invalid schedule inputs
> * make the number of weeks configurable
> * show which rule keeps a specific backup
> * keep all if all options are zero
> * minor style improvements
>
I find it pretty nice now, great job!
I'd only reorder it a bit, make it a full-page application and add some prune/schedule
documentation - we could maybe just pull that 1:1 from the docs html output or
something, it hasn't to be extra long just a short widget explanation and some other
info which would assist users to understand the concepts and plan their schedule
interactively.
I.e., something like the following layout:
+--------------------------------------------------+--------------------------+
| | |
| Simulated | |
| Backup Schedule | Documentation |
| | |
+-------------------------+------------------------+ * What this widget is |
| | | |
| Prune keep settings | Prune "keep" view | * prune params explained|
| | | |
| | | * ... |
| | | |
| | | |
+-------------------------+------------------------+ |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| Calendar | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
+--------------------------------------------------+--------------------------+
But in any way, I like it a lot more than v1, and that wasn't bad either :)
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2020-10-29 16:11 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-10-28 13:12 [pbs-devel] [RFC v2 proxmox-backup] create prune simulator Fabian Ebner
2020-10-29 16:11 ` Thomas Lamprecht
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