From fd9aa8dfa287c7023912aa6b7a485fc0bce1343c Mon Sep 17 00:00:00 2001 From: Dominik Csapak Date: Thu, 28 Jan 2021 12:59:48 +0100 Subject: [PATCH] ui: tape: add ChangerStatus panel this lets the users manage changers and lets them view the status of one by having an overview of: * slots for tapes * import/export slots * drives lets the user: * barcode-label all the tapes in the library * move tapes between slots, into/out of drives * show some basic info when a tape is loaded into a drive * show the status of a drive * clean a drive Signed-off-by: Dominik Csapak --- www/Makefile | 1 + www/tape/ChangerStatus.js | 631 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 632 insertions(+) create mode 100644 www/tape/ChangerStatus.js diff --git a/www/Makefile b/www/Makefile index aad5f7ce..db486f71 100644 --- a/www/Makefile +++ b/www/Makefile @@ -21,6 +21,7 @@ TAPE_UI_FILES= \ tape/window/PoolEdit.js \ tape/window/TapeBackup.js \ tape/BackupOverview.js \ + tape/ChangerStatus.js \ TapeManagement.js endif diff --git a/www/tape/ChangerStatus.js b/www/tape/ChangerStatus.js new file mode 100644 index 00000000..75af75ce --- /dev/null +++ b/www/tape/ChangerStatus.js @@ -0,0 +1,631 @@ +Ext.define('PBS.TapeManagement.ChangerStatus', { + extend: 'Ext.panel.Panel', + alias: 'widget.pbsChangerStatus', + + viewModel: { + data: { + changer: '', + }, + + formulas: { + changerSelected: (get) => get('changer') !== '', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + changerChange: function(field, value) { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + vm.set('changer', value); + if (view.rendered) { + me.reload(); + } + }, + + onAdd: function() { + let me = this; + Ext.create('PBS.TapeManagement.ChangerEditWindow', { + listeners: { + destroy: function() { + me.reloadList(); + }, + }, + }).show(); + }, + + onEdit: function() { + let me = this; + let vm = me.getViewModel(); + let changerid = vm.get('changer'); + Ext.create('PBS.TapeManagement.ChangerEditWindow', { + changerid, + autoLoad: true, + listeners: { + destroy: () => me.reload(), + }, + }).show(); + }, + + slotTransfer: function(view, rI, cI, button, el, record) { + let me = this; + let vm = me.getViewModel(); + let from = record.data['entry-id']; + let changer = encodeURIComponent(vm.get('changer')); + Ext.create('Proxmox.window.Edit', { + title: gettext('Transfer'), + isCreate: true, + submitText: gettext('OK'), + method: 'POST', + url: `/api2/extjs/tape/changer/${changer}/transfer`, + items: [ + { + xtype: 'displayfield', + name: 'from', + value: from, + submitValue: true, + fieldLabel: gettext('From Slot'), + }, + { + xtype: 'proxmoxintegerfield', + name: 'to', + fieldLabel: gettext('To Slot'), + }, + ], + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + load: function(view, rI, cI, button, el, record) { + let me = this; + let vm = me.getViewModel(); + let label = record.data['label-text']; + + let changer = vm.get('changer'); + + Ext.create('Proxmox.window.Edit', { + isCreate: true, + submitText: gettext('OK'), + title: gettext('Load Media into Drive'), + url: `/api2/extjs/tape/drive`, + submitUrl: function(url, values) { + let drive = values.drive; + delete values.drive; + return `${url}/${encodeURIComponent(drive)}/load-media`; + }, + items: [ + { + xtype: 'displayfield', + name: 'label-text', + value: label, + submitValue: true, + fieldLabel: gettext('Media'), + }, + { + xtype: 'pbsDriveSelector', + fieldLabel: gettext('Drive'), + changer: changer, + name: 'drive', + }, + ], + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + unload: async function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + Proxmox.Utils.setErrorMask(view, true); + try { + await PBS.Async.api2({ + method: 'PUT', + url: `/api2/extjs/tape/drive/${encodeURIComponent(drive)}/unload`, + }); + Proxmox.Utils.setErrorMask(view); + me.reload(); + } catch (error) { + Ext.Msg.alert(gettext('Error'), error); + Proxmox.Utils.setErrorMask(view); + me.reload(); + } + }, + + driveCommand: function(driveid, command, callback, params, method) { + let me = this; + let view = me.getView(); + params = params || {}; + method = method || 'GET'; + Proxmox.Utils.API2Request({ + url: `/api2/extjs/tape/drive/${driveid}/${command}`, + method, + waitMsgTarget: view, + params, + success: function(response) { + callback(response); + }, + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + + cartridgeMemory: function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + me.driveCommand(drive, 'cartridge-memory', function(response) { + Ext.create('Ext.window.Window', { + title: gettext('Cartridge Memory'), + modal: true, + width: 600, + height: 450, + layout: 'fit', + scrollable: true, + items: [ + { + xtype: 'grid', + store: { + data: response.result.data, + }, + columns: [ + { + text: gettext('ID'), + dataIndex: 'id', + width: 60, + }, + { + text: gettext('Name'), + dataIndex: 'name', + flex: 2, + }, + { + text: gettext('Value'), + dataIndex: 'value', + flex: 1, + }, + ], + }, + ], + }).show(); + }); + }, + + cleanDrive: function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + me.driveCommand(drive, 'clean', function(response) { + Ext.create('Proxmox.window.TaskProgress', { + upid: response.result.data, + taskDone: function() { + me.reload(); + }, + }).show(); + }, {}, 'PUT'); + }, + + volumeStatistics: function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + me.driveCommand(drive, 'volume-statistics', function(response) { + Ext.create('Ext.window.Window', { + title: gettext('Volume Statistics'), + modal: true, + width: 600, + height: 450, + layout: 'fit', + scrollable: true, + items: [ + { + xtype: 'grid', + store: { + data: response.result.data, + }, + columns: [ + { + text: gettext('ID'), + dataIndex: 'id', + width: 60, + }, + { + text: gettext('Name'), + dataIndex: 'name', + flex: 2, + }, + { + text: gettext('Value'), + dataIndex: 'value', + flex: 1, + }, + ], + }, + ], + }).show(); + }); + }, + + readLabel: function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + me.driveCommand(drive, 'read-label', function(response) { + let lines = []; + for (const [key, val] of Object.entries(response.result.data)) { + lines.push(`${key}: ${val}`); + } + + let txt = lines.join('
'); + + Ext.Msg.show({ + title: gettext('Label Information'), + message: txt, + icon: undefined, + }); + }); + }, + + status: function(view, rI, cI, button, el, record) { + let me = this; + let drive = record.data.name; + me.driveCommand(drive, 'status', function(response) { + let lines = []; + for (const [key, val] of Object.entries(response.result.data)) { + lines.push(`${key}: ${val}`); + } + + let txt = lines.join('
'); + + Ext.Msg.show({ + title: gettext('Label Information'), + message: txt, + icon: undefined, + }); + }); + }, + + reloadList: function() { + let me = this; + me.lookup('changerselector').getStore().load(); + }, + + barcodeLabel: function() { + let me = this; + let vm = me.getViewModel(); + let changer = vm.get('changer'); + if (changer === '') { + return; + } + + Ext.create('Proxmox.window.Edit', { + title: gettext('Barcode Label'), + showTaskViewer: true, + url: '/api2/extjs/tape/drive', + submitUrl: function(url, values) { + let drive = values.drive; + delete values.drive; + return `${url}/${encodeURIComponent(drive)}/barcode-label-media`; + }, + + items: [ + { + xtype: 'pbsDriveSelector', + fieldLabel: gettext('Drive'), + name: 'drive', + changer: changer, + }, + { + xtype: 'pbsMediaPoolSelector', + fieldLabel: gettext('Pool'), + name: 'pool', + skipEmptyText: true, + allowBlank: true, + }, + ], + }).show(); + }, + + reload: async function() { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let changer = vm.get('changer'); + if (changer === '') { + return; + } + + try { + Proxmox.Utils.setErrorMask(view, true); + Proxmox.Utils.setErrorMask(me.lookup('content')); + let status = await PBS.Async.api2({ + url: `/api2/extjs/tape/changer/${encodeURIComponent(changer)}/status`, + }); + let drives = await PBS.Async.api2({ + url: `/api2/extjs/tape/drive?changer=${encodeURIComponent(changer)}`, + }); + + let data = { + slot: [], + 'import-export': [], + drive: [], + }; + + let drive_entries = {}; + + for (const entry of drives.result.data) { + drive_entries[entry['changer-drivenum'] || 0] = entry; + } + + for (let entry of status.result.data) { + let type = entry['entry-kind']; + + if (type === 'drive' && drive_entries[entry['entry-id']] !== undefined) { + entry = Ext.applyIf(entry, drive_entries[entry['entry-id']]); + } + + data[type].push(entry); + } + + + me.lookup('slots').getStore().setData(data.slot); + me.lookup('import_export').getStore().setData(data['import-export']); + me.lookup('drives').getStore().setData(data.drive); + + Proxmox.Utils.setErrorMask(view); + } catch (err) { + Proxmox.Utils.setErrorMask(view); + Proxmox.Utils.setErrorMask(me.lookup('content'), err); + } + }, + }, + + listeners: { + activate: 'reload', + }, + + tbar: [ + { + fieldLabel: gettext('Changer'), + xtype: 'pbsChangerSelector', + reference: 'changerselector', + autoSelect: true, + listeners: { + change: 'changerChange', + }, + }, + '-', + { + text: gettext('Reload'), + xtype: 'proxmoxButton', + handler: 'reload', + selModel: false, + }, + '-', + { + text: gettext('Add'), + xtype: 'proxmoxButton', + handler: 'onAdd', + selModel: false, + }, + { + text: gettext('Edit'), + xtype: 'proxmoxButton', + handler: 'onEdit', + bind: { + disabled: '{!changerSelected}', + }, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/api2/extjs/config/changer', + callback: 'reloadList', + selModel: false, + getRecordName: function() { + let me = this; + let vm = me.up('panel').getViewModel(); + return vm.get('changer'); + }, + getUrl: function() { + let me = this; + let vm = me.up('panel').getViewModel(); + return `/api2/extjs/config/changer/${vm.get('changer')}`; + }, + bind: { + disabled: '{!changerSelected}', + }, + }, + '-', + { + text: gettext('Barcode Label'), + xtype: 'proxmoxButton', + handler: 'barcodeLabel', + iconCls: 'fa fa-barcode', + bind: { + disabled: '{!changerSelected}', + }, + }, + ], + + layout: 'auto', + bodyPadding: 5, + scrollable: true, + + items: [ + { + xtype: 'container', + reference: 'content', + layout: { + type: 'hbox', + aling: 'stretch', + }, + items: [ + { + xtype: 'grid', + reference: 'slots', + title: gettext('Slots'), + padding: 5, + flex: 1, + store: { + data: [], + }, + columns: [ + { + text: gettext('Slot'), + dataIndex: 'entry-id', + width: 50, + }, + { + text: gettext("Content"), + dataIndex: 'label-text', + flex: 1, + renderer: (value) => value || '', + }, + { + text: gettext('Actions'), + xtype: 'actioncolumn', + width: 100, + items: [ + { + iconCls: 'fa fa-rotate-90 fa-exchange', + handler: 'slotTransfer', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + { + iconCls: 'fa fa-rotate-90 fa-upload', + handler: 'load', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + ], + }, + ], + }, + { + xtype: 'container', + flex: 2, + defaults: { + padding: 5, + }, + items: [ + { + xtype: 'grid', + reference: 'drives', + title: gettext('Drives'), + store: { + fields: ['entry-id', 'label-text', 'model', 'name', 'vendor', 'serial'], + data: [], + }, + columns: [ + { + text: gettext('Slot'), + dataIndex: 'entry-id', + width: 50, + }, + { + text: gettext("Content"), + dataIndex: 'label-text', + flex: 1, + renderer: (value) => value || '', + }, + { + text: gettext("Name"), + sortable: true, + dataIndex: 'name', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + text: gettext("Vendor"), + sortable: true, + dataIndex: 'vendor', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + text: gettext("Model"), + sortable: true, + dataIndex: 'model', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + text: gettext("Serial"), + sortable: true, + dataIndex: 'serial', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + xtype: 'actioncolumn', + text: gettext('Actions'), + width: 140, + items: [ + { + iconCls: 'fa fa-rotate-270 fa-upload', + handler: 'unload', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + { + iconCls: 'fa fa-hdd-o', + handler: 'cartridgeMemory', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + { + iconCls: 'fa fa-line-chart', + handler: 'volumeStatistics', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + { + iconCls: 'fa fa-tag', + handler: 'readLabel', + isDisabled: (v, r, c, i, rec) => !rec.data['label-text'], + }, + { + iconCls: 'fa fa-info-circle', + handler: 'status', + }, + { + iconCls: 'fa fa-shower', + handler: 'cleanDrive', + }, + ], + }, + ], + }, + { + xtype: 'grid', + reference: 'import_export', + store: { + data: [], + }, + title: gettext('Import-Export'), + columns: [ + { + text: gettext('Slot'), + dataIndex: 'entry-id', + width: 50, + }, + { + text: gettext("Content"), + dataIndex: 'label-text', + renderer: (value) => value || '', + flex: 1, + }, + { + text: gettext('Actions'), + items: [], + width: 80, + }, + ], + }, + ], + }, + ], + }, + ], +});