]>
git.proxmox.com Git - proxmox-backup.git/blob - docs/prune-simulator/prune-simulator.js
505eb8527fbee97ce36ee2f0603e3d4dd1d34461
1 // avoid errors when running without development tools
2 if (!Ext
.isDefined(Ext
.global
.console
)) {
9 Ext
.onReady(function() {
10 const NOW
= new Date();
12 'keep-last': 'orange',
13 'keep-hourly': 'purple',
14 'keep-daily': 'yellow',
15 'keep-weekly': 'green',
16 'keep-monthly': 'blue',
22 'keep-hourly': 'white',
23 'keep-daily': 'black',
24 'keep-weekly': 'white',
25 'keep-monthly': 'white',
26 'keep-yearly': 'white',
30 Ext
.define('PBS.prunesimulator.Documentation', {
32 alias
: 'widget.prunesimulatorDocumentation',
34 html
: '<iframe style="width:100%;height:100%" src="./documentation.html"/>',
37 Ext
.define('PBS.prunesimulator.CalendarEvent', {
38 extend
: 'Ext.form.field.ComboBox',
39 alias
: 'widget.prunesimulatorCalendarEvent',
48 field
: ['value', 'text'],
50 { value
: '0/2:00', text
: "Every two hours" },
51 { value
: '0/6:00', text
: "Every six hours" },
52 { value
: '2,22:30', text
: "At 02:30 and 22:30" },
53 { value
: '08..17:00/30', text
: "From 08:00 to 17:30 every 30 minutes" },
54 { value
: 'HOUR:MINUTE', text
: "Custom schedule" },
59 '<ul class="x-list-plain"><tpl for=".">',
60 '<li role="option" class="x-boundlist-item">{text}</li>',
71 Ext
.define('PBS.prunesimulator.DayOfWeekSelector', {
72 extend
: 'Ext.form.field.ComboBox',
73 alias
: 'widget.prunesimulatorDayOfWeekSelector',
82 field
: ['value', 'text'],
84 { value
: 'mon', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[1]) },
85 { value
: 'tue', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[2]) },
86 { value
: 'wed', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[3]) },
87 { value
: 'thu', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[4]) },
88 { value
: 'fri', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[5]) },
89 { value
: 'sat', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[6]) },
90 { value
: 'sun', text
: Ext
.util
.Format
.htmlDecode(Ext
.Date
.dayNames
[0]) },
95 Ext
.define('pbs-prune-list', {
96 extend
: 'Ext.data.Model',
101 dateFormat
: 'timestamp',
114 Ext
.define('PBS.prunesimulator.PruneList', {
115 extend
: 'Ext.panel.Panel',
116 alias
: 'widget.prunesimulatorPruneList',
118 initComponent: function() {
122 throw "no store specified";
131 header
: 'Backup Time',
132 dataIndex
: 'backuptime',
133 renderer: function(value
, metaData
, record
) {
134 let text
= Ext
.Date
.format(value
, 'Y-m-d H:i:s');
135 if (record
.data
.mark
=== 'keep') {
137 let bgColor
= COLORS
[record
.data
.keepName
];
138 let textColor
= TEXT_COLORS
[record
.data
.keepName
];
139 return '<div style="background-color: ' + bgColor
+ '; ' +
140 'color: ' + textColor
+ ';">' + text
+ '</div>';
145 return '<div style="text-decoration: line-through;">' + text
+ '</div>';
152 header
: 'Keep (reason)',
154 renderer: function(value
, metaData
, record
) {
155 if (record
.data
.mark
=== 'keep') {
156 return 'keep (' + record
.data
.keepName
+ ')';
172 Ext
.define('PBS.prunesimulator.WeekTable', {
173 extend
: 'Ext.panel.Panel',
174 alias
: 'widget.prunesimulatorWeekTable',
178 let backups
= me
.store
.data
.items
;
180 let html
= '<table>';
182 let now
= new Date(NOW
.getTime());
183 let skip
= 7 - parseInt(Ext
.Date
.format(now
, 'N'), 10);
184 let tableStartDate
= Ext
.Date
.add(now
, Ext
.Date
.DAY
, skip
);
188 for (let i
= 0; bIndex
< backups
.length
; i
++) {
191 for (let j
= 0; j
< 7; j
++) {
192 html
+= '<td style="vertical-align: top;' +
194 'border: black 1px solid;' +
197 let date
= Ext
.Date
.subtract(tableStartDate
, Ext
.Date
.DAY
, j
+ 7 * i
);
198 let currentDay
= Ext
.Date
.format(date
, 'd/m/Y');
200 let isBackupOnDay = function(backup
, day
) {
201 return backup
&& Ext
.Date
.format(backup
.data
.backuptime
, 'd/m/Y') === day
;
204 let backup
= backups
[bIndex
];
206 html
+= '<table><tr><th style="border-bottom: black 1px solid;">' +
207 Ext
.Date
.format(date
, 'D, d M Y') + '</th>';
209 while (isBackupOnDay(backup
, currentDay
)) {
212 let text
= Ext
.Date
.format(backup
.data
.backuptime
, 'H:i');
213 if (backup
.data
.mark
=== 'remove') {
214 html
+= '<div style="text-decoration: line-through;">' + text
+ '</div>';
216 text
+= ' (' + backup
.data
.keepName
+ ')';
218 let bgColor
= COLORS
[backup
.data
.keepName
];
219 let textColor
= TEXT_COLORS
[backup
.data
.keepName
];
220 html
+= '<div style="background-color: ' + bgColor
+ '; ' +
221 'color: ' + textColor
+ ';">' + text
+ '</div>';
223 html
+= '<div>' + text
+ '</div>';
226 html
+= '</td></tr>';
227 backup
= backups
[++bIndex
];
240 initComponent: function() {
244 throw "no store specified";
247 let reload = function() {
251 me
.store
.on("datachanged", reload
);
259 Ext
.define('PBS.PruneSimulatorPanel', {
260 extend
: 'Ext.panel.Panel',
261 alias
: 'widget.prunesimulatorPanel',
265 calendarHidden: function(get) {
266 return !get('showCalendar.checked');
271 getValues: function() {
276 Ext
.Array
.each(me
.query('[isFormField]'), function(field
) {
277 let data
= field
.getSubmitData();
278 Ext
.Object
.each(data
, function(name
, val
) {
287 xclass
: 'Ext.app.ViewController',
289 init: function(view
) {
290 this.reloadFull(); // initial load
294 'field[fieldGroup=keep]': { change
: 'reloadPrune' },
297 reloadFull: function() {
299 let view
= me
.getView();
301 let params
= view
.getValues();
303 let [hourSpec
, minuteSpec
] = params
['schedule-time'].split(':');
305 if (!hourSpec
|| !minuteSpec
) {
306 Ext
.Msg
.alert('Error', 'Invalid schedule');
310 let matchTimeSpec = function(timeSpec
, rangeMin
, rangeMax
) {
311 let specValues
= timeSpec
.split(',');
314 let assertValid = function(value
) {
315 let num
= Number(value
);
317 throw value
+ " is not an integer";
318 } else if (value
< rangeMin
|| value
> rangeMax
) {
319 throw "number '" + value
+ "' is not in the range '" + rangeMin
+ ".." + rangeMax
+ "'";
324 specValues
.forEach(function(value
) {
325 if (value
.includes('..')) {
326 let [start
, end
] = value
.split('..');
327 start
= assertValid(start
);
328 end
= assertValid(end
);
330 throw "interval start is bigger then interval end '" + start
+ " > " + end
+ "'";
332 for (let i
= start
; i
<= end
; i
++) {
335 } else if (value
.includes('/')) {
336 let [start
, step
] = value
.split('/');
337 start
= assertValid(start
);
338 step
= assertValid(step
);
339 for (let i
= start
; i
<= rangeMax
; i
+= step
) {
342 } else if (value
=== '*') {
343 for (let i
= rangeMin
; i
<= rangeMax
; i
++) {
347 value
= assertValid(value
);
352 return Object
.keys(matches
);
358 hours
= matchTimeSpec(hourSpec
, 0, 23);
359 minutes
= matchTimeSpec(minuteSpec
, 0, 59);
361 Ext
.Msg
.alert('Error', err
);
364 let backups
= me
.populateFromSchedule(
365 params
['schedule-weekdays'],
368 params
.numberOfWeeks
,
371 me
.pruneSelect(backups
, params
);
373 view
.pruneStore
.setData(backups
);
376 reloadPrune: function() {
378 let view
= me
.getView();
380 let params
= view
.getValues();
383 view
.pruneStore
.getData().items
.forEach(function(item
) {
385 backuptime
: item
.data
.backuptime
,
389 me
.pruneSelect(backups
, params
);
391 view
.pruneStore
.setData(backups
);
394 // backups are sorted descending by date
395 populateFromSchedule: function(weekdays
, hours
, minutes
, weekCount
) {
397 weekdays
.includes('sun'),
398 weekdays
.includes('mon'),
399 weekdays
.includes('tue'),
400 weekdays
.includes('wed'),
401 weekdays
.includes('thu'),
402 weekdays
.includes('fri'),
403 weekdays
.includes('sat'),
406 let todaysDate
= new Date(NOW
.getTime());
408 let timesOnSingleDay
= [];
410 hours
.forEach(function(hour
) {
411 minutes
.forEach(function(minute
) {
412 todaysDate
.setHours(hour
);
413 todaysDate
.setMinutes(minute
);
414 timesOnSingleDay
.push(todaysDate
.getTime());
418 // ordering here and iterating backwards through days
419 // ensures that everything is ordered
420 timesOnSingleDay
.sort(function(a
, b
) {
426 for (let i
= 0; i
< 7 * weekCount
; i
++) {
427 let daysDate
= Ext
.Date
.subtract(todaysDate
, Ext
.Date
.DAY
, i
);
428 let weekday
= parseInt(Ext
.Date
.format(daysDate
, 'w'), 10);
429 if (weekdayFlags
[weekday
]) {
430 timesOnSingleDay
.forEach(function(time
) {
432 backuptime
: Ext
.Date
.subtract(new Date(time
), Ext
.Date
.DAY
, i
),
441 pruneMark: function(backups
, keepCount
, keepName
, idFunc
) {
446 let alreadyIncluded
= {};
447 let newlyIncluded
= {};
448 let newlyIncludedCount
= 0;
450 let finished
= false;
452 backups
.forEach(function(backup
) {
453 let mark
= backup
.mark
;
454 let id
= idFunc(backup
);
456 if (finished
|| alreadyIncluded
[id
]) {
461 if (mark
=== 'keep') {
462 alreadyIncluded
[id
] = true;
467 if (!newlyIncluded
[id
]) {
468 if (newlyIncludedCount
>= keepCount
) {
472 newlyIncluded
[id
] = true;
473 newlyIncludedCount
++;
474 backup
.mark
= 'keep';
475 backup
.keepName
= keepName
;
477 backup
.mark
= 'remove';
482 // backups need to be sorted descending by date
483 pruneSelect: function(backups
, keepParams
) {
486 if (Number(keepParams
['keep-last']) +
487 Number(keepParams
['keep-hourly']) +
488 Number(keepParams
['keep-daily']) +
489 Number(keepParams
['keep-weekly']) +
490 Number(keepParams
['keep-monthly']) +
491 Number(keepParams
['keep-yearly']) === 0) {
492 backups
.forEach(function(backup
) {
493 backup
.mark
= 'keep';
494 backup
.keepName
= 'all zero';
500 me
.pruneMark(backups
, keepParams
['keep-last'], 'keep-last', function(backup
) {
501 return backup
.backuptime
;
503 me
.pruneMark(backups
, keepParams
['keep-hourly'], 'keep-hourly', function(backup
) {
504 return Ext
.Date
.format(backup
.backuptime
, 'H/d/m/Y');
506 me
.pruneMark(backups
, keepParams
['keep-daily'], 'keep-daily', function(backup
) {
507 return Ext
.Date
.format(backup
.backuptime
, 'd/m/Y');
509 me
.pruneMark(backups
, keepParams
['keep-weekly'], 'keep-weekly', function(backup
) {
510 // ISO-8601 week and week-based year
511 return Ext
.Date
.format(backup
.backuptime
, 'W/o');
513 me
.pruneMark(backups
, keepParams
['keep-monthly'], 'keep-monthly', function(backup
) {
514 return Ext
.Date
.format(backup
.backuptime
, 'm/Y');
516 me
.pruneMark(backups
, keepParams
['keep-yearly'], 'keep-yearly', function(backup
) {
517 return Ext
.Date
.format(backup
.backuptime
, 'Y');
520 backups
.forEach(function(backup
) {
521 backup
.mark
= backup
.mark
|| 'remove';
528 xtype
: 'numberfield',
531 fieldLabel
: 'keep-last',
538 xtype
: 'numberfield',
541 fieldLabel
: 'keep-hourly',
548 xtype
: 'numberfield',
551 fieldLabel
: 'keep-daily',
558 xtype
: 'numberfield',
561 fieldLabel
: 'keep-weekly',
568 xtype
: 'numberfield',
569 name
: 'keep-monthly',
571 fieldLabel
: 'keep-monthly',
578 xtype
: 'numberfield',
581 fieldLabel
: 'keep-yearly',
589 initComponent: function() {
592 me
.pruneStore
= Ext
.create('Ext.data.Store', {
593 model
: 'pbs-prune-list',
594 sorters
: { property
: 'backuptime', direction
: 'DESC' },
597 let scheduleItems
= [
599 xtype
: 'prunesimulatorDayOfWeekSelector',
600 name
: 'schedule-weekdays',
601 fieldLabel
: 'Day of week',
602 value
: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
608 xtype
: 'prunesimulatorCalendarEvent',
609 name
: 'schedule-time',
612 fieldLabel
: 'Backup schedule',
616 xtype
: 'numberfield',
617 name
: 'numberOfWeeks',
619 fieldLabel
: 'Number of weeks',
627 name
: 'schedule-button',
628 text
: 'Update Schedule',
629 handler: function() {
630 me
.controller
.reloadFull();
649 name
: 'showCalendar',
650 reference
: 'showCalendar',
651 fieldLabel
: 'Show Calendar:',
658 reference
: 'showColors',
659 fieldLabel
: 'Show Colors:',
661 handler: function(checkbox
, checked
) {
662 Ext
.Array
.each(me
.query('[isFormField]'), function(field
) {
663 if (field
.fieldGroup
!== 'keep') {
668 field
.setFieldStyle('background-color: ' + COLORS
[field
.name
] + '; ' +
669 'color: ' + TEXT_COLORS
[field
.name
] + ';');
671 field
.setFieldStyle('background-color: white; color: black;');
675 me
.lookupReference('weekTable').useColors
= checked
;
676 me
.lookupReference('pruneList').useColors
= checked
;
678 me
.controller
.reloadPrune();
686 title
: 'Backup Schedule',
687 items
: scheduleItems
,
698 title
: 'Prune Options',
705 xtype
: 'prunesimulatorPruneList',
706 store
: me
.pruneStore
,
707 reference
: 'pruneList',
718 xtype
: 'prunesimulatorWeekTable',
719 reference
: 'weekTable',
720 store
: me
.pruneStore
,
722 hidden
: '{calendarHidden}',
731 Ext
.create('Ext.container.Viewport', {
733 renderTo
: Ext
.getBody(),
736 xtype
: 'prunesimulatorPanel',
737 title
: 'PBS Prune Simulator',
747 xtype
: 'prunesimulatorDocumentation',