]> git.proxmox.com Git - proxmox-backup.git/blob - www/tape/ChangerStatus.js
ui: tape/ChangerStatus: show more inventory info for tapes in slots
[proxmox-backup.git] / www / tape / ChangerStatus.js
1 Ext.define('PBS.TapeManagement.ChangerStatus', {
2 extend: 'Ext.panel.Panel',
3 alias: 'widget.pbsChangerStatus',
4
5 viewModel: {
6 data: {
7 changer: '',
8 },
9
10 formulas: {
11 changerSelected: (get) => get('changer') !== '',
12 },
13 },
14
15 controller: {
16 xclass: 'Ext.app.ViewController',
17
18 changerChange: function(field, value) {
19 let me = this;
20 let view = me.getView();
21 let vm = me.getViewModel();
22 vm.set('changer', value);
23 if (view.rendered) {
24 me.reload();
25 }
26 },
27
28 importTape: function(view, rI, cI, button, el, record) {
29 let me = this;
30 let vm = me.getViewModel();
31 let from = record.data['entry-id'];
32 let changer = encodeURIComponent(vm.get('changer'));
33 Ext.create('Proxmox.window.Edit', {
34 title: gettext('Import'),
35 isCreate: true,
36 submitText: gettext('OK'),
37 method: 'POST',
38 url: `/api2/extjs/tape/changer/${changer}/transfer`,
39 items: [
40 {
41 xtype: 'displayfield',
42 name: 'from',
43 value: from,
44 submitValue: true,
45 fieldLabel: gettext('From Slot'),
46 },
47 {
48 xtype: 'proxmoxintegerfield',
49 name: 'to',
50 fieldLabel: gettext('To Slot'),
51 },
52 ],
53 listeners: {
54 destroy: function() {
55 me.reload();
56 },
57 },
58 }).show();
59 },
60
61 slotTransfer: function(view, rI, cI, button, el, record) {
62 let me = this;
63 let vm = me.getViewModel();
64 let from = record.data['entry-id'];
65 let changer = encodeURIComponent(vm.get('changer'));
66 Ext.create('Proxmox.window.Edit', {
67 title: gettext('Transfer'),
68 isCreate: true,
69 submitText: gettext('OK'),
70 method: 'POST',
71 url: `/api2/extjs/tape/changer/${changer}/transfer`,
72 items: [
73 {
74 xtype: 'displayfield',
75 name: 'from',
76 value: from,
77 submitValue: true,
78 fieldLabel: gettext('From Slot'),
79 },
80 {
81 xtype: 'proxmoxintegerfield',
82 name: 'to',
83 fieldLabel: gettext('To Slot'),
84 },
85 ],
86 listeners: {
87 destroy: function() {
88 me.reload();
89 },
90 },
91 }).show();
92 },
93
94 load: function(view, rI, cI, button, el, record) {
95 let me = this;
96 let vm = me.getViewModel();
97 let label = record.data['label-text'];
98
99 let changer = vm.get('changer');
100
101 Ext.create('Proxmox.window.Edit', {
102 isCreate: true,
103 submitText: gettext('OK'),
104 title: gettext('Load Media into Drive'),
105 url: `/api2/extjs/tape/drive`,
106 submitUrl: function(url, values) {
107 let drive = values.drive;
108 delete values.drive;
109 return `${url}/${encodeURIComponent(drive)}/load-media`;
110 },
111 items: [
112 {
113 xtype: 'displayfield',
114 name: 'label-text',
115 value: label,
116 submitValue: true,
117 fieldLabel: gettext('Media'),
118 },
119 {
120 xtype: 'pbsDriveSelector',
121 fieldLabel: gettext('Drive'),
122 changer: changer,
123 name: 'drive',
124 },
125 ],
126 listeners: {
127 destroy: function() {
128 me.reload();
129 },
130 },
131 }).show();
132 },
133
134 unload: async function(view, rI, cI, button, el, record) {
135 let me = this;
136 let drive = record.data.name;
137 Proxmox.Utils.setErrorMask(view, true);
138 try {
139 await PBS.Async.api2({
140 method: 'PUT',
141 url: `/api2/extjs/tape/drive/${encodeURIComponent(drive)}/unload`,
142 });
143 Proxmox.Utils.setErrorMask(view);
144 me.reload();
145 } catch (error) {
146 Ext.Msg.alert(gettext('Error'), error);
147 Proxmox.Utils.setErrorMask(view);
148 me.reload();
149 }
150 },
151
152 driveCommand: function(driveid, command, callback, params, method) {
153 let me = this;
154 let view = me.getView();
155 params = params || {};
156 method = method || 'GET';
157 Proxmox.Utils.API2Request({
158 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
159 method,
160 waitMsgTarget: view,
161 params,
162 success: function(response) {
163 callback(response);
164 },
165 failure: function(response) {
166 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
167 },
168 });
169 },
170
171 cartridgeMemory: function(view, rI, cI, button, el, record) {
172 let me = this;
173 let drive = record.data.name;
174 me.driveCommand(drive, 'cartridge-memory', function(response) {
175 Ext.create('Ext.window.Window', {
176 title: gettext('Cartridge Memory'),
177 modal: true,
178 width: 600,
179 height: 450,
180 layout: 'fit',
181 scrollable: true,
182 items: [
183 {
184 xtype: 'grid',
185 store: {
186 data: response.result.data,
187 },
188 columns: [
189 {
190 text: gettext('ID'),
191 dataIndex: 'id',
192 width: 60,
193 },
194 {
195 text: gettext('Name'),
196 dataIndex: 'name',
197 flex: 2,
198 },
199 {
200 text: gettext('Value'),
201 dataIndex: 'value',
202 flex: 1,
203 },
204 ],
205 },
206 ],
207 }).show();
208 });
209 },
210
211 cleanDrive: function(view, rI, cI, button, el, record) {
212 let me = this;
213 let drive = record.data.name;
214 me.driveCommand(drive, 'clean', function(response) {
215 Ext.create('Proxmox.window.TaskProgress', {
216 upid: response.result.data,
217 taskDone: function() {
218 me.reload();
219 },
220 }).show();
221 }, {}, 'PUT');
222 },
223
224 volumeStatistics: function(view, rI, cI, button, el, record) {
225 let me = this;
226 let drive = record.data.name;
227 me.driveCommand(drive, 'volume-statistics', function(response) {
228 Ext.create('Ext.window.Window', {
229 title: gettext('Volume Statistics'),
230 modal: true,
231 width: 600,
232 height: 450,
233 layout: 'fit',
234 scrollable: true,
235 items: [
236 {
237 xtype: 'grid',
238 store: {
239 data: response.result.data,
240 },
241 columns: [
242 {
243 text: gettext('ID'),
244 dataIndex: 'id',
245 width: 60,
246 },
247 {
248 text: gettext('Name'),
249 dataIndex: 'name',
250 flex: 2,
251 },
252 {
253 text: gettext('Value'),
254 dataIndex: 'value',
255 flex: 1,
256 },
257 ],
258 },
259 ],
260 }).show();
261 });
262 },
263
264 readLabel: function(view, rI, cI, button, el, record) {
265 let me = this;
266 let drive = record.data.name;
267 me.driveCommand(drive, 'read-label', function(response) {
268 let lines = [];
269 for (const [key, val] of Object.entries(response.result.data)) {
270 lines.push(`${key}: ${val}`);
271 }
272
273 let txt = lines.join('<br>');
274
275 Ext.Msg.show({
276 title: gettext('Label Information'),
277 message: txt,
278 icon: undefined,
279 });
280 });
281 },
282
283 status: function(view, rI, cI, button, el, record) {
284 let me = this;
285 let drive = record.data.name;
286 me.driveCommand(drive, 'status', function(response) {
287 let lines = [];
288 for (const [key, val] of Object.entries(response.result.data)) {
289 lines.push(`${key}: ${val}`);
290 }
291
292 let txt = lines.join('<br>');
293
294 Ext.Msg.show({
295 title: gettext('Status'),
296 message: txt,
297 icon: undefined,
298 });
299 });
300 },
301
302 reloadList: function() {
303 let me = this;
304 me.lookup('changerselector').getStore().load();
305 },
306
307 barcodeLabel: function() {
308 let me = this;
309 let vm = me.getViewModel();
310 let changer = vm.get('changer');
311 if (changer === '') {
312 return;
313 }
314
315 Ext.create('Proxmox.window.Edit', {
316 title: gettext('Barcode Label'),
317 showTaskViewer: true,
318 method: 'POST',
319 url: '/api2/extjs/tape/drive',
320 submitUrl: function(url, values) {
321 let drive = values.drive;
322 delete values.drive;
323 return `${url}/${encodeURIComponent(drive)}/barcode-label-media`;
324 },
325
326 items: [
327 {
328 xtype: 'pbsDriveSelector',
329 fieldLabel: gettext('Drive'),
330 name: 'drive',
331 changer: changer,
332 },
333 {
334 xtype: 'pbsMediaPoolSelector',
335 fieldLabel: gettext('Pool'),
336 name: 'pool',
337 skipEmptyText: true,
338 allowBlank: true,
339 },
340 ],
341 }).show();
342 },
343
344 inventory: function() {
345 let me = this;
346 let vm = me.getViewModel();
347 let changer = vm.get('changer');
348 if (changer === '') {
349 return;
350 }
351
352 Ext.create('Proxmox.window.Edit', {
353 title: gettext('Inventory'),
354 showTaskViewer: true,
355 method: 'PUT',
356 url: '/api2/extjs/tape/drive',
357 submitUrl: function(url, values) {
358 let drive = values.drive;
359 delete values.drive;
360 return `${url}/${encodeURIComponent(drive)}/inventory`;
361 },
362
363 items: [
364 {
365 xtype: 'pbsDriveSelector',
366 fieldLabel: gettext('Drive'),
367 name: 'drive',
368 changer: changer,
369 },
370 ],
371 }).show();
372 },
373
374 reload: async function() {
375 let me = this;
376 let view = me.getView();
377 let vm = me.getViewModel();
378 let changer = vm.get('changer');
379 if (changer === '') {
380 return;
381 }
382
383 try {
384 Proxmox.Utils.setErrorMask(view, true);
385 Proxmox.Utils.setErrorMask(me.lookup('content'));
386 let status_fut = PBS.Async.api2({
387 url: `/api2/extjs/tape/changer/${encodeURIComponent(changer)}/status`,
388 });
389 let drives_fut = PBS.Async.api2({
390 url: `/api2/extjs/tape/drive?changer=${encodeURIComponent(changer)}`,
391 });
392
393 let tapes_fut = PBS.Async.api2({
394 url: '/api2/extjs/tape/media/list',
395 });
396
397 let [status, drives, tapes_list] = await Promise.all([status_fut, drives_fut, tapes_fut]);
398
399 let data = {
400 slot: [],
401 'import-export': [],
402 drive: [],
403 };
404
405 let tapes = {};
406
407 for (const tape of tapes_list.result.data) {
408 tapes[tape['label-text']] = {
409 labeled: true,
410 pool: tape.pool,
411 status: tape.expired ? 'expired' : tape.status,
412 };
413 }
414
415 let drive_entries = {};
416
417 for (const entry of drives.result.data) {
418 drive_entries[entry['changer-drivenum'] || 0] = entry;
419 }
420
421 for (let entry of status.result.data) {
422 let type = entry['entry-kind'];
423
424 if (type === 'drive' && drive_entries[entry['entry-id']] !== undefined) {
425 entry = Ext.applyIf(entry, drive_entries[entry['entry-id']]);
426 }
427
428 if (tapes[entry['label-text']] !== undefined) {
429 entry['is-labeled'] = true;
430 entry.pool = tapes[entry['label-text']].pool;
431 entry.status = tapes[entry['label-text']].status;
432 } else {
433 entry['is-labeled'] = false;
434 }
435
436 data[type].push(entry);
437 }
438
439
440 me.lookup('slots').getStore().setData(data.slot);
441 me.lookup('import_export').getStore().setData(data['import-export']);
442 me.lookup('drives').getStore().setData(data.drive);
443
444 Proxmox.Utils.setErrorMask(view);
445 } catch (err) {
446 Proxmox.Utils.setErrorMask(view);
447 Proxmox.Utils.setErrorMask(me.lookup('content'), err);
448 }
449 },
450 },
451
452 listeners: {
453 activate: 'reload',
454 },
455
456 tbar: [
457 {
458 fieldLabel: gettext('Changer'),
459 xtype: 'pbsChangerSelector',
460 reference: 'changerselector',
461 autoSelect: true,
462 listeners: {
463 change: 'changerChange',
464 },
465 },
466 '-',
467 {
468 text: gettext('Reload'),
469 xtype: 'proxmoxButton',
470 handler: 'reload',
471 selModel: false,
472 },
473 '-',
474 {
475 text: gettext('Barcode Label'),
476 xtype: 'proxmoxButton',
477 handler: 'barcodeLabel',
478 iconCls: 'fa fa-barcode',
479 bind: {
480 disabled: '{!changerSelected}',
481 },
482 },
483 {
484 text: gettext('Inventory'),
485 xtype: 'proxmoxButton',
486 handler: 'inventory',
487 iconCls: 'fa fa-book',
488 bind: {
489 disabled: '{!changerSelected}',
490 },
491 },
492 ],
493
494 layout: 'auto',
495 bodyPadding: 5,
496 scrollable: true,
497
498 items: [
499 {
500 xtype: 'container',
501 reference: 'content',
502 layout: {
503 type: 'hbox',
504 aling: 'stretch',
505 },
506 items: [
507 {
508 xtype: 'grid',
509 reference: 'slots',
510 title: gettext('Slots'),
511 padding: 5,
512 flex: 1,
513 store: {
514 data: [],
515 },
516 columns: [
517 {
518 text: gettext('Slot'),
519 dataIndex: 'entry-id',
520 width: 50,
521 },
522 {
523 text: gettext("Content"),
524 dataIndex: 'label-text',
525 flex: 1,
526 renderer: (value) => value || '',
527 },
528 {
529 text: gettext('Inventory'),
530 dataIndex: 'is-labeled',
531 flex: 1,
532 renderer: function(value, mD, record) {
533 if (!record.data['label-text']) {
534 return "";
535 }
536
537 if (record.data['label-text'].startsWith("CLN")) {
538 return "";
539 }
540
541 if (!value) {
542 return gettext('Not Labeled');
543 }
544
545 let status = record.data.status;
546 if (record.data.pool) {
547 return `${status} (${record.data.pool})`;
548 }
549 return status;
550 },
551 },
552 {
553 text: gettext('Actions'),
554 xtype: 'actioncolumn',
555 width: 100,
556 items: [
557 {
558 iconCls: 'fa fa-rotate-90 fa-exchange',
559 handler: 'slotTransfer',
560 tooltip: gettext('Transfer'),
561 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
562 },
563 {
564 iconCls: 'fa fa-rotate-90 fa-upload',
565 handler: 'load',
566 tooltip: gettext('Load'),
567 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
568 },
569 ],
570 },
571 ],
572 },
573 {
574 xtype: 'container',
575 flex: 2,
576 defaults: {
577 padding: 5,
578 },
579 items: [
580 {
581 xtype: 'grid',
582 reference: 'drives',
583 title: gettext('Drives'),
584 store: {
585 fields: ['entry-id', 'label-text', 'model', 'name', 'vendor', 'serial'],
586 data: [],
587 },
588 columns: [
589 {
590 text: gettext('Slot'),
591 dataIndex: 'entry-id',
592 width: 50,
593 },
594 {
595 text: gettext("Content"),
596 dataIndex: 'label-text',
597 flex: 1,
598 renderer: (value) => value || '',
599 },
600 {
601 text: gettext("Name"),
602 sortable: true,
603 dataIndex: 'name',
604 flex: 1,
605 renderer: Ext.htmlEncode,
606 },
607 {
608 text: gettext("Vendor"),
609 sortable: true,
610 dataIndex: 'vendor',
611 flex: 1,
612 renderer: Ext.htmlEncode,
613 },
614 {
615 text: gettext("Model"),
616 sortable: true,
617 dataIndex: 'model',
618 flex: 1,
619 renderer: Ext.htmlEncode,
620 },
621 {
622 text: gettext("Serial"),
623 sortable: true,
624 dataIndex: 'serial',
625 flex: 1,
626 renderer: Ext.htmlEncode,
627 },
628 {
629 xtype: 'actioncolumn',
630 text: gettext('Actions'),
631 width: 140,
632 items: [
633 {
634 iconCls: 'fa fa-rotate-270 fa-upload',
635 handler: 'unload',
636 tooltip: gettext('Unload'),
637 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
638 },
639 {
640 iconCls: 'fa fa-hdd-o',
641 handler: 'cartridgeMemory',
642 tooltip: gettext('Cartridge Memory'),
643 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
644 },
645 {
646 iconCls: 'fa fa-line-chart',
647 handler: 'volumeStatistics',
648 tooltip: gettext('Volume Statistics'),
649 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
650 },
651 {
652 iconCls: 'fa fa-tag',
653 handler: 'readLabel',
654 tooltip: gettext('Read Label'),
655 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
656 },
657 {
658 iconCls: 'fa fa-info-circle',
659 tooltip: gettext('Status'),
660 handler: 'status',
661 },
662 {
663 iconCls: 'fa fa-shower',
664 tooltip: gettext('Clean Drive'),
665 handler: 'cleanDrive',
666 },
667 ],
668 },
669 ],
670 },
671 {
672 xtype: 'grid',
673 reference: 'import_export',
674 store: {
675 data: [],
676 },
677 title: gettext('Import-Export'),
678 columns: [
679 {
680 text: gettext('Slot'),
681 dataIndex: 'entry-id',
682 width: 50,
683 },
684 {
685 text: gettext("Content"),
686 dataIndex: 'label-text',
687 renderer: (value) => value || '',
688 flex: 1,
689 },
690 {
691 text: gettext('Actions'),
692 xtype: 'actioncolumn',
693 items: [
694 {
695 iconCls: 'fa fa-rotate-270 fa-upload',
696 handler: 'importTape',
697 tooltip: gettext('Import'),
698 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
699 },
700 ],
701 width: 80,
702 },
703 ],
704 },
705 ],
706 },
707 ],
708 },
709 ],
710 });