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