]> git.proxmox.com Git - proxmox-backup.git/blame - docs/prune-simulator/prune-simulator_source.js
prune sim: rework simulation options layout
[proxmox-backup.git] / docs / prune-simulator / prune-simulator_source.js
CommitLineData
442d6da8 1// for Toolkit.js
d7386690 2function gettext(val) { return val; };
67fd0979
FE
3
4Ext.onReady(function() {
67fd0979
FE
5 const COLORS = {
6 'keep-last': 'orange',
7 'keep-hourly': 'purple',
8 'keep-daily': 'yellow',
9 'keep-weekly': 'green',
10 'keep-monthly': 'blue',
11 'keep-yearly': 'red',
12 'all zero': 'white',
13 };
14 const TEXT_COLORS = {
15 'keep-last': 'black',
16 'keep-hourly': 'white',
17 'keep-daily': 'black',
18 'keep-weekly': 'white',
19 'keep-monthly': 'white',
20 'keep-yearly': 'white',
21 'all zero': 'black',
22 };
23
24 Ext.define('PBS.prunesimulator.Documentation', {
25 extend: 'Ext.Panel',
26 alias: 'widget.prunesimulatorDocumentation',
27
6823fdc7 28 html: '<iframe style="width:100%;height:100%;border:0px;" src="./documentation.html"/>',
67fd0979
FE
29 });
30
31 Ext.define('PBS.prunesimulator.CalendarEvent', {
32 extend: 'Ext.form.field.ComboBox',
33 alias: 'widget.prunesimulatorCalendarEvent',
34
35 editable: true,
36
67fd0979
FE
37 valueField: 'value',
38 queryMode: 'local',
39
40 store: {
41 field: ['value', 'text'],
42 data: [
43 { value: '0/2:00', text: "Every two hours" },
44 { value: '0/6:00', text: "Every six hours" },
45 { value: '2,22:30', text: "At 02:30 and 22:30" },
b83b12cf 46 { value: '00:00', text: "At 00:00" },
67fd0979
FE
47 { value: '08..17:00/30', text: "From 08:00 to 17:30 every 30 minutes" },
48 { value: 'HOUR:MINUTE', text: "Custom schedule" },
49 ],
50 },
51
52 tpl: [
53 '<ul class="x-list-plain"><tpl for=".">',
54 '<li role="option" class="x-boundlist-item">{text}</li>',
55 '</tpl></ul>',
56 ],
57
58 displayTpl: [
59 '<tpl for=".">',
60 '{value}',
61 '</tpl>',
62 ],
63 });
64
65 Ext.define('PBS.prunesimulator.DayOfWeekSelector', {
66 extend: 'Ext.form.field.ComboBox',
67 alias: 'widget.prunesimulatorDayOfWeekSelector',
68
69 editable: false,
70
71 displayField: 'text',
72 valueField: 'value',
73 queryMode: 'local',
74
75 store: {
76 field: ['value', 'text'],
77 data: [
78 { value: 'mon', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[1]) },
79 { value: 'tue', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[2]) },
80 { value: 'wed', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[3]) },
81 { value: 'thu', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[4]) },
82 { value: 'fri', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[5]) },
83 { value: 'sat', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[6]) },
84 { value: 'sun', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[0]) },
85 ],
86 },
87 });
88
89 Ext.define('pbs-prune-list', {
90 extend: 'Ext.data.Model',
91 fields: [
92 {
93 name: 'backuptime',
94 type: 'date',
95 dateFormat: 'timestamp',
96 },
97 {
98 name: 'mark',
99 type: 'string',
100 },
101 {
102 name: 'keepName',
103 type: 'string',
104 },
105 ],
106 });
107
108 Ext.define('PBS.prunesimulator.PruneList', {
109 extend: 'Ext.panel.Panel',
110 alias: 'widget.prunesimulatorPruneList',
111
bb044304
TL
112 viewModel: {},
113
114 items: [{
115 xtype: 'grid',
116 bind: {
117 store: '{store}',
118 },
119 border: false,
120 columns: [
121 {
122 header: 'Backup Time',
123 dataIndex: 'backuptime',
124 renderer: function(value, metaData, { data }) {
125 let text = Ext.Date.format(value, 'Y-m-d H:i:s');
126 if (data.mark !== 'keep') {
127 return `<div style="text-decoration: line-through;">${text}</div>`;
128 }
129 if (me.useColors) {
130 let bgColor = COLORS[data.keepName];
131 let textColor = TEXT_COLORS[data.keepName];
132 return `<div style="background-color: ${bgColor};color: ${textColor};">${text}</div>`;
133 } else {
134 return text;
135 }
136 },
137 flex: 1,
138 sortable: false,
139 },
140 {
141 header: 'Keep (reason)',
142 dataIndex: 'mark',
143 renderer: function(value, metaData, { data }) {
144 if (data.mark !== 'keep') {
145 return value;
146 }
147 if (data.keepCount) {
148 return `keep (${data.keepName}: ${data.keepCount})`;
149 } else {
150 return `keep (${data.keepName})`;
151 }
152 },
153 width: 200,
154 sortable: false,
155 },
156 ],
157 }],
158
67fd0979 159 initComponent: function() {
60d2a615 160 let me = this;
67fd0979
FE
161
162 if (!me.store) {
163 throw "no store specified";
164 }
67fd0979 165 me.callParent();
bb044304 166 me.getViewModel().set('store', me.store);
67fd0979
FE
167 },
168 });
169
170 Ext.define('PBS.prunesimulator.WeekTable', {
171 extend: 'Ext.panel.Panel',
172 alias: 'widget.prunesimulatorWeekTable',
173
174 reload: function() {
175 let me = this;
176 let backups = me.store.data.items;
177
435a6c5e 178 let html = '<table class="cal">';
67fd0979 179
f44e2386 180 let now = new Date(me.up().getViewModel().get('now'));
67fd0979
FE
181 let skip = 7 - parseInt(Ext.Date.format(now, 'N'), 10);
182 let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);
183
184 let bIndex = 0;
185
186 for (let i = 0; bIndex < backups.length; i++) {
187 html += '<tr>';
188
189 for (let j = 0; j < 7; j++) {
67fd0979
FE
190 let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
191 let currentDay = Ext.Date.format(date, 'd/m/Y');
192
435a6c5e
TL
193 let dayOfWeekCls = Ext.Date.format(date, 'D').toLowerCase();
194 let firstOfMonthCls = Ext.Date.format(date, 'd') === '01'
195 ? 'first-of-month'
196 : '';
197 html += `<td class="cal-day ${dayOfWeekCls} ${firstOfMonthCls}">`;
198
199 const isBackupOnDay = function(backup, day) {
67fd0979
FE
200 return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
201 };
202
203 let backup = backups[bIndex];
204
435a6c5e
TL
205 html += '<table><tr>';
206 html += `<th class="cal-day-date">${Ext.Date.format(date, 'D, d M Y')}</th>`;
67fd0979
FE
207
208 while (isBackupOnDay(backup, currentDay)) {
209 html += '<tr><td>';
210
211 let text = Ext.Date.format(backup.data.backuptime, 'H:i');
212 if (backup.data.mark === 'remove') {
435a6c5e 213 html += `<span class="strikethrough">${text}</span>`;
67fd0979 214 } else {
924d6d40 215 if (backup.data.keepCount) {
0a0ba078 216 text += ` (${backup.data.keepName} ${backup.data.keepCount})`;
924d6d40
FE
217 } else {
218 text += ` (${backup.data.keepName})`;
219 }
67fd0979
FE
220 if (me.useColors) {
221 let bgColor = COLORS[backup.data.keepName];
222 let textColor = TEXT_COLORS[backup.data.keepName];
bb044304 223 html += `<span style="background-color: ${bgColor}; color: ${textColor};">${text}</span>`;
67fd0979 224 } else {
435a6c5e 225 html += `<span class="black">${text}</span>`;
67fd0979
FE
226 }
227 }
228 html += '</td></tr>';
229 backup = backups[++bIndex];
230 }
231 html += '</table>';
232 html += '</div>';
233 html += '</td>';
234 }
235
236 html += '</tr>';
237 }
238
239 me.setHtml(html);
240 },
241
242 initComponent: function() {
243 let me = this;
244
245 if (!me.store) {
246 throw "no store specified";
247 }
248
249 let reload = function() {
250 me.reload();
251 };
252
253 me.store.on("datachanged", reload);
254
255 me.callParent();
256
257 me.reload();
258 },
259 });
260
21b55284
FE
261 Ext.define('PBS.PruneSimulatorKeepInput', {
262 extend: 'Ext.form.field.Number',
263 alias: 'widget.prunesimulatorKeepInput',
264
265 allowBlank: true,
266 fieldGroup: 'keep',
267 minValue: 1,
268
269 listeners: {
270 afterrender: function(field) {
271 this.triggers.clear.setVisible(field.value !== null);
272 },
273 change: function(field, newValue, oldValue) {
274 this.triggers.clear.setVisible(newValue !== null);
275 },
276 },
277 triggers: {
278 clear: {
279 cls: 'clear-trigger',
280 weight: -1,
281 handler: function() {
282 this.triggers.clear.setVisible(false);
283 this.setValue(null);
284 },
285 },
286 },
287 });
288
67fd0979
FE
289 Ext.define('PBS.PruneSimulatorPanel', {
290 extend: 'Ext.panel.Panel',
291 alias: 'widget.prunesimulatorPanel',
292
293 viewModel: {
f44e2386
MH
294 data: {
295 now: new Date(),
296 },
67fd0979
FE
297 },
298
299 getValues: function() {
300 let me = this;
301
302 let values = {};
303
304 Ext.Array.each(me.query('[isFormField]'), function(field) {
305 let data = field.getSubmitData();
306 Ext.Object.each(data, function(name, val) {
307 values[name] = val;
308 });
309 });
310
311 return values;
312 },
313
314 controller: {
315 xclass: 'Ext.app.ViewController',
316
317 init: function(view) {
318 this.reloadFull(); // initial load
bad26df1 319 this.switchColor(true);
67fd0979
FE
320 },
321
322 control: {
323 'field[fieldGroup=keep]': { change: 'reloadPrune' },
324 },
325
326 reloadFull: function() {
327 let me = this;
328 let view = me.getView();
329
330 let params = view.getValues();
331
332 let [hourSpec, minuteSpec] = params['schedule-time'].split(':');
333
334 if (!hourSpec || !minuteSpec) {
335 Ext.Msg.alert('Error', 'Invalid schedule');
336 return;
337 }
338
339 let matchTimeSpec = function(timeSpec, rangeMin, rangeMax) {
340 let specValues = timeSpec.split(',');
341 let matches = {};
342
343 let assertValid = function(value) {
344 let num = Number(value);
345 if (isNaN(num)) {
346 throw value + " is not an integer";
347 } else if (value < rangeMin || value > rangeMax) {
348 throw "number '" + value + "' is not in the range '" + rangeMin + ".." + rangeMax + "'";
349 }
350 return num;
351 };
352
353 specValues.forEach(function(value) {
354 if (value.includes('..')) {
355 let [start, end] = value.split('..');
356 start = assertValid(start);
357 end = assertValid(end);
358 if (start > end) {
359 throw "interval start is bigger then interval end '" + start + " > " + end + "'";
360 }
361 for (let i = start; i <= end; i++) {
362 matches[i] = 1;
363 }
364 } else if (value.includes('/')) {
365 let [start, step] = value.split('/');
366 start = assertValid(start);
367 step = assertValid(step);
368 for (let i = start; i <= rangeMax; i += step) {
369 matches[i] = 1;
370 }
371 } else if (value === '*') {
372 for (let i = rangeMin; i <= rangeMax; i++) {
373 matches[i] = 1;
374 }
375 } else {
376 value = assertValid(value);
377 matches[value] = 1;
378 }
379 });
380
381 return Object.keys(matches);
382 };
383
384 let hours, minutes;
385
386 try {
387 hours = matchTimeSpec(hourSpec, 0, 23);
388 minutes = matchTimeSpec(minuteSpec, 0, 59);
389 } catch (err) {
390 Ext.Msg.alert('Error', err);
7f0f3666 391 return;
67fd0979
FE
392 }
393
394 let backups = me.populateFromSchedule(
395 params['schedule-weekdays'],
396 hours,
397 minutes,
398 params.numberOfWeeks,
399 );
400
401 me.pruneSelect(backups, params);
402
403 view.pruneStore.setData(backups);
404 },
405
406 reloadPrune: function() {
407 let me = this;
408 let view = me.getView();
409
410 let params = view.getValues();
411
412 let backups = [];
413 view.pruneStore.getData().items.forEach(function(item) {
414 backups.push({
415 backuptime: item.data.backuptime,
416 });
417 });
418
419 me.pruneSelect(backups, params);
420
421 view.pruneStore.setData(backups);
422 },
423
424 // backups are sorted descending by date
425 populateFromSchedule: function(weekdays, hours, minutes, weekCount) {
f44e2386
MH
426 const me = this;
427
67fd0979
FE
428 let weekdayFlags = [
429 weekdays.includes('sun'),
430 weekdays.includes('mon'),
431 weekdays.includes('tue'),
432 weekdays.includes('wed'),
433 weekdays.includes('thu'),
434 weekdays.includes('fri'),
435 weekdays.includes('sat'),
436 ];
437
f44e2386
MH
438 const vmDate = me.getViewModel().get('now');
439 let todaysDate = new Date(vmDate);
67fd0979
FE
440
441 let timesOnSingleDay = [];
442
443 hours.forEach(function(hour) {
444 minutes.forEach(function(minute) {
445 todaysDate.setHours(hour);
446 todaysDate.setMinutes(minute);
447 timesOnSingleDay.push(todaysDate.getTime());
448 });
449 });
450
6a99b930 451 // sort recent times first, backups array below is ordered now -> past
7680525e 452 timesOnSingleDay.sort((a, b) => b - a);
67fd0979
FE
453
454 let backups = [];
455
456 for (let i = 0; i < 7 * weekCount; i++) {
457 let daysDate = Ext.Date.subtract(todaysDate, Ext.Date.DAY, i);
458 let weekday = parseInt(Ext.Date.format(daysDate, 'w'), 10);
459 if (weekdayFlags[weekday]) {
460 timesOnSingleDay.forEach(function(time) {
f44e2386
MH
461 const backuptime = Ext.Date.subtract(new Date(time), Ext.Date.DAY, i);
462 if (backuptime <= vmDate) {
463 backups.push({ backuptime: backuptime });
464 }
67fd0979
FE
465 });
466 }
467 }
468
469 return backups;
470 },
471
472 pruneMark: function(backups, keepCount, keepName, idFunc) {
473 if (!keepCount) {
474 return;
475 }
476
477 let alreadyIncluded = {};
478 let newlyIncluded = {};
479 let newlyIncludedCount = 0;
480
481 let finished = false;
482
483 backups.forEach(function(backup) {
484 let mark = backup.mark;
39478aa5
FE
485 if (mark && mark === 'keep') {
486 let id = idFunc(backup);
487 alreadyIncluded[id] = true;
67fd0979 488 }
39478aa5 489 });
67fd0979 490
39478aa5
FE
491 backups.forEach(function(backup) {
492 let mark = backup.mark;
493 let id = idFunc(backup);
494
495 if (finished || alreadyIncluded[id] || mark) {
67fd0979
FE
496 return;
497 }
498
499 if (!newlyIncluded[id]) {
500 if (newlyIncludedCount >= keepCount) {
501 finished = true;
502 return;
503 }
504 newlyIncluded[id] = true;
505 newlyIncludedCount++;
506 backup.mark = 'keep';
507 backup.keepName = keepName;
924d6d40 508 backup.keepCount = newlyIncludedCount;
67fd0979
FE
509 } else {
510 backup.mark = 'remove';
511 }
512 });
513 },
514
515 // backups need to be sorted descending by date
516 pruneSelect: function(backups, keepParams) {
517 let me = this;
518
519 if (Number(keepParams['keep-last']) +
520 Number(keepParams['keep-hourly']) +
521 Number(keepParams['keep-daily']) +
522 Number(keepParams['keep-weekly']) +
523 Number(keepParams['keep-monthly']) +
524 Number(keepParams['keep-yearly']) === 0) {
525 backups.forEach(function(backup) {
526 backup.mark = 'keep';
924d6d40 527 backup.keepName = 'keep-all';
67fd0979
FE
528 });
529
530 return;
531 }
532
533 me.pruneMark(backups, keepParams['keep-last'], 'keep-last', function(backup) {
534 return backup.backuptime;
535 });
536 me.pruneMark(backups, keepParams['keep-hourly'], 'keep-hourly', function(backup) {
537 return Ext.Date.format(backup.backuptime, 'H/d/m/Y');
538 });
539 me.pruneMark(backups, keepParams['keep-daily'], 'keep-daily', function(backup) {
540 return Ext.Date.format(backup.backuptime, 'd/m/Y');
541 });
542 me.pruneMark(backups, keepParams['keep-weekly'], 'keep-weekly', function(backup) {
543 // ISO-8601 week and week-based year
544 return Ext.Date.format(backup.backuptime, 'W/o');
545 });
546 me.pruneMark(backups, keepParams['keep-monthly'], 'keep-monthly', function(backup) {
547 return Ext.Date.format(backup.backuptime, 'm/Y');
548 });
549 me.pruneMark(backups, keepParams['keep-yearly'], 'keep-yearly', function(backup) {
550 return Ext.Date.format(backup.backuptime, 'Y');
551 });
552
553 backups.forEach(function(backup) {
554 backup.mark = backup.mark || 'remove';
555 });
556 },
bad26df1
TL
557
558 toggleColors: function(checkbox, checked) {
559 this.switchColor(checked);
560 },
561
562 switchColor: function(useColors) {
563 let me = this;
564 let view = me.getView();
565
566 const getStyle = name =>
567 `background-color: ${COLORS[name]}; color: ${TEXT_COLORS[name]};`;
568
569 for (const field of view.query('[isFormField]')) {
570 if (field.fieldGroup !== 'keep') {
571 continue;
572 }
573 if (useColors) {
574 field.setFieldStyle(getStyle(field.name));
575 } else {
576 field.setFieldStyle('background-color: white; color: #444;');
577 }
578 }
579
580 me.lookup('weekTable').useColors = useColors;
581 me.lookup('pruneList').useColors = useColors;
582
583 me.reloadPrune();
584 },
67fd0979
FE
585 },
586
587 keepItems: [
588 {
21b55284 589 xtype: 'prunesimulatorKeepInput',
67fd0979 590 name: 'keep-last',
67fd0979 591 fieldLabel: 'keep-last',
67fd0979 592 value: 4,
67fd0979
FE
593 },
594 {
21b55284 595 xtype: 'prunesimulatorKeepInput',
67fd0979 596 name: 'keep-hourly',
67fd0979 597 fieldLabel: 'keep-hourly',
67fd0979
FE
598 },
599 {
21b55284 600 xtype: 'prunesimulatorKeepInput',
67fd0979 601 name: 'keep-daily',
67fd0979 602 fieldLabel: 'keep-daily',
67fd0979 603 value: 5,
67fd0979
FE
604 },
605 {
21b55284 606 xtype: 'prunesimulatorKeepInput',
67fd0979 607 name: 'keep-weekly',
67fd0979 608 fieldLabel: 'keep-weekly',
67fd0979 609 value: 2,
67fd0979
FE
610 },
611 {
21b55284 612 xtype: 'prunesimulatorKeepInput',
67fd0979 613 name: 'keep-monthly',
67fd0979 614 fieldLabel: 'keep-monthly',
67fd0979
FE
615 },
616 {
21b55284 617 xtype: 'prunesimulatorKeepInput',
67fd0979 618 name: 'keep-yearly',
67fd0979 619 fieldLabel: 'keep-yearly',
67fd0979
FE
620 },
621 ],
622
623 initComponent: function() {
624 var me = this;
f44e2386 625 const vm = me.getViewModel();
67fd0979
FE
626
627 me.pruneStore = Ext.create('Ext.data.Store', {
628 model: 'pbs-prune-list',
629 sorters: { property: 'backuptime', direction: 'DESC' },
630 });
631
67fd0979
FE
632 me.items = [
633 {
634 xtype: 'panel',
6823fdc7
DM
635 layout: {
636 type: 'hbox',
637 align: 'stretch',
638 },
639 border: false,
67fd0979
FE
640 items: [
641 {
87cbd8c4 642 title: 'View Options',
67fd0979
FE
643 layout: 'anchor',
644 flex: 1,
6823fdc7
DM
645 border: false,
646 bodyPadding: 10,
67fd0979
FE
647 items: [
648 {
67fd0979
FE
649 xtype: 'checkbox',
650 name: 'showCalendar',
651 reference: 'showCalendar',
652 fieldLabel: 'Show Calendar:',
1f4befe1 653 checked: true,
67fd0979
FE
654 },
655 {
67fd0979
FE
656 xtype: 'checkbox',
657 name: 'showColors',
658 reference: 'showColors',
659 fieldLabel: 'Show Colors:',
bad26df1
TL
660 checked: true,
661 handler: 'toggleColors',
67fd0979
FE
662 },
663 ],
664 },
6823fdc7 665 { xtype: "panel", width: 1, border: 1 },
67fd0979 666 {
6ed79592 667 xtype: 'form',
f44e2386
MH
668 layout: 'hbox',
669 flex: 2,
6823fdc7 670 border: false,
87cbd8c4
TL
671 title: 'Backup Job Simulation',
672 dockedItems: [{
673 xtype: 'button',
674 text: 'Update Simulation',
675 handler: 'reloadFull',
676 formBind: true,
677 dock: 'bottom',
678 margin: '1 15',
679 }],
680 bodyPadding: 3,
4c75ee34
TL
681 items: [
682 {
87cbd8c4
TL
683 xtype: 'fieldset',
684 title: 'Backup Job',
f44e2386 685 layout: 'anchor',
87cbd8c4
TL
686 flex: 4,
687 height: 110,
f44e2386 688 defaults: {
87cbd8c4
TL
689 labelWidth: 90,
690 padding: '0 0 0 10',
691 width: '95%',
692 minWidth: 150,
f44e2386
MH
693 },
694 items: [
695 {
696 xtype: 'prunesimulatorDayOfWeekSelector',
697 name: 'schedule-weekdays',
698 fieldLabel: 'Day of week',
699 value: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
700 allowBlank: false,
701 multiSelect: true,
f44e2386
MH
702 },
703 {
704 xtype: 'prunesimulatorCalendarEvent',
705 name: 'schedule-time',
706 allowBlank: false,
707 value: '0/6:00',
87cbd8c4 708 fieldLabel: 'Schedule',
f44e2386
MH
709 },
710 ],
4c75ee34
TL
711 },
712 {
87cbd8c4
TL
713 xtype: 'fieldset',
714 title: 'Simulation Time Range',
f44e2386 715 layout: 'anchor',
87cbd8c4
TL
716 flex: 3,
717 height: 110,
f44e2386 718 defaults: {
87cbd8c4
TL
719 labelWidth: 70,
720 width: 220,
721 padding: '0 0 0 10',
722 width: '95%',
723 minWidth: 150,
f44e2386
MH
724 },
725 items: [
726 {
727 xtype: 'datefield',
728 name: 'currentDate',
87cbd8c4 729 fieldLabel: 'End Date',
f44e2386 730 allowBlank: false,
f44e2386
MH
731 format: 'Y-m-d',
732 value: vm.get('now'),
733 listeners: {
734 change: function(self, newDate) {
735 if (!self.isValid()) {
736 return;
737 }
738 const date = me.getViewModel().get('now');
739 date.setFullYear(
740 newDate.getFullYear(),
741 newDate.getMonth(),
742 newDate.getDate(),
743 );
744 },
745 },
746 },
747 {
748 xtype: 'timefield',
749 name: 'currentTime',
750 reference: 'currentTime',
87cbd8c4 751 fieldLabel: 'End Time',
f44e2386 752 allowBlank: false,
f44e2386 753 format: 'H:i',
87cbd8c4
TL
754 // cant bind value because ExtJS sets the year to 2008 to
755 // protect against DST issues and date picker zeroes hour/minute
f44e2386
MH
756 value: vm.get('now'),
757 listeners: {
758 change: function(self, time) {
759 if (!self.isValid()) {
760 return;
761 }
762 const date = me.getViewModel().get('now');
763 date.setHours(time.getHours());
764 date.setMinutes(time.getMinutes());
765 },
766 },
767 },
87cbd8c4
TL
768 {
769 xtype: 'fieldcontainer',
770 fieldLabel: 'Duration',
771 layout: 'hbox',
772 items: [{
773 xtype: 'numberfield',
774 name: 'numberOfWeeks',
775 hideLabel: true,
776 allowBlank: false,
777 minValue: 1,
778 value: 15,
779 maxValue: 260, // five years
780 flex: 1,
781 }, {
782 xtype: 'displayfield',
783 value: 'Weeks',
784 submitValue: false,
785 hideLabel: true,
786 padding: '0 0 0 5',
787 width: 40,
788 }],
789 },
f44e2386 790 ],
4c75ee34
TL
791 },
792 ],
67fd0979
FE
793 },
794 ],
795 },
796 {
797 xtype: 'panel',
6823fdc7
DM
798 layout: {
799 type: 'hbox',
800 align: 'stretch',
801 },
67fd0979 802 flex: 1,
6823fdc7 803 border: false,
67fd0979
FE
804 items: [
805 {
806 layout: 'anchor',
807 title: 'Prune Options',
6823fdc7
DM
808 border: false,
809 bodyPadding: 10,
60d2a615 810 scrollable: true,
67fd0979
FE
811 items: me.keepItems,
812 flex: 1,
813 },
6823fdc7 814 { xtype: "panel", width: 1, border: 1 },
67fd0979
FE
815 {
816 layout: 'fit',
817 title: 'Backups',
6823fdc7 818 border: false,
67fd0979
FE
819 xtype: 'prunesimulatorPruneList',
820 store: me.pruneStore,
821 reference: 'pruneList',
f44e2386 822 flex: 2,
67fd0979
FE
823 },
824 ],
825 },
826 {
827 layout: 'anchor',
828 title: 'Calendar',
829 autoScroll: true,
830 flex: 2,
831 xtype: 'prunesimulatorWeekTable',
832 reference: 'weekTable',
833 store: me.pruneStore,
834 bind: {
790627b4 835 hidden: '{!showCalendar.checked}',
67fd0979
FE
836 },
837 },
838 ];
839
840 me.callParent();
841 },
842 });
843
844 Ext.create('Ext.container.Viewport', {
845 layout: 'border',
846 renderTo: Ext.getBody(),
847 items: [
848 {
849 xtype: 'prunesimulatorPanel',
1b03910d 850 title: 'Proxmox Backup Server - Prune Simulator',
67fd0979
FE
851 region: 'west',
852 layout: {
853 type: 'vbox',
854 align: 'stretch',
855 pack: 'start',
856 },
f5c6a2c9
TL
857 flex: 3,
858 maxWidth: 1090,
67fd0979
FE
859 },
860 {
861 xtype: 'prunesimulatorDocumentation',
862 title: 'Usage',
6823fdc7 863 border: false,
f5c6a2c9 864 flex: 2,
67fd0979
FE
865 region: 'center',
866 },
867 ],
868 });
869});
870