]> git.proxmox.com Git - proxmox-backup.git/blobdiff - www/tape/window/TapeRestore.js
ui: tapeRestore: make window non-resizable
[proxmox-backup.git] / www / tape / window / TapeRestore.js
index a9deb745f23080498b2eb6aedc952acce76dbd52..d4bf43bbccb12e6139db1bf1aa4c7120638d3402 100644 (file)
 Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
-    extend: 'Proxmox.window.Edit',
+    extend: 'Ext.window.Window',
     alias: 'widget.pbsTapeRestoreWindow',
     mixins: ['Proxmox.Mixin.CBind'],
 
+    title: gettext('Restore Media-Set'),
+
     width: 800,
-    title: gettext('Restore Media Set'),
+    height: 500,
+
     url: '/api2/extjs/tape/restore',
     method: 'POST',
-    showTaskViewer: true,
-    isCreate: true,
 
-    defaults: {
-       labelWidth: 120,
+    resizable: false,
+    modal: true,
+
+    mediaset: undefined,
+    prefilter: undefined,
+    uuid: undefined,
+
+    cbindData: function(config) {
+       let me = this;
+       if (me.prefilter !== undefined) {
+           me.title = gettext('Restore Snapshot(s)');
+       }
+       return {};
     },
 
-    referenceHolder: true,
+    layout: 'fit',
+    bodyPadding: 0,
 
-    items: [
-       {
-           xtype: 'inputpanel',
-
-           onGetValues: function(values) {
-               let me = this;
-               let datastores = [];
-               if (values.store && values.store !== "") {
-                   datastores.push(values.store);
-                   delete values.store;
-               }
+    viewModel: {
+       data: {
+           uuid: "",
+           singleDatastore: true,
+       },
+       formulas: {
+           singleSelectorLabel: get =>
+               get('singleDatastore') ? gettext('Target Datastore') : gettext('Default Datastore'),
+           singleSelectorEmptyText: get => get('singleDatastore') ? '' : Proxmox.Utils.NoneText,
+       },
+    },
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       panelIsValid: function(panel) {
+           return panel.query('[isFormField]').every(field => field.isValid());
+       },
+
+       changeMediaSet: function(field, value) {
+           let me = this;
+           let vm = me.getViewModel();
+           vm.set('uuid', value);
+           me.updateSnapshots();
+       },
+
+       checkValidity: function() {
+           let me = this;
 
-               if (values.mapping) {
-                   datastores.push(values.mapping);
-                   delete values.mapping;
+           let tabpanel = me.lookup('tabpanel');
+           if (!tabpanel) {
+               return; // can get triggered early, when the tabpanel is not yet available
+           }
+           let items = tabpanel.items;
+
+           let indexOfActiveTab = items.indexOf(tabpanel.getActiveTab());
+           let indexOfLastValidTab = 0;
+
+           let checkValidity = true;
+           items.each((panel) => {
+               if (checkValidity) {
+                   panel.setDisabled(false);
+                   indexOfLastValidTab = items.indexOf(panel);
+                   if (!me.panelIsValid(panel)) {
+                       checkValidity = false;
+                   }
+               } else {
+                   panel.setDisabled(true);
                }
 
-               values.store = datastores.join(',');
+               return true;
+           });
 
-               return values;
-           },
+           if (indexOfLastValidTab < indexOfActiveTab) {
+               tabpanel.setActiveTab(indexOfLastValidTab);
+           } else {
+               me.setButtonState(tabpanel.getActiveTab());
+           }
+       },
 
-           column1: [
-               {
-                   xtype: 'displayfield',
-                   fieldLabel: gettext('Media Set'),
-                   cbind: {
-                       value: '{mediaset}',
-                   },
-               },
-               {
-                   xtype: 'displayfield',
-                   fieldLabel: gettext('Media Set UUID'),
-                   name: 'media-set',
-                   submitValue: true,
-                   cbind: {
-                       value: '{uuid}',
-                   },
-               },
-               {
-                   xtype: 'pbsDriveSelector',
-                   fieldLabel: gettext('Drive'),
-                   name: 'drive',
-               },
-           ],
+       setButtonState: function(panel) {
+           let me = this;
+           let isValid = me.panelIsValid(panel);
+           let nextButton = me.lookup('nextButton');
+           let finishButton = me.lookup('finishButton');
+           nextButton.setDisabled(!isValid);
+           finishButton.setDisabled(!isValid);
+       },
 
-           column2: [
-               {
-                   xtype: 'pbsUserSelector',
-                   name: 'notify-user',
-                   fieldLabel: gettext('Notify User'),
-                   emptyText: gettext('Current User'),
-                   value: null,
-                   allowBlank: true,
-                   skipEmptyText: true,
-                   renderer: Ext.String.htmlEncode,
-               },
-               {
-                   xtype: 'pbsUserSelector',
-                   name: 'owner',
-                   fieldLabel: gettext('Owner'),
-                   emptyText: gettext('Current User'),
-                   value: null,
-                   allowBlank: true,
-                   skipEmptyText: true,
-                   renderer: Ext.String.htmlEncode,
+       changeButtonVisibility: function(tabpanel, newItem) {
+           let me = this;
+           let items = tabpanel.items;
+
+           let backButton = me.lookup('backButton');
+           let nextButton = me.lookup('nextButton');
+           let finishButton = me.lookup('finishButton');
+
+           let isLast = items.last() === newItem;
+           let isFirst = items.first() === newItem;
+
+           backButton.setVisible(!isFirst);
+           nextButton.setVisible(!isLast);
+           finishButton.setVisible(isLast);
+
+           me.setButtonState(newItem);
+       },
+
+       previousTab: function() {
+           let me = this;
+           let tabpanel = me.lookup('tabpanel');
+           let index = tabpanel.items.indexOf(tabpanel.getActiveTab());
+           tabpanel.setActiveTab(index - 1);
+       },
+
+       nextTab: function() {
+           let me = this;
+           let tabpanel = me.lookup('tabpanel');
+           let index = tabpanel.items.indexOf(tabpanel.getActiveTab());
+           tabpanel.setActiveTab(index + 1);
+       },
+
+       getValues: function() {
+           let me = this;
+
+           let values = {};
+
+           let tabpanel = me.lookup('tabpanel');
+           tabpanel
+               .query('inputpanel')
+               .forEach((panel) =>
+                   Proxmox.Utils.assemble_field_data(values, panel.getValues()));
+
+           return values;
+       },
+
+       finish: function() {
+           let me = this;
+           let view = me.getView();
+
+           let values = me.getValues();
+           let url = view.url;
+           let method = view.method;
+
+           Proxmox.Utils.API2Request({
+               url,
+               waitMsgTarget: view,
+               method,
+               params: values,
+               failure: function(response, options) {
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
                },
-               {
-                   xtype: 'pbsDataStoreSelector',
-                   fieldLabel: gettext('Datastore'),
-                   reference: 'defaultDatastore',
-                   name: 'store',
-                   listeners: {
-                       change: function(field, value) {
-                           let me = this;
-                           let grid = me.up('window').lookup('mappingGrid');
-                           grid.setNeedStores(!value);
-                       },
-                   },
+               success: function(response, options) {
+                       // keep around so we can trigger our close events when background action completes
+                       view.hide();
+
+                       Ext.create('Proxmox.window.TaskViewer', {
+                           autoShow: true,
+                           upid: response.result.data,
+                           listeners: {
+                               destroy: () => view.close(),
+                           },
+                       });
                },
-           ],
+           });
+       },
 
-           columnB: [
-               {
-                   fieldLabel: gettext('Datastore Mapping'),
-                   labelWidth: 200,
-                   hidden: true,
-                   reference: 'mappingLabel',
-                   xtype: 'displayfield',
+       updateDatastores: function(grid, values) {
+           let me = this;
+           if (values === 'all') {
+               values = [];
+           }
+           let datastores = {};
+           values.forEach((snapshotOrDatastore) => {
+               let datastore = snapshotOrDatastore;
+               if (snapshotOrDatastore.indexOf(':') !== -1) {
+                   let snapshot = snapshotOrDatastore;
+                   let match = snapshot.split(':');
+                   datastore = match[0];
+               } datastores[datastore] = true;
+           });
+
+           me.setDataStores(Object.keys(datastores));
+       },
+
+       setDataStores: function(datastores, initial) {
+           let me = this;
+
+           // save all datastores on the first setting, and restore them if we selected all
+           if (initial) {
+               me.datastores = datastores;
+           } else if (datastores.length === 0) {
+               datastores = me.datastores;
+           }
+
+           const singleDatastore = !datastores || datastores.length <= 1;
+           me.getViewModel().set('singleDatastore', singleDatastore);
+
+           let grid = me.lookup('mappingGrid');
+           if (!singleDatastore && grid) {
+               grid.setDataStores(datastores);
+           }
+       },
+
+       updateSnapshots: function() {
+           let me = this;
+           let view = me.getView();
+           let grid = me.lookup('snapshotGrid');
+           let vm = me.getViewModel();
+           let uuid = vm.get('uuid');
+
+           Proxmox.Utils.API2Request({
+               waitMsgTarget: view,
+               url: `/tape/media/content?media-set=${uuid}`,
+               success: function(response, opt) {
+                   let datastores = {};
+                   for (const content of response.result.data) {
+                       datastores[content.store] = true;
+                   }
+                   me.setDataStores(Object.keys(datastores), true);
+                   if (response.result.data.length > 0) {
+                       grid.setDisabled(false);
+                       grid.setData(response.result.data);
+                       grid.getSelectionModel().selectAll();
+                       // we've shown a big list, center the window again
+                       view.center();
+                   }
                },
-               {
-                   xtype: 'pbsDataStoreMappingField',
-                   reference: 'mappingGrid',
-                   name: 'mapping',
-                   defaultBindProperty: 'value',
-                   hidden: true,
+               failure: function() {
+                   // ignore failing api call, maybe catalog is missing
+                   me.setDataStores([], true);
                },
-           ],
+           });
        },
-    ],
-
-    setDataStores: function(datastores) {
-       let me = this;
 
-       let label = me.lookup('mappingLabel');
-       let grid = me.lookup('mappingGrid');
-       let defaultField = me.lookup('defaultDatastore');
-
-       if (!datastores || datastores.length <= 1) {
-           label.setVisible(false);
-           grid.setVisible(false);
-           defaultField.setFieldLabel(gettext('Datastore'));
-           defaultField.setAllowBlank(false);
-           defaultField.setEmptyText("");
-           return;
-       }
+       init: function(view) {
+           let me = this;
+           let vm = me.getViewModel();
 
-       label.setVisible(true);
-       defaultField.setFieldLabel(gettext('Default Datastore'));
-       defaultField.setAllowBlank(true);
-       defaultField.setEmptyText(Proxmox.Utils.NoneText);
+           vm.set('uuid', view.uuid);
+       },
 
-       grid.setDataStores(datastores);
-       grid.setVisible(true);
+       control: {
+           '[isFormField]': {
+               change: 'checkValidity',
+               validitychange: 'checkValidity',
+           },
+           'tabpanel': {
+               tabchange: 'changeButtonVisibility',
+           },
+       },
     },
 
-    initComponent: function() {
-       let me = this;
+    buttons: [
+       {
+           text: gettext('Back'),
+           reference: 'backButton',
+           handler: 'previousTab',
+           hidden: true,
+       },
+       {
+           text: gettext('Next'),
+           reference: 'nextButton',
+           handler: 'nextTab',
+       },
+       {
+           text: gettext('Restore'),
+           reference: 'finishButton',
+           handler: 'finish',
+           hidden: true,
+       },
+    ],
 
-       me.callParent();
-       if (me.datastores) {
-           me.setDataStores(me.datastores);
-       } else {
-           // use timeout so that the window is rendered already
-           // for correct masking
-           setTimeout(function() {
-               Proxmox.Utils.API2Request({
-                   waitMsgTarget: me,
-                   url: `/tape/media/content?media-set=${me.uuid}`,
-                   success: function(response, opt) {
-                       let datastores = {};
-                       for (const content of response.result.data) {
-                           datastores[content.store] = true;
+    items: [
+       {
+           xtype: 'tabpanel',
+           reference: 'tabpanel',
+           layout: 'fit',
+           bodyPadding: 10,
+           items: [
+               {
+                   title: gettext('Snapshot Selection'),
+                   xtype: 'inputpanel',
+                   onGetValues: function(values) {
+                       let me = this;
+
+                       if (values !== "all" &&
+                           Ext.isString(values.snapshots) &&
+                           values.snapshots &&
+                           values.snapshots.indexOf(':') !== -1
+                       ) {
+                           values.snapshots = values.snapshots.split(',');
+                       } else {
+                           delete values.snapshots;
                        }
-                       me.setDataStores(Object.keys(datastores));
+
+                       return values;
                    },
-                   failure: function() {
-                       // ignore failing api call, maybe catalog is missing
-                       me.setDataStores();
+
+                   column1: [
+                       {
+                           xtype: 'pbsMediaSetSelector',
+                           fieldLabel: gettext('Media-Set'),
+                           width: 350,
+                           submitValue: false,
+                           emptyText: gettext('Select Media-Set to restore'),
+                           bind: {
+                               value: '{uuid}',
+                           },
+                           cbind: {
+                               hidden: '{uuid}',
+                               disabled: '{uuid}',
+                           },
+                           listeners: {
+                               change: 'changeMediaSet',
+                           },
+                       },
+                       {
+                           xtype: 'displayfield',
+                           fieldLabel: gettext('Media-Set'),
+                           cbind: {
+                               value: '{mediaset}',
+                               hidden: '{!uuid}',
+                               disabled: '{!uuid}',
+                           },
+                       },
+                   ],
+
+                   column2: [
+                       {
+                           xtype: 'displayfield',
+                           fieldLabel: gettext('Media-Set UUID'),
+                           name: 'media-set',
+                           submitValue: true,
+                           bind: {
+                               value: '{uuid}',
+                               hidden: '{!uuid}',
+                               disabled: '{!uuid}',
+                           },
+                       },
+                   ],
+
+                   columnB: [
+                       {
+                           xtype: 'pbsTapeSnapshotGrid',
+                           reference: 'snapshotGrid',
+                           name: 'snapshots',
+                           height: 322,
+                           disabled: true, // will be shown/enabled on successful load
+                           listeners: {
+                               change: 'updateDatastores',
+                           },
+                           cbind: {
+                               prefilter: '{prefilter}',
+                           },
+                       },
+                   ],
+               },
+               {
+                   title: gettext('Target'),
+                   xtype: 'inputpanel',
+                   onGetValues: function(values) {
+                       let me = this;
+                       let datastores = [];
+                       if (values.store.toString() !== "") {
+                           datastores.push(values.store);
+                           delete values.store;
+                       }
+
+                       if (values.mapping.toString() !== "") {
+                           datastores.push(values.mapping);
+                       }
+                       delete values.mapping;
+
+                       values.store = datastores.join(',');
+
+                       return values;
                    },
-               });
-           }, 10);
-       }
+                   column1: [
+                       {
+                           xtype: 'pbsUserSelector',
+                           name: 'notify-user',
+                           fieldLabel: gettext('Notify User'),
+                           emptyText: gettext('Current User'),
+                           value: null,
+                           allowBlank: true,
+                           skipEmptyText: true,
+                           renderer: Ext.String.htmlEncode,
+                       },
+                       {
+                           xtype: 'pbsUserSelector',
+                           name: 'owner',
+                           fieldLabel: gettext('Owner'),
+                           emptyText: gettext('Current User'),
+                           value: null,
+                           allowBlank: true,
+                           skipEmptyText: true,
+                           renderer: Ext.String.htmlEncode,
+                       },
+                   ],
+
+                   column2: [
+                       {
+                           xtype: 'pbsDriveSelector',
+                           name: 'drive',
+                           fieldLabel: gettext('Drive'),
+                           labelWidth: 120,
+                       },
+                       {
+                           xtype: 'pbsDataStoreSelector',
+                           name: 'store',
+                           labelWidth: 120,
+                           bind: {
+                               fieldLabel: '{singleSelectorLabel}',
+                               emptyText: '{singleSelectorEmptyText}',
+                               allowBlank: '{!singleDatastore}',
+                           },
+                           listeners: {
+                               change: function(field, value) {
+                                   this.up('window').lookup('mappingGrid').setNeedStores(!value);
+                               },
+                           },
+                       },
+                   ],
+
+                   columnB: [
+                       {
+                           xtype: 'displayfield',
+                           fieldLabel: gettext('Datastore Mapping'),
+                           labelWidth: 200,
+                           bind: {
+                               hidden: '{singleDatastore}',
+                           },
+                       },
+                       {
+                           xtype: 'pbsDataStoreMappingField',
+                           name: 'mapping',
+                           reference: 'mappingGrid',
+                           height: 260,
+                           defaultBindProperty: 'value',
+                           bind: {
+                               hidden: '{singleDatastore}',
+                           },
+                       },
+                   ],
+               },
+           ],
+       },
+    ],
+
+    listeners: {
+       afterrender: 'updateSnapshots',
     },
 });
 
@@ -177,12 +468,13 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
     alias: 'widget.pbsDataStoreMappingField',
     mixins: ['Ext.form.field.Field'],
 
+    scrollable: true,
+
     getValue: function() {
        let me = this;
        let datastores = [];
-       me.getStore().each((rec) => {
-           let source = rec.data.source;
-           let target = rec.data.target;
+       me.getStore().each(rec => {
+           let { source, target } = rec.data;
            if (target && target !== "") {
                datastores.push(`${source}=${target}`);
            }
@@ -191,12 +483,18 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
        return datastores.join(',');
     },
 
-    // this determines if we need at least one valid mapping
-    needStores: false,
+    viewModel: {
+       data: {
+           needStores: false, // this determines if we need at least one valid mapping
+       },
+       formulas: {
+           emptyMeans: get => get('needStores') ? Proxmox.Utils.NoneText : Proxmox.Utils.defaultText,
+       },
+    },
 
     setNeedStores: function(needStores) {
        let me = this;
-       me.needStores = needStores;
+       me.getViewModel().set('needStores', needStores);
        me.checkChange();
        me.validate();
     },
@@ -211,32 +509,36 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
        let me = this;
        let error = false;
 
-       if (me.needStores) {
+       if (me.getViewModel().get('needStores')) {
            error = true;
-           me.getStore().each((rec) => {
+           me.getStore().each(rec => {
                if (rec.data.target) {
                    error = false;
                }
            });
        }
 
+       let el = me.getActionEl();
        if (error) {
            me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
            let errorMsg = gettext("Need at least one mapping");
-           me.getActionEl().dom.setAttribute('data-errorqtip', errorMsg);
+           if (el) {
+               el.dom.setAttribute('data-errorqtip', errorMsg);
+           }
 
            return [errorMsg];
        }
        me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
-       me.getActionEl().dom.setAttribute('data-errorqtip', "");
+       if (el) {
+           el.dom.setAttribute('data-errorqtip', "");
+       }
        return [];
     },
 
     setDataStores: function(datastores) {
        let me = this;
-       let store = me.getStore();
-       let data = [];
 
+       let data = [];
        for (const datastore of datastores) {
            data.push({
                source: datastore,
@@ -244,7 +546,7 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
            });
        }
 
-       store.setData(data);
+       me.getStore().setData(data);
     },
 
     viewConfig: {
@@ -266,8 +568,11 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
            flex: 1,
            widget: {
                xtype: 'pbsDataStoreSelector',
+               isFormField: false,
                allowBlank: true,
-               emptyText: Proxmox.Utils.NoneText,
+               bind: {
+                   emptyText: '{emptyMeans}',
+               },
                listeners: {
                    change: function(selector, value) {
                        let me = this;
@@ -283,3 +588,210 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
        },
     ],
 });
+
+Ext.define('PBS.TapeManagement.SnapshotGrid', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pbsTapeSnapshotGrid',
+    mixins: ['Ext.form.field.Field'],
+
+    getValue: function() {
+       let me = this;
+       let snapshots = [];
+
+       let storeCounts = {};
+
+       me.getSelection().forEach((rec) => {
+           let id = rec.get('id');
+           let store = rec.data.store;
+           let snap = rec.data.snapshot;
+           // only add if not filtered
+           if (me.store.findExact('id', id) !== -1) {
+               snapshots.push(`${store}:${snap}`);
+               if (storeCounts[store] === undefined) {
+                   storeCounts[store] = 0;
+               }
+               storeCounts[store]++;
+           }
+       });
+
+       // getSource returns null if data is not filtered
+       let originalData = me.store.getData().getSource() || me.store.getData();
+
+       if (snapshots.length === originalData.length) {
+           return "all";
+       }
+
+       let wholeStores = [];
+       let wholeStoresSelected = true;
+       for (const [store, count] of Object.entries(storeCounts)) {
+           if (me.storeCounts[store] === count) {
+               wholeStores.push(store);
+           } else {
+               wholeStoresSelected = false;
+               break;
+           }
+       }
+
+       if (wholeStoresSelected) {
+           return wholeStores;
+       }
+
+       return snapshots;
+    },
+
+    setValue: function(value) {
+       let me = this;
+       // not implemented
+       return me;
+    },
+
+    getErrors: function(value) {
+       let me = this;
+       if (me.getSelection().length < 1) {
+           me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+           let errorMsg = gettext("Need at least one snapshot");
+           let el = me.getActionEl();
+           if (el) {
+               el.dom.setAttribute('data-errorqtip', errorMsg);
+           }
+
+           return [errorMsg];
+       }
+       me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+       let el = me.getActionEl();
+       if (el) {
+           el.dom.setAttribute('data-errorqtip', "");
+       }
+       return [];
+    },
+
+    setData: function(records) {
+       let me = this;
+       let storeCounts = {};
+       records.forEach((rec) => {
+           let store = rec.store;
+           if (storeCounts[store] === undefined) {
+               storeCounts[store] = 0;
+           }
+           storeCounts[store]++;
+       });
+       me.storeCounts = storeCounts;
+       me.getStore().setData(records);
+    },
+
+    scrollable: true,
+    plugins: 'gridfilters',
+
+    viewConfig: {
+       emptyText: gettext('No Snapshots'),
+       markDirty: false,
+    },
+
+    selModel: 'checkboxmodel',
+    store: {
+       sorters: ['store', 'snapshot'],
+       data: [],
+       filters: [],
+    },
+
+    listeners: {
+       selectionchange: function() {
+           // to trigger validity and error checks
+           this.checkChange();
+       },
+    },
+
+    checkChangeEvents: [
+       'selectionchange',
+       'change',
+    ],
+
+    columns: [
+       {
+           text: gettext('Source Datastore'),
+           dataIndex: 'store',
+           filter: {
+               type: 'list',
+           },
+           flex: 1,
+       },
+       {
+           text: gettext('Snapshot'),
+           dataIndex: 'snapshot',
+           filter: {
+               type: 'string',
+           },
+           flex: 2,
+       },
+    ],
+
+    initComponent: function() {
+       let me = this;
+       me.callParent();
+       if (me.prefilter !== undefined) {
+           if (me.prefilter.store !== undefined) {
+               me.store.filters.add(
+                   {
+                       id: 'x-gridfilter-store',
+                       property: 'store',
+                       operator: 'in',
+                       value: [me.prefilter.store],
+                   },
+               );
+           }
+
+           if (me.prefilter.snapshot !== undefined) {
+               me.store.filters.add(
+                   {
+                       id: 'x-gridfilter-snapshot',
+                       property: 'snapshot',
+                       value: me.prefilter.snapshot,
+                   },
+               );
+           }
+       }
+
+       me.mon(me.store, 'filterchange', () => me.checkChange());
+    },
+});
+
+Ext.define('PBS.TapeManagement.MediaSetSelector', {
+    extend: 'Proxmox.form.ComboGrid',
+    alias: 'widget.pbsMediaSetSelector',
+
+    allowBlank: false,
+    displayField: 'media-set-name',
+    valueField: 'media-set-uuid',
+    autoSelect: false,
+
+    store: {
+       proxy: {
+           type: 'proxmox',
+           url: '/api2/json/tape/media/media-sets',
+       },
+       autoLoad: true,
+       idProperty: 'media-set-uuid',
+       sorters: ['pool', 'media-set-ctime'],
+    },
+
+    listConfig: {
+       width: 600,
+       columns: [
+           {
+               text: gettext('Pool'),
+               dataIndex: 'pool',
+               flex: 1,
+           },
+           {
+               text: gettext('Name'),
+               dataIndex: 'media-set-name',
+               width: 180,
+           },
+           {
+               text: gettext('Media-Set UUID'),
+               dataIndex: 'media-set-uuid',
+               width: 280,
+           },
+       ],
+    },
+});