]> git.proxmox.com Git - proxmox-backup.git/blob - www/tape/window/TapeRestore.js
fix #3934 tape owner-selector to Authid
[proxmox-backup.git] / www / tape / window / TapeRestore.js
1 Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
2 extend: 'Ext.window.Window',
3 alias: 'widget.pbsTapeRestoreWindow',
4 mixins: ['Proxmox.Mixin.CBind'],
5
6 title: gettext('Restore Media-Set'),
7
8 width: 800,
9 height: 500,
10
11 url: '/api2/extjs/tape/restore',
12 method: 'POST',
13
14 resizable: false,
15 modal: true,
16
17 mediaset: undefined,
18 prefilter: undefined,
19 uuid: undefined,
20
21 cbindData: function(config) {
22 let me = this;
23 if (me.prefilter !== undefined) {
24 me.title = gettext('Restore Snapshot(s)');
25 }
26 return {};
27 },
28
29 layout: 'fit',
30 bodyPadding: 0,
31
32 viewModel: {
33 data: {
34 uuid: "",
35 singleDatastore: true,
36 },
37 formulas: {
38 singleSelectorLabel: get =>
39 get('singleDatastore') ? gettext('Target Datastore') : gettext('Default Datastore'),
40 singleSelectorEmptyText: get => get('singleDatastore') ? '' : Proxmox.Utils.NoneText,
41 },
42 },
43
44 controller: {
45 xclass: 'Ext.app.ViewController',
46
47 panelIsValid: function(panel) {
48 return panel.query('[isFormField]').every(field => field.isValid());
49 },
50
51 changeMediaSet: function(field, value) {
52 let me = this;
53 let vm = me.getViewModel();
54 vm.set('uuid', value);
55 me.updateSnapshots();
56 },
57
58 checkValidity: function() {
59 let me = this;
60
61 let tabpanel = me.lookup('tabpanel');
62 if (!tabpanel) {
63 return; // can get triggered early, when the tabpanel is not yet available
64 }
65 let items = tabpanel.items;
66
67 let indexOfActiveTab = items.indexOf(tabpanel.getActiveTab());
68 let indexOfLastValidTab = 0;
69
70 let checkValidity = true;
71 items.each((panel) => {
72 if (checkValidity) {
73 panel.setDisabled(false);
74 indexOfLastValidTab = items.indexOf(panel);
75 if (!me.panelIsValid(panel)) {
76 checkValidity = false;
77 }
78 } else {
79 panel.setDisabled(true);
80 }
81
82 return true;
83 });
84
85 if (indexOfLastValidTab < indexOfActiveTab) {
86 tabpanel.setActiveTab(indexOfLastValidTab);
87 } else {
88 me.setButtonState(tabpanel.getActiveTab());
89 }
90 },
91
92 setButtonState: function(panel) {
93 let me = this;
94 let isValid = me.panelIsValid(panel);
95 let nextButton = me.lookup('nextButton');
96 let finishButton = me.lookup('finishButton');
97 nextButton.setDisabled(!isValid);
98 finishButton.setDisabled(!isValid);
99 },
100
101 changeButtonVisibility: function(tabpanel, newItem) {
102 let me = this;
103 let items = tabpanel.items;
104
105 let backButton = me.lookup('backButton');
106 let nextButton = me.lookup('nextButton');
107 let finishButton = me.lookup('finishButton');
108
109 let isLast = items.last() === newItem;
110 let isFirst = items.first() === newItem;
111
112 backButton.setVisible(!isFirst);
113 nextButton.setVisible(!isLast);
114 finishButton.setVisible(isLast);
115
116 me.setButtonState(newItem);
117 },
118
119 previousTab: function() {
120 let me = this;
121 let tabpanel = me.lookup('tabpanel');
122 let index = tabpanel.items.indexOf(tabpanel.getActiveTab());
123 tabpanel.setActiveTab(index - 1);
124 },
125
126 nextTab: function() {
127 let me = this;
128 let tabpanel = me.lookup('tabpanel');
129 let index = tabpanel.items.indexOf(tabpanel.getActiveTab());
130 tabpanel.setActiveTab(index + 1);
131 },
132
133 getValues: function() {
134 let me = this;
135
136 let values = {};
137
138 let tabpanel = me.lookup('tabpanel');
139 tabpanel
140 .query('inputpanel')
141 .forEach((panel) =>
142 Proxmox.Utils.assemble_field_data(values, panel.getValues()));
143
144 return values;
145 },
146
147 finish: function() {
148 let me = this;
149 let view = me.getView();
150
151 let values = me.getValues();
152 let url = view.url;
153 let method = view.method;
154
155 Proxmox.Utils.API2Request({
156 url,
157 waitMsgTarget: view,
158 method,
159 params: values,
160 failure: function(response, options) {
161 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
162 },
163 success: function(response, options) {
164 // keep around so we can trigger our close events when background action completes
165 view.hide();
166
167 Ext.create('Proxmox.window.TaskViewer', {
168 autoShow: true,
169 upid: response.result.data,
170 listeners: {
171 destroy: () => view.close(),
172 },
173 });
174 },
175 });
176 },
177
178 updateDatastores: function(grid, values) {
179 let me = this;
180 if (values === 'all') {
181 values = [];
182 }
183 let datastores = {};
184 values.forEach((snapshotOrDatastore) => {
185 let datastore = snapshotOrDatastore;
186 if (snapshotOrDatastore.indexOf(':') !== -1) {
187 let snapshot = snapshotOrDatastore;
188 let match = snapshot.split(':');
189 datastore = match[0];
190 } datastores[datastore] = true;
191 });
192
193 me.setDataStores(Object.keys(datastores));
194 },
195
196 setDataStores: function(datastores, initial) {
197 let me = this;
198
199 // save all datastores on the first setting, and restore them if we selected all
200 if (initial) {
201 me.datastores = datastores;
202 } else if (datastores.length === 0) {
203 datastores = me.datastores;
204 }
205
206 const singleDatastore = !datastores || datastores.length <= 1;
207 me.getViewModel().set('singleDatastore', singleDatastore);
208
209 let grid = me.lookup('mappingGrid');
210 if (!singleDatastore && grid) {
211 grid.setDataStores(datastores);
212 }
213 },
214
215 updateSnapshots: function() {
216 let me = this;
217 let view = me.getView();
218 let grid = me.lookup('snapshotGrid');
219 let vm = me.getViewModel();
220 let uuid = vm.get('uuid');
221
222 Proxmox.Utils.API2Request({
223 waitMsgTarget: view,
224 url: `/tape/media/content?media-set=${uuid}`,
225 success: function(response, opt) {
226 let datastores = {};
227 for (const content of response.result.data) {
228 datastores[content.store] = true;
229 }
230 me.setDataStores(Object.keys(datastores), true);
231 if (response.result.data.length > 0) {
232 grid.setDisabled(false);
233 grid.setData(response.result.data);
234 grid.getSelectionModel().selectAll();
235 // we've shown a big list, center the window again
236 view.center();
237 }
238 },
239 failure: function() {
240 // ignore failing api call, maybe catalog is missing
241 me.setDataStores([], true);
242 },
243 });
244 },
245
246 init: function(view) {
247 let me = this;
248 let vm = me.getViewModel();
249
250 vm.set('uuid', view.uuid);
251 },
252
253 control: {
254 '[isFormField]': {
255 change: 'checkValidity',
256 validitychange: 'checkValidity',
257 },
258 'tabpanel': {
259 tabchange: 'changeButtonVisibility',
260 },
261 },
262 },
263
264 buttons: [
265 {
266 text: gettext('Back'),
267 reference: 'backButton',
268 handler: 'previousTab',
269 hidden: true,
270 },
271 {
272 text: gettext('Next'),
273 reference: 'nextButton',
274 handler: 'nextTab',
275 },
276 {
277 text: gettext('Restore'),
278 reference: 'finishButton',
279 handler: 'finish',
280 hidden: true,
281 },
282 ],
283
284 items: [
285 {
286 xtype: 'tabpanel',
287 reference: 'tabpanel',
288 layout: 'fit',
289 bodyPadding: 10,
290 items: [
291 {
292 title: gettext('Snapshot Selection'),
293 xtype: 'inputpanel',
294 onGetValues: function(values) {
295 let me = this;
296
297 if (values !== "all" &&
298 Ext.isString(values.snapshots) &&
299 values.snapshots &&
300 values.snapshots.indexOf(':') !== -1
301 ) {
302 values.snapshots = values.snapshots.split(',');
303 } else {
304 delete values.snapshots;
305 }
306
307 return values;
308 },
309
310 column1: [
311 {
312 xtype: 'pbsMediaSetSelector',
313 fieldLabel: gettext('Media-Set'),
314 width: 350,
315 submitValue: false,
316 emptyText: gettext('Select Media-Set to restore'),
317 bind: {
318 value: '{uuid}',
319 },
320 cbind: {
321 hidden: '{uuid}',
322 disabled: '{uuid}',
323 },
324 listeners: {
325 change: 'changeMediaSet',
326 },
327 },
328 {
329 xtype: 'displayfield',
330 fieldLabel: gettext('Media-Set'),
331 cbind: {
332 value: '{mediaset}',
333 hidden: '{!uuid}',
334 disabled: '{!uuid}',
335 },
336 },
337 ],
338
339 column2: [
340 {
341 xtype: 'displayfield',
342 fieldLabel: gettext('Media-Set UUID'),
343 name: 'media-set',
344 submitValue: true,
345 bind: {
346 value: '{uuid}',
347 hidden: '{!uuid}',
348 disabled: '{!uuid}',
349 },
350 },
351 ],
352
353 columnB: [
354 {
355 xtype: 'pbsTapeSnapshotGrid',
356 reference: 'snapshotGrid',
357 name: 'snapshots',
358 height: 322,
359 disabled: true, // will be shown/enabled on successful load
360 listeners: {
361 change: 'updateDatastores',
362 },
363 cbind: {
364 prefilter: '{prefilter}',
365 },
366 },
367 ],
368 },
369 {
370 title: gettext('Target'),
371 xtype: 'inputpanel',
372 onGetValues: function(values) {
373 let me = this;
374 let datastores = [];
375 if (values.store.toString() !== "") {
376 datastores.push(values.store);
377 delete values.store;
378 }
379
380 if (values.mapping.toString() !== "") {
381 datastores.push(values.mapping);
382 }
383 delete values.mapping;
384
385 values.store = datastores.join(',');
386
387 return values;
388 },
389 column1: [
390 {
391 xtype: 'pmxUserSelector',
392 name: 'notify-user',
393 fieldLabel: gettext('Notify User'),
394 emptyText: gettext('Current User'),
395 value: null,
396 allowBlank: true,
397 skipEmptyText: true,
398 renderer: Ext.String.htmlEncode,
399 },
400 {
401 xtype: 'pbsAuthidSelector',
402 name: 'owner',
403 fieldLabel: gettext('Owner'),
404 emptyText: gettext('Current Auth ID'),
405 value: null,
406 allowBlank: true,
407 skipEmptyText: true,
408 renderer: Ext.String.htmlEncode,
409 },
410 ],
411
412 column2: [
413 {
414 xtype: 'pbsDriveSelector',
415 name: 'drive',
416 fieldLabel: gettext('Drive'),
417 labelWidth: 120,
418 },
419 {
420 xtype: 'pbsDataStoreSelector',
421 name: 'store',
422 labelWidth: 120,
423 bind: {
424 fieldLabel: '{singleSelectorLabel}',
425 emptyText: '{singleSelectorEmptyText}',
426 allowBlank: '{!singleDatastore}',
427 },
428 listeners: {
429 change: function(field, value) {
430 this.up('window').lookup('mappingGrid').setNeedStores(!value);
431 },
432 },
433 },
434 ],
435
436 columnB: [
437 {
438 xtype: 'displayfield',
439 fieldLabel: gettext('Datastore Mapping'),
440 labelWidth: 200,
441 bind: {
442 hidden: '{singleDatastore}',
443 },
444 },
445 {
446 xtype: 'pbsDataStoreMappingField',
447 name: 'mapping',
448 reference: 'mappingGrid',
449 height: 260,
450 defaultBindProperty: 'value',
451 bind: {
452 hidden: '{singleDatastore}',
453 },
454 },
455 ],
456 },
457 ],
458 },
459 ],
460
461 listeners: {
462 afterrender: 'updateSnapshots',
463 },
464 });
465
466 Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
467 extend: 'Ext.grid.Panel',
468 alias: 'widget.pbsDataStoreMappingField',
469 mixins: ['Ext.form.field.Field'],
470
471 scrollable: true,
472
473 getValue: function() {
474 let me = this;
475 let datastores = [];
476 me.getStore().each(rec => {
477 let { source, target } = rec.data;
478 if (target && target !== "") {
479 datastores.push(`${source}=${target}`);
480 }
481 });
482
483 return datastores.join(',');
484 },
485
486 viewModel: {
487 data: {
488 needStores: false, // this determines if we need at least one valid mapping
489 },
490 formulas: {
491 emptyMeans: get => get('needStores') ? Proxmox.Utils.NoneText : Proxmox.Utils.defaultText,
492 },
493 },
494
495 setNeedStores: function(needStores) {
496 let me = this;
497 me.getViewModel().set('needStores', needStores);
498 me.checkChange();
499 me.validate();
500 },
501
502 setValue: function(value) {
503 let me = this;
504 me.setDataStores(value);
505 return me;
506 },
507
508 getErrors: function(value) {
509 let me = this;
510 let error = false;
511
512 if (me.getViewModel().get('needStores')) {
513 error = true;
514 me.getStore().each(rec => {
515 if (rec.data.target) {
516 error = false;
517 }
518 });
519 }
520
521 let el = me.getActionEl();
522 if (error) {
523 me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
524 let errorMsg = gettext("Need at least one mapping");
525 if (el) {
526 el.dom.setAttribute('data-errorqtip', errorMsg);
527 }
528
529 return [errorMsg];
530 }
531 me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
532 if (el) {
533 el.dom.setAttribute('data-errorqtip', "");
534 }
535 return [];
536 },
537
538 setDataStores: function(datastores) {
539 let me = this;
540
541 let data = [];
542 for (const datastore of datastores) {
543 data.push({
544 source: datastore,
545 target: '',
546 });
547 }
548
549 me.getStore().setData(data);
550 },
551
552 viewConfig: {
553 markDirty: false,
554 },
555
556 store: { data: [] },
557
558 columns: [
559 {
560 text: gettext('Source Datastore'),
561 dataIndex: 'source',
562 flex: 1,
563 },
564 {
565 text: gettext('Target Datastore'),
566 xtype: 'widgetcolumn',
567 dataIndex: 'target',
568 flex: 1,
569 widget: {
570 xtype: 'pbsDataStoreSelector',
571 isFormField: false,
572 allowBlank: true,
573 bind: {
574 emptyText: '{emptyMeans}',
575 },
576 listeners: {
577 change: function(selector, value) {
578 let me = this;
579 let rec = me.getWidgetRecord();
580 if (!rec) {
581 return;
582 }
583 rec.set('target', value);
584 me.up('grid').checkChange();
585 },
586 },
587 },
588 },
589 ],
590 });
591
592 Ext.define('PBS.TapeManagement.SnapshotGrid', {
593 extend: 'Ext.grid.Panel',
594 alias: 'widget.pbsTapeSnapshotGrid',
595 mixins: ['Ext.form.field.Field'],
596
597 getValue: function() {
598 let me = this;
599 let snapshots = [];
600
601 let storeCounts = {};
602
603 me.getSelection().forEach((rec) => {
604 let id = rec.get('id');
605 let store = rec.data.store;
606 let snap = rec.data.snapshot;
607 // only add if not filtered
608 if (me.store.findExact('id', id) !== -1) {
609 snapshots.push(`${store}:${snap}`);
610 if (storeCounts[store] === undefined) {
611 storeCounts[store] = 0;
612 }
613 storeCounts[store]++;
614 }
615 });
616
617 // getSource returns null if data is not filtered
618 let originalData = me.store.getData().getSource() || me.store.getData();
619
620 if (snapshots.length === originalData.length) {
621 return "all";
622 }
623
624 let wholeStores = [];
625 let wholeStoresSelected = true;
626 for (const [store, count] of Object.entries(storeCounts)) {
627 if (me.storeCounts[store] === count) {
628 wholeStores.push(store);
629 } else {
630 wholeStoresSelected = false;
631 break;
632 }
633 }
634
635 if (wholeStoresSelected) {
636 return wholeStores;
637 }
638
639 return snapshots;
640 },
641
642 setValue: function(value) {
643 let me = this;
644 // not implemented
645 return me;
646 },
647
648 getErrors: function(value) {
649 let me = this;
650 if (me.getSelection().length < 1) {
651 me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
652 let errorMsg = gettext("Need at least one snapshot");
653 let el = me.getActionEl();
654 if (el) {
655 el.dom.setAttribute('data-errorqtip', errorMsg);
656 }
657
658 return [errorMsg];
659 }
660 me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
661 let el = me.getActionEl();
662 if (el) {
663 el.dom.setAttribute('data-errorqtip', "");
664 }
665 return [];
666 },
667
668 setData: function(records) {
669 let me = this;
670 let storeCounts = {};
671 records.forEach((rec) => {
672 let store = rec.store;
673 if (storeCounts[store] === undefined) {
674 storeCounts[store] = 0;
675 }
676 storeCounts[store]++;
677 });
678 me.storeCounts = storeCounts;
679 me.getStore().setData(records);
680 },
681
682 scrollable: true,
683 plugins: 'gridfilters',
684
685 viewConfig: {
686 emptyText: gettext('No Snapshots'),
687 markDirty: false,
688 },
689
690 selModel: 'checkboxmodel',
691 store: {
692 sorters: ['store', 'snapshot'],
693 data: [],
694 filters: [],
695 },
696
697 listeners: {
698 selectionchange: function() {
699 // to trigger validity and error checks
700 this.checkChange();
701 },
702 },
703
704 checkChangeEvents: [
705 'selectionchange',
706 'change',
707 ],
708
709 columns: [
710 {
711 text: gettext('Source Datastore'),
712 dataIndex: 'store',
713 filter: {
714 type: 'list',
715 },
716 flex: 1,
717 },
718 {
719 text: gettext('Snapshot'),
720 dataIndex: 'snapshot',
721 filter: {
722 type: 'string',
723 },
724 flex: 2,
725 },
726 ],
727
728 initComponent: function() {
729 let me = this;
730 me.callParent();
731 if (me.prefilter !== undefined) {
732 if (me.prefilter.store !== undefined) {
733 me.store.filters.add(
734 {
735 id: 'x-gridfilter-store',
736 property: 'store',
737 operator: 'in',
738 value: [me.prefilter.store],
739 },
740 );
741 }
742
743 if (me.prefilter.snapshot !== undefined) {
744 me.store.filters.add(
745 {
746 id: 'x-gridfilter-snapshot',
747 property: 'snapshot',
748 value: me.prefilter.snapshot,
749 },
750 );
751 }
752 }
753
754 me.mon(me.store, 'filterchange', () => me.checkChange());
755 },
756 });
757
758 Ext.define('PBS.TapeManagement.MediaSetSelector', {
759 extend: 'Proxmox.form.ComboGrid',
760 alias: 'widget.pbsMediaSetSelector',
761
762 allowBlank: false,
763 displayField: 'media-set-name',
764 valueField: 'media-set-uuid',
765 autoSelect: false,
766
767 store: {
768 proxy: {
769 type: 'proxmox',
770 url: '/api2/json/tape/media/media-sets',
771 },
772 autoLoad: true,
773 idProperty: 'media-set-uuid',
774 sorters: ['pool', 'media-set-ctime'],
775 },
776
777 listConfig: {
778 width: 600,
779 columns: [
780 {
781 text: gettext('Pool'),
782 dataIndex: 'pool',
783 flex: 1,
784 },
785 {
786 text: gettext('Name'),
787 dataIndex: 'media-set-name',
788 width: 180,
789 },
790 {
791 text: gettext('Media-Set UUID'),
792 dataIndex: 'media-set-uuid',
793 width: 280,
794 },
795 ],
796 },
797 });