forked from proxmox-mirrors/proxmox-backup

Use `proxmox-biome format --write`, which is a small wrapper around [Biome] that sets the desired config options for matching the Proxmox style guide as close as possible. Note that the space between function key word and parameter parenthesis for anonymous functions or function names and parameter parenthesis for named functions is rather odd and not per our style guide, but this is not configurable and while it would be relatively simple to change in the formatter code of biome, we would like to avoid doing so for both maintenance reason and as this is seemingly the default in the "web" industry, as prettier–one of the most popular formatters for web projects–uses this and Biome copied the style mostly from there. Signed-off-by: Dominik Csapak <d.csapak@proxmox.com> [TL: add commit message with some background] Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
942 lines
30 KiB
JavaScript
942 lines
30 KiB
JavaScript
Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pbsTapeRestoreWindow',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
title: gettext('Restore Media-Set'),
|
|
|
|
width: 800,
|
|
height: 500,
|
|
|
|
url: '/api2/extjs/tape/restore',
|
|
method: 'POST',
|
|
|
|
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 {};
|
|
},
|
|
|
|
layout: 'fit',
|
|
bodyPadding: 0,
|
|
|
|
viewModel: {
|
|
data: {
|
|
uuid: '',
|
|
singleDatastore: true,
|
|
notificationMode: 'notification-system',
|
|
},
|
|
formulas: {
|
|
singleSelectorLabel: (get) =>
|
|
get('singleDatastore') ? gettext('Target Datastore') : gettext('Default Datastore'),
|
|
singleSelectorEmptyText: (get) =>
|
|
get('singleDatastore') ? '' : Proxmox.Utils.NoneText,
|
|
singleSelectorLabelNs: (get) =>
|
|
get('singleDatastore') ? gettext('Target Namespace') : gettext('Default Namespace'),
|
|
notificationSystemSelected: (get) => get('notificationMode') === 'notification-system',
|
|
},
|
|
},
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (indexOfLastValidTab < indexOfActiveTab) {
|
|
tabpanel.setActiveTab(indexOfLastValidTab);
|
|
} else {
|
|
me.setButtonState(tabpanel.getActiveTab());
|
|
}
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
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);
|
|
},
|
|
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(),
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
updateDatastores: function (grid, values) {
|
|
let me = this;
|
|
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();
|
|
}
|
|
},
|
|
failure: function () {
|
|
// ignore failing api call, maybe catalog is missing
|
|
me.setDataStores([], true);
|
|
},
|
|
});
|
|
},
|
|
|
|
init: function (view) {
|
|
let me = this;
|
|
let vm = me.getViewModel();
|
|
|
|
vm.set('uuid', view.uuid);
|
|
},
|
|
|
|
control: {
|
|
'[isFormField]': {
|
|
change: 'checkValidity',
|
|
validitychange: 'checkValidity',
|
|
},
|
|
tabpanel: {
|
|
tabchange: 'changeButtonVisibility',
|
|
},
|
|
},
|
|
},
|
|
|
|
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,
|
|
},
|
|
],
|
|
|
|
items: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
reference: 'tabpanel',
|
|
layout: 'fit',
|
|
bodyPadding: 10,
|
|
items: [
|
|
{
|
|
title: gettext('Snapshot Selection'),
|
|
xtype: 'inputpanel',
|
|
onGetValues: function (values) {
|
|
let me = this;
|
|
|
|
// cannot use the string serialized one from onGetValues, so gather manually
|
|
delete values.snapshots;
|
|
let snapshots = me.down('pbsTapeSnapshotGrid').getValue();
|
|
|
|
if (snapshots.length > 0 && snapshots[0].indexOf(':') !== -1) {
|
|
values.snapshots = snapshots;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
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 controller = me.up('window').getController();
|
|
let vm = controller.getViewModel();
|
|
let datastores = [];
|
|
if (values.store.toString() !== '') {
|
|
let target = values.store.toString();
|
|
delete values.store;
|
|
|
|
let source = [];
|
|
if (vm.get('singleDatastore')) {
|
|
// can be '[]' (for all), a list of datastores, or a list of snapshots
|
|
source = controller.lookup('snapshotGrid').getValue();
|
|
if (source.length > 0) {
|
|
if (source[0].indexOf(':') !== -1) {
|
|
// one or multiple snapshots are selected
|
|
// extract datastore from first
|
|
source = source[0].split(':')[0];
|
|
} else {
|
|
// one whole datstore is selected
|
|
source = source[0];
|
|
}
|
|
} else {
|
|
// must be [] (all snapshots) so we use it as a default target
|
|
}
|
|
} else {
|
|
// there is more than one datastore to be restored, so this is just
|
|
// the default fallback
|
|
}
|
|
|
|
if (Ext.isString(source)) {
|
|
datastores.push(`${source}=${target}`);
|
|
} else {
|
|
datastores.push(target);
|
|
}
|
|
}
|
|
|
|
let defaultNs = values.defaultNs;
|
|
delete values.defaultNs;
|
|
|
|
// cannot use the string serialized one from onGetValues, so gather manually
|
|
delete values.mapping;
|
|
let [ds_map, ns_map] = me.down('pbsDataStoreMappingField').getValue();
|
|
if (ds_map !== '') {
|
|
datastores.push(ds_map);
|
|
}
|
|
if (ns_map.length > 0) {
|
|
values.namespaces = ns_map;
|
|
}
|
|
|
|
if (
|
|
defaultNs &&
|
|
ns_map.length === 0 &&
|
|
controller.datastores.length === 1
|
|
) {
|
|
// we only have one datastore and a default ns
|
|
values.namespaces = [
|
|
`store=${controller.datastores[0]},target=${defaultNs}`,
|
|
];
|
|
}
|
|
|
|
values.store = datastores.join(',');
|
|
|
|
return values;
|
|
},
|
|
column1: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
comboItems: [
|
|
['legacy-sendmail', gettext('Email (legacy)')],
|
|
['notification-system', gettext('Notification system')],
|
|
],
|
|
fieldLabel: gettext('Notification mode'),
|
|
name: 'notification-mode',
|
|
bind: {
|
|
value: '{notificationMode}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pmxUserSelector',
|
|
name: 'notify-user',
|
|
fieldLabel: gettext('Notify User'),
|
|
emptyText: gettext('Current User'),
|
|
value: null,
|
|
allowBlank: true,
|
|
skipEmptyText: true,
|
|
renderer: Ext.String.htmlEncode,
|
|
bind: {
|
|
disabled: '{notificationSystemSelected}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pbsAuthidSelector',
|
|
name: 'owner',
|
|
fieldLabel: gettext('Owner'),
|
|
emptyText: gettext('Current Auth ID'),
|
|
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').setDefaultStore(value);
|
|
this.up('window').lookup('defaultNs').setDatastore(value);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pbsNamespaceSelector',
|
|
name: 'defaultNs',
|
|
reference: 'defaultNs',
|
|
labelWidth: 120,
|
|
bind: {
|
|
fieldLabel: '{singleSelectorLabelNs}',
|
|
},
|
|
listeners: {
|
|
change: function (field, value) {
|
|
this.up('window').lookup('mappingGrid').setDefaultNs(value);
|
|
},
|
|
},
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Datastore Mapping'),
|
|
labelWidth: 200,
|
|
bind: {
|
|
hidden: '{singleDatastore}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pbsDataStoreMappingField',
|
|
name: 'mapping',
|
|
reference: 'mappingGrid',
|
|
height: 240,
|
|
defaultBindProperty: 'value',
|
|
bind: {
|
|
hidden: '{singleDatastore}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
listeners: {
|
|
afterrender: 'updateSnapshots',
|
|
},
|
|
});
|
|
|
|
Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pbsDataStoreMappingField',
|
|
mixins: ['Ext.form.field.Field'],
|
|
|
|
scrollable: true,
|
|
|
|
getValue: function () {
|
|
let me = this;
|
|
let datastores = [];
|
|
let namespaces = [];
|
|
let defaultStore = me.getViewModel().get('defaultStore');
|
|
let defaultNs = me.getViewModel().get('defaultNs');
|
|
me.getStore().each((rec) => {
|
|
let { source, target, targetns } = rec.data;
|
|
if (target && target !== '') {
|
|
datastores.push(`${source}=${target}`);
|
|
}
|
|
if (target || defaultStore) {
|
|
let ns = targetns || defaultNs;
|
|
if (ns) {
|
|
namespaces.push(`store=${source},target=${ns}`);
|
|
} else {
|
|
namespaces.push(`store=${source}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
return [datastores.join(','), namespaces];
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
defaultStore: '',
|
|
defaultNs: false,
|
|
},
|
|
formulas: {
|
|
emptyStore: (get) => get('defaultStore') || Proxmox.Utils.NoneText,
|
|
emptyNs: (get) => get('defaultNs') || gettext('Root'),
|
|
},
|
|
},
|
|
|
|
setDefaultStore: function (store) {
|
|
let me = this;
|
|
me.getViewModel().set('defaultStore', store);
|
|
me.getStore().each((rec) => {
|
|
if (!rec.dswidget) {
|
|
return; // not yet attached
|
|
}
|
|
if (!rec.dswidget.getValue()) {
|
|
rec.nswidget.setDatastore(store);
|
|
}
|
|
});
|
|
me.checkChange();
|
|
me.validate();
|
|
},
|
|
|
|
setDefaultNs: function (defaultNs) {
|
|
let me = this;
|
|
me.getViewModel().set('defaultNs', defaultNs);
|
|
me.checkChange();
|
|
me.validate();
|
|
},
|
|
|
|
setValue: function (value) {
|
|
let me = this;
|
|
me.setDataStores(value);
|
|
return me;
|
|
},
|
|
|
|
getErrors: function (value) {
|
|
let me = this;
|
|
let error = false;
|
|
|
|
if (!me.getViewModel().get('defaultStore')) {
|
|
error = true;
|
|
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');
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', errorMsg);
|
|
}
|
|
|
|
return [errorMsg];
|
|
}
|
|
me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', '');
|
|
}
|
|
return [];
|
|
},
|
|
|
|
setDataStores: function (datastores) {
|
|
let me = this;
|
|
|
|
let data = [];
|
|
for (const datastore of datastores) {
|
|
data.push({
|
|
source: datastore,
|
|
target: '',
|
|
targetNs: '',
|
|
});
|
|
}
|
|
|
|
me.getStore().setData(data);
|
|
},
|
|
|
|
viewConfig: {
|
|
markDirty: false,
|
|
},
|
|
|
|
store: { data: [] },
|
|
|
|
listeners: {
|
|
beforedestroy: function () {
|
|
// break cyclic reference
|
|
this.getStore()?.each((rec) => {
|
|
delete rec.nswidget;
|
|
delete rec.dswidget;
|
|
});
|
|
},
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
text: gettext('Source Datastore'),
|
|
dataIndex: 'source',
|
|
flex: 1,
|
|
},
|
|
{
|
|
text: gettext('Target Datastore'),
|
|
xtype: 'widgetcolumn',
|
|
onWidgetAttach: function (col, widget, rec) {
|
|
// so that we can access it from the store
|
|
rec.dswidget = widget;
|
|
},
|
|
dataIndex: 'target',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'pbsDataStoreSelector',
|
|
isFormField: false,
|
|
allowBlank: true,
|
|
bind: {
|
|
emptyText: '{emptyStore}',
|
|
},
|
|
listeners: {
|
|
change: function (selector, value) {
|
|
let me = this;
|
|
let rec = me.getWidgetRecord();
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
rec.set('target', value);
|
|
rec.nswidget.setDatastore(value);
|
|
me.up('grid').checkChange();
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Target Namespace'),
|
|
xtype: 'widgetcolumn',
|
|
onWidgetAttach: function (col, widget, rec) {
|
|
// so that we can access it from the store
|
|
rec.nswidget = widget;
|
|
},
|
|
dataIndex: 'targetns',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'pbsNamespaceSelector',
|
|
isFormField: false,
|
|
allowBlank: true,
|
|
bind: {
|
|
emptyText: '{emptyNs}',
|
|
},
|
|
listeners: {
|
|
change: function (selector, value) {
|
|
let me = this;
|
|
let rec = me.getWidgetRecord();
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
rec.set('targetns', value);
|
|
me.up('grid').checkChange();
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
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 selectedStoreCounts = {};
|
|
|
|
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 (selectedStoreCounts[store] === undefined) {
|
|
selectedStoreCounts[store] = 0;
|
|
}
|
|
selectedStoreCounts[store]++;
|
|
}
|
|
});
|
|
|
|
// getSource returns null if data is not filtered
|
|
let originalData = me.store.getData().getSource() || me.store.getData();
|
|
|
|
if (snapshots.length === originalData.length) {
|
|
return [];
|
|
}
|
|
|
|
let wholeStores = [];
|
|
let onlyWholeStoresSelected = true;
|
|
for (const [store, selectedCount] of Object.entries(selectedStoreCounts)) {
|
|
if (me.storeCounts[store] === selectedCount) {
|
|
wholeStores.push(store);
|
|
} else {
|
|
onlyWholeStoresSelected = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (onlyWholeStoresSelected) {
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
});
|