The names of notification matchers and targets have to be safe IDs, but only the name of webhook targets was checked in the UI, and that without saying what is wrong. For all others, an invalid name, for example one containing a space, was only rejected by the API with a 'parameter verification errors (400)' message, without saying which characters are allowed. Add a 'SafeId' vtype, which uses the existing safe ID regex and says which characters are allowed, and use it for the names of matchers and all target types. Signed-off-by: Michal Fox <me@dualfroz.com>
931 lines
29 KiB
JavaScript
931 lines
29 KiB
JavaScript
// ExtJS related things
|
|
|
|
// do not send '_dc' parameter
|
|
Ext.Ajax.disableCaching = false;
|
|
|
|
// custom Vtypes
|
|
Ext.apply(Ext.form.field.VTypes, {
|
|
IPAddress: function (v) {
|
|
return Proxmox.Utils.IP4_match.test(v);
|
|
},
|
|
IPAddressText: gettext('Example') + ': 192.168.1.1',
|
|
IPAddressMask: /[\d.]/i,
|
|
|
|
IPCIDRAddress: function (v) {
|
|
let result = Proxmox.Utils.IP4_cidr_match.exec(v);
|
|
// limits according to JSON Schema see
|
|
// pve-common/src/PVE/JSONSchema.pm
|
|
return result !== null && result[1] >= 8 && result[1] <= 32;
|
|
},
|
|
IPCIDRAddressText:
|
|
gettext('Example') + ': 192.168.1.1/24<br>' + gettext('Valid CIDR Range') + ': 8-32',
|
|
IPCIDRAddressMask: /[\d./]/i,
|
|
|
|
IP6Address: function (v) {
|
|
return Proxmox.Utils.IP6_match.test(v);
|
|
},
|
|
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
|
|
IP6AddressMask: /[A-Fa-f0-9:]/,
|
|
|
|
IP6CIDRAddress: function (v) {
|
|
let result = Proxmox.Utils.IP6_cidr_match.exec(v);
|
|
// limits according to JSON Schema see
|
|
// pve-common/src/PVE/JSONSchema.pm
|
|
return result !== null && result[1] >= 8 && result[1] <= 128;
|
|
},
|
|
IP6CIDRAddressText:
|
|
gettext('Example') + ': 2001:DB8::42/64<br>' + gettext('Valid CIDR Range') + ': 8-128',
|
|
IP6CIDRAddressMask: /[A-Fa-f0-9:/]/,
|
|
|
|
IP6PrefixLength: function (v) {
|
|
return v >= 0 && v <= 128;
|
|
},
|
|
IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
|
|
IP6PrefixLengthMask: /[0-9]/,
|
|
|
|
IP64Address: function (v) {
|
|
return Proxmox.Utils.IP64_match.test(v);
|
|
},
|
|
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
|
|
IP64AddressMask: /[A-Fa-f0-9.:]/,
|
|
|
|
IP64CIDRAddress: function (v) {
|
|
let result = Proxmox.Utils.IP64_cidr_match.exec(v);
|
|
if (result === null) {
|
|
return false;
|
|
}
|
|
if (result[1] !== undefined) {
|
|
return result[1] >= 8 && result[1] <= 128;
|
|
} else if (result[2] !== undefined) {
|
|
return result[2] >= 8 && result[2] <= 32;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64',
|
|
IP64CIDRAddressMask: /[A-Fa-f0-9.:/]/,
|
|
|
|
MacAddress: function (v) {
|
|
return /^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/.test(v);
|
|
},
|
|
MacAddressMask: /[a-fA-F0-9:]/,
|
|
MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
|
|
|
|
MacPrefix: function (v) {
|
|
return /^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i.test(v);
|
|
},
|
|
MacPrefixMask: /[a-fA-F0-9:]/,
|
|
MacPrefixText:
|
|
gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
|
|
|
|
BridgeName: function (v) {
|
|
return /^[a-zA-Z][a-zA-Z0-9_]{0,9}$/.test(v);
|
|
},
|
|
VlanName: function (v) {
|
|
if (Proxmox.Utils.VlanInterface_match.test(v)) {
|
|
return true;
|
|
} else if (Proxmox.Utils.Vlan_match.test(v)) {
|
|
return true;
|
|
}
|
|
return true;
|
|
},
|
|
BridgeNameText: gettext('Format') + ': alphanumeric string starting with a character',
|
|
|
|
BondName: function (v) {
|
|
return /^bond\d{1,4}$/.test(v);
|
|
},
|
|
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
|
|
|
|
InterfaceName: function (v) {
|
|
return /^[a-z][a-z0-9_]{1,20}$/.test(v);
|
|
},
|
|
InterfaceNameText:
|
|
gettext('Allowed characters') +
|
|
": 'a-z', '0-9', '_'<br />" +
|
|
gettext('Minimum characters') +
|
|
': 2<br />' +
|
|
gettext('Maximum characters') +
|
|
': 21<br />' +
|
|
gettext('Must start with') +
|
|
": 'a-z'",
|
|
|
|
StorageId: function (v) {
|
|
return /^[a-z][a-z0-9\-_.]*[a-z0-9]$/i.test(v);
|
|
},
|
|
StorageIdText:
|
|
gettext('Allowed characters') +
|
|
": 'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
|
|
gettext('Minimum characters') +
|
|
': 2<br />' +
|
|
gettext('Must start with') +
|
|
": 'A-Z', 'a-z'<br />" +
|
|
gettext('Must end with') +
|
|
": 'A-Z', 'a-z', '0-9'<br />",
|
|
|
|
ConfigId: function (v) {
|
|
return /^[a-z][a-z0-9_-]+$/i.test(v);
|
|
},
|
|
ConfigIdText:
|
|
gettext('Allowed characters') +
|
|
": 'A-Z', 'a-z', '0-9', '_', '-'<br />" +
|
|
gettext('Minimum characters') +
|
|
': 2<br />' +
|
|
gettext('Must start with') +
|
|
': ' +
|
|
gettext('letter'),
|
|
|
|
SafeId: function (v) {
|
|
return Proxmox.Utils.safeIdRegex.test(v);
|
|
},
|
|
SafeIdText:
|
|
gettext('Allowed characters') +
|
|
": 'A-Z', 'a-z', '0-9', '_', '-', '.'<br />" +
|
|
gettext('Must start with') +
|
|
": 'A-Z', 'a-z', '0-9', '_'",
|
|
|
|
HttpProxy: function (v) {
|
|
return /^http:\/\/.*$/.test(v);
|
|
},
|
|
HttpProxyText: gettext('Example') + ': http://username:password@host:port/',
|
|
|
|
CpuSet: function (v) {
|
|
return Proxmox.Utils.CpuSet_match.test(v);
|
|
},
|
|
CpuSetText: gettext('This is not a valid CpuSet'),
|
|
|
|
DnsName: function (v) {
|
|
return Proxmox.Utils.DnsName_match.test(v);
|
|
},
|
|
DnsNameText: gettext('This is not a valid hostname'),
|
|
|
|
DnsNameOrWildcard: function (v) {
|
|
return Proxmox.Utils.DnsName_or_Wildcard_match.test(v);
|
|
},
|
|
DnsNameOrWildcardText: gettext('This is not a valid hostname'),
|
|
|
|
// email regex used by pve-common
|
|
proxmoxMail: function (v) {
|
|
return /^[\w+-~]+(\.[\w+-~]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(v);
|
|
},
|
|
proxmoxMailText: gettext('Example') + ': user@example.com',
|
|
|
|
DnsOrIp: function (v) {
|
|
if (!Proxmox.Utils.DnsName_match.test(v) && !Proxmox.Utils.IP64_match.test(v)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
|
|
|
|
HostPort: function (v) {
|
|
return (
|
|
Proxmox.Utils.HostPort_match.test(v) ||
|
|
Proxmox.Utils.HostPortBrackets_match.test(v) ||
|
|
Proxmox.Utils.IP6_dotnotation_match.test(v)
|
|
);
|
|
},
|
|
HostPortText: gettext('Host/IP address or optional port is invalid'),
|
|
|
|
HostList: function (v) {
|
|
let list = v.split(/[ ,;]+/);
|
|
let i;
|
|
for (i = 0; i < list.length; i++) {
|
|
if (list[i] === '') {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
!Proxmox.Utils.HostPort_match.test(list[i]) &&
|
|
!Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
|
|
!Proxmox.Utils.IP6_dotnotation_match.test(list[i])
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
HostListText: gettext('Not a valid list of hosts'),
|
|
|
|
password: function (val, field) {
|
|
if (field.initialPassField) {
|
|
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
|
|
return val === pwd.getValue();
|
|
}
|
|
return true;
|
|
},
|
|
|
|
passwordText: gettext('Passwords do not match'),
|
|
|
|
email: function (value) {
|
|
let emailre = /^[\w+~-]+(\.[\w+~-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/;
|
|
return emailre.test(value);
|
|
},
|
|
});
|
|
|
|
// we always want the number in x.y format and never in, e.g., x,y
|
|
Ext.define('PVE.form.field.Number', {
|
|
override: 'Ext.form.field.Number',
|
|
submitLocaleSeparator: false,
|
|
});
|
|
|
|
// avois spamming the console and if we ever use this avoid a CORS block error too
|
|
Ext.define('PVE.draw.Container', {
|
|
override: 'Ext.draw.Container',
|
|
defaultDownloadServerUrl: document.location.origin, // avoid that pointing to http://svg.sencha.io
|
|
applyDownloadServerUrl: function (url) {
|
|
// avoid noisy warning, we don't really use that anyway
|
|
url = url || this.defaultDownloadServerUrl;
|
|
return url;
|
|
},
|
|
});
|
|
|
|
// ExtJs 5-6 has an issue with caching
|
|
// see https://www.sencha.com/forum/showthread.php?308989
|
|
Ext.define('Proxmox.UnderlayPool', {
|
|
override: 'Ext.dom.UnderlayPool',
|
|
|
|
checkOut: function () {
|
|
let cache = this.cache,
|
|
len = cache.length,
|
|
el;
|
|
|
|
// do cleanup because some of the objects might have been destroyed
|
|
while (len--) {
|
|
if (cache[len].destroyed) {
|
|
cache.splice(len, 1);
|
|
}
|
|
}
|
|
// end do cleanup
|
|
|
|
el = cache.shift();
|
|
|
|
if (!el) {
|
|
el = Ext.Element.create(this.elementConfig);
|
|
el.setVisibilityMode(2);
|
|
//<debug>
|
|
// tell the spec runner to ignore this element when checking if the dom is clean
|
|
el.dom.setAttribute('data-sticky', true);
|
|
//</debug>
|
|
}
|
|
|
|
return el;
|
|
},
|
|
});
|
|
|
|
// if the order of the values are not the same in originalValue and value
|
|
// extjs will not overwrite value, but marks the field dirty and thus
|
|
// the reset button will be enabled (but clicking it changes nothing)
|
|
// so if the arrays are not the same after resetting, we
|
|
// clear and set it
|
|
Ext.define('Proxmox.form.ComboBox', {
|
|
override: 'Ext.form.field.ComboBox',
|
|
|
|
reset: function () {
|
|
// copied from combobox
|
|
let me = this;
|
|
me.callParent();
|
|
|
|
// clear and set when not the same
|
|
let value = me.getValue();
|
|
if (
|
|
Ext.isArray(me.originalValue) &&
|
|
Ext.isArray(value) &&
|
|
!Ext.Array.equals(value, me.originalValue)
|
|
) {
|
|
me.clearValue();
|
|
me.setValue(me.originalValue);
|
|
}
|
|
},
|
|
|
|
// we also want to open the trigger on editable comboboxes by default
|
|
initComponent: function () {
|
|
let me = this;
|
|
me.callParent();
|
|
|
|
if (me.editable) {
|
|
// The trigger.picker causes first a focus event on the field then
|
|
// toggles the selection picker. Thus skip expanding in this case,
|
|
// else our focus listener expands and the picker.trigger then
|
|
// collapses it directly afterwards.
|
|
Ext.override(me.triggers.picker, {
|
|
onMouseDown: function (e) {
|
|
// copied "should we focus" check from Ext.form.trigger.Trigger
|
|
if (
|
|
e.pointerType !== 'touch' &&
|
|
!this.field.owns(Ext.Element.getActiveElement())
|
|
) {
|
|
me.skip_expand_on_focus = true;
|
|
}
|
|
this.callParent(arguments);
|
|
},
|
|
});
|
|
|
|
me.on('focus', function (combobox) {
|
|
if (!combobox.isExpanded && !combobox.skip_expand_on_focus) {
|
|
combobox.expand();
|
|
}
|
|
combobox.skip_expand_on_focus = false;
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
// when refreshing a grid/tree view, restoring the focus moves the view back to
|
|
// the previously focused item. Save scroll position before refocusing.
|
|
Ext.define(null, {
|
|
override: 'Ext.view.Table',
|
|
|
|
jumpToFocus: false,
|
|
|
|
saveFocusState: function () {
|
|
var me = this,
|
|
store = me.dataSource,
|
|
actionableMode = me.actionableMode,
|
|
navModel = me.getNavigationModel(),
|
|
focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
|
|
activeElement = Ext.fly(Ext.Element.getActiveElement()),
|
|
focusCell =
|
|
focusPosition && focusPosition.view === me && Ext.fly(focusPosition.getCell(true)),
|
|
refocusRow,
|
|
refocusCol,
|
|
record;
|
|
|
|
// The navModel may return a position that is in a locked partner, so check that
|
|
// the focusPosition's cell contains the focus before going forward.
|
|
// The skipSaveFocusState is set by Actionables which actively control
|
|
// focus destination. See CellEditing#activateCell.
|
|
if (!me.skipSaveFocusState && focusCell && focusCell.contains(activeElement)) {
|
|
// Separate this from the instance that the nav model is using.
|
|
focusPosition = focusPosition.clone();
|
|
|
|
// While we deactivate the focused element, suspend focus processing on it.
|
|
activeElement.suspendFocusEvents();
|
|
|
|
// Suspend actionable mode.
|
|
// Each Actionable must silently save its state ready to resume when focus
|
|
// can be restored but should only do that if the activeElement is not the cell itself,
|
|
// this happens when the grid is refreshed while one of the actionables is being
|
|
// deactivated (e.g. Calling view refresh inside CellEditor 'edit' event listener).
|
|
if (actionableMode && focusCell.dom !== activeElement.dom) {
|
|
me.suspendActionableMode();
|
|
} else {
|
|
// Clear position, otherwise the setPosition on the other side
|
|
// will be rejected as a no-op if the resumption position is logically
|
|
// equivalent.
|
|
actionableMode = false;
|
|
navModel.setPosition();
|
|
}
|
|
|
|
// Do not leave the element in that state in case refresh fails, and restoration
|
|
// closure not called.
|
|
activeElement.resumeFocusEvents();
|
|
|
|
// if the store is expanding or collapsing, we should never scroll the view.
|
|
if (store.isExpandingOrCollapsing) {
|
|
return Ext.emptyFn;
|
|
}
|
|
|
|
// The following function will attempt to refocus back in the same mode to the same cell
|
|
// as it was at before based upon the previous record (if it's still in the store),
|
|
// or the row index.
|
|
return function () {
|
|
var all;
|
|
|
|
// May have changed due to reconfigure
|
|
store = me.dataSource;
|
|
|
|
// If we still have data, attempt to refocus in the same mode.
|
|
if (store.getCount()) {
|
|
all = me.all;
|
|
|
|
// Adjust expectations of where we are able to refocus according to
|
|
// what kind of destruction might have been wrought on this view's DOM
|
|
// during focus save.
|
|
refocusRow = Math.min(
|
|
Math.max(focusPosition.rowIdx, all.startIndex),
|
|
all.endIndex,
|
|
);
|
|
|
|
refocusCol = Math.min(
|
|
focusPosition.colIdx,
|
|
me.getVisibleColumnManager().getColumns().length - 1,
|
|
);
|
|
|
|
record = focusPosition.record;
|
|
|
|
focusPosition = new Ext.grid.CellContext(me).setPosition(
|
|
record && store.contains(record) && !record.isCollapsedPlaceholder
|
|
? record
|
|
: refocusRow,
|
|
refocusCol,
|
|
);
|
|
|
|
// Maybe there are no cells. eg: all groups collapsed.
|
|
if (focusPosition.getCell(true)) {
|
|
if (actionableMode) {
|
|
me.resumeActionableMode(focusPosition);
|
|
} else {
|
|
// we sometimes want to scroll back to where we are
|
|
|
|
let x = me.getScrollX();
|
|
let y = me.getScrollY();
|
|
|
|
// Pass "preventNavigation" as true
|
|
// so that that does not cause selection.
|
|
navModel.setPosition(focusPosition, null, null, null, true);
|
|
|
|
if (!navModel.getPosition()) {
|
|
focusPosition.column.focus();
|
|
}
|
|
|
|
if (!me.jumpToFocus) {
|
|
me.scrollTo(x, y);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// No rows - focus associated column header
|
|
focusPosition.column.focus();
|
|
}
|
|
};
|
|
}
|
|
return Ext.emptyFn;
|
|
},
|
|
});
|
|
|
|
// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
|
|
// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
|
|
// data to be submitted.
|
|
Ext.define('Proxmox.form.field.Text', {
|
|
override: 'Ext.form.field.Text',
|
|
|
|
setSubmitValue: function (v) {
|
|
this.submitValue = v;
|
|
},
|
|
});
|
|
|
|
// make mousescrolling work in firefox in the containers overflowhandler,
|
|
// by using only the 'wheel' event not 'mousewheel'(fixed in 7.3)
|
|
// also reverse the scrolldirection (fixed in 7.3)
|
|
// and reduce the default increment
|
|
Ext.define(null, {
|
|
override: 'Ext.layout.container.boxOverflow.Scroller',
|
|
|
|
wheelIncrement: 1,
|
|
|
|
getWheelDelta: function (e) {
|
|
return -e.getWheelDelta(e);
|
|
},
|
|
|
|
onOwnerRender: function (owner) {
|
|
var me = this,
|
|
scrollable = {
|
|
isBoxOverflowScroller: true,
|
|
x: false,
|
|
y: false,
|
|
listeners: {
|
|
scrollend: this.onScrollEnd,
|
|
scope: this,
|
|
},
|
|
};
|
|
|
|
// If no obstrusive scrollbars, allow natural scrolling on mobile touch devices
|
|
if (!Ext.scrollbar.width() && !Ext.platformTags.desktop) {
|
|
scrollable[owner.layout.horizontal ? 'x' : 'y'] = true;
|
|
} else {
|
|
me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheel, me, {
|
|
destroyable: true,
|
|
});
|
|
}
|
|
|
|
owner.setScrollable(scrollable);
|
|
},
|
|
});
|
|
|
|
// extj 6.7 reversed mousewheel direction... (fixed in 7.3)
|
|
// https://forum.sencha.com/forum/showthread.php?472517-Mousewheel-scroll-direction-in-numberfield-with-spinners
|
|
// also use the 'wheel' event instead of 'mousewheel' (fixed in 7.3)
|
|
Ext.define('Proxmox.form.field.Spinner', {
|
|
override: 'Ext.form.field.Spinner',
|
|
|
|
onRender: function () {
|
|
let me = this;
|
|
|
|
me.callParent();
|
|
|
|
// Init mouse wheel
|
|
if (me.mouseWheelEnabled) {
|
|
// Unlisten Ext generated listener ('mousewheel' is deprecated anyway)
|
|
me.mun(me.bodyEl, 'mousewheel', me.onMouseWheel, me);
|
|
|
|
me.mon(me.bodyEl, 'wheel', me.onMouseWheel, me);
|
|
}
|
|
},
|
|
|
|
onMouseWheel: function (e) {
|
|
var me = this,
|
|
delta;
|
|
if (me.hasFocus) {
|
|
delta = e.getWheelDelta();
|
|
if (delta > 0) {
|
|
me.spinDown();
|
|
} else if (delta < 0) {
|
|
me.spinUp();
|
|
}
|
|
e.stopEvent();
|
|
me.onSpinEnd();
|
|
}
|
|
},
|
|
});
|
|
|
|
// add '@' to the valid id
|
|
Ext.define('Proxmox.validIdReOverride', {
|
|
override: 'Ext.Component',
|
|
validIdRe: /^[a-z_][a-z0-9\-_@]*$/i,
|
|
});
|
|
|
|
Ext.define('Proxmox.selection.CheckboxModel', {
|
|
override: 'Ext.selection.CheckboxModel',
|
|
|
|
// [P] use whole checkbox cell to multiselect, not only the checkbox
|
|
checkSelector: '.x-grid-cell-row-checker',
|
|
|
|
// TODO: remove all optimizations below to an override for parent 'Ext.selection.Model' ??
|
|
|
|
// [ P: optimized to remove all records at once as single remove is O(n^3) slow ]
|
|
// records can be an index, a record or an array of records
|
|
doDeselect: function (records, suppressEvent) {
|
|
var me = this,
|
|
selected = me.selected,
|
|
i = 0,
|
|
len,
|
|
record,
|
|
commit;
|
|
if (me.locked || !me.store) {
|
|
return false;
|
|
}
|
|
if (typeof records === 'number') {
|
|
// No matching record, jump out
|
|
record = me.store.getAt(records);
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
records = [record];
|
|
} else if (!Ext.isArray(records)) {
|
|
records = [records];
|
|
}
|
|
// [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
|
|
// returning false, thus the original implementation removed only here in the commit fn,
|
|
// which has an abysmal performance O(n^3). As blocking removal is not the norm, go do the
|
|
// reverse, record blocked records and remove them from the to-be-removed array before
|
|
// applying it. A FF86 i9-9900K on 10k records goes from >40s to ~33ms for >90% deselection
|
|
let committed = false;
|
|
commit = function () {
|
|
committed = true;
|
|
if (record === me.selectionStart) {
|
|
me.selectionStart = null;
|
|
}
|
|
};
|
|
let removalBlocked = [];
|
|
len = records.length;
|
|
me.suspendChanges();
|
|
for (; i < len; i++) {
|
|
record = records[i];
|
|
if (me.isSelected(record)) {
|
|
committed = false;
|
|
me.onSelectChange(record, false, suppressEvent, commit);
|
|
if (!committed) {
|
|
removalBlocked.push(record);
|
|
}
|
|
if (me.destroyed) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (removalBlocked.length > 0) {
|
|
records.remove(removalBlocked);
|
|
}
|
|
selected.remove(records); // [P] FAST(er)
|
|
me.lastSelected = selected.last();
|
|
me.resumeChanges();
|
|
// fire selchange if there was a change and there is no suppressEvent flag
|
|
me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
|
|
return records.length;
|
|
},
|
|
|
|
doMultiSelect: function (records, keepExisting, suppressEvent) {
|
|
var me = this,
|
|
selected = me.selected,
|
|
change = false,
|
|
result,
|
|
i,
|
|
len,
|
|
record,
|
|
commit;
|
|
|
|
if (me.locked) {
|
|
return;
|
|
}
|
|
|
|
records = !Ext.isArray(records) ? [records] : records;
|
|
len = records.length;
|
|
if (!keepExisting && selected.getCount() > 0) {
|
|
result = me.deselectDuringSelect(records, suppressEvent);
|
|
if (me.destroyed) {
|
|
return;
|
|
}
|
|
if (result[0]) {
|
|
// We had a failure during selection, so jump out
|
|
// Fire selection change if we did deselect anything
|
|
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
|
|
return;
|
|
} else {
|
|
// Means something has been deselected, so we've had a change
|
|
change = result[1] > 0;
|
|
}
|
|
}
|
|
|
|
let gotBlocked,
|
|
blockedRecords = [];
|
|
commit = function () {
|
|
if (!selected.getCount()) {
|
|
me.selectionStart = record;
|
|
}
|
|
gotBlocked = false;
|
|
change = true;
|
|
};
|
|
|
|
for (i = 0; i < len; i++) {
|
|
record = records[i];
|
|
if (me.isSelected(record)) {
|
|
continue;
|
|
}
|
|
|
|
gotBlocked = true;
|
|
me.onSelectChange(record, true, suppressEvent, commit);
|
|
if (me.destroyed) {
|
|
return;
|
|
}
|
|
if (gotBlocked) {
|
|
blockedRecords.push(record);
|
|
}
|
|
}
|
|
if (blockedRecords.length > 0) {
|
|
records.remove(blockedRecords);
|
|
}
|
|
selected.add(records);
|
|
me.lastSelected = record;
|
|
|
|
// fire selchange if there was a change and there is no suppressEvent flag
|
|
me.maybeFireSelectionChange(change && !suppressEvent);
|
|
},
|
|
deselectDuringSelect: function (toSelect, suppressEvent) {
|
|
var me = this,
|
|
selected = me.selected.getRange(),
|
|
changed = 0,
|
|
failed = false;
|
|
// Prevent selection change events from firing, will happen during select
|
|
me.suspendChanges();
|
|
me.deselectingDuringSelect = true;
|
|
let toDeselect = selected.filter((item) => !Ext.Array.contains(toSelect, item));
|
|
if (toDeselect.length > 0) {
|
|
changed = me.doDeselect(toDeselect, suppressEvent);
|
|
if (!changed) {
|
|
failed = true;
|
|
}
|
|
if (me.destroyed) {
|
|
failed = true;
|
|
changed = 0;
|
|
}
|
|
}
|
|
me.deselectingDuringSelect = false;
|
|
me.resumeChanges();
|
|
return [failed, changed];
|
|
},
|
|
});
|
|
|
|
// stop nulling of properties
|
|
Ext.define('Proxmox.Component', {
|
|
override: 'Ext.Component',
|
|
clearPropertiesOnDestroy: false,
|
|
});
|
|
|
|
// Fix drag&drop for vms and desktops that detect 'pen' pointerType
|
|
// NOTE: this part has been rewritten in ExtJS 7.4, so re-check once we can upgrade
|
|
Ext.define('Proxmox.view.DragZone', {
|
|
override: 'Ext.view.DragZone',
|
|
|
|
onItemMouseDown: function (view, record, item, index, e) {
|
|
// Ignore touchstart.
|
|
// For touch events, we use longpress.
|
|
if (e.pointerType !== 'touch') {
|
|
this.onTriggerGesture(view, record, item, index, e);
|
|
}
|
|
},
|
|
});
|
|
|
|
// Fix text selection on drag when using DragZone,
|
|
// see https://forum.sencha.com/forum/showthread.php?335100
|
|
Ext.define('Proxmox.dd.DragDropManager', {
|
|
override: 'Ext.dd.DragDropManager',
|
|
|
|
stopEvent: function (e) {
|
|
if (this.stopPropagation) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
if (this.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
});
|
|
|
|
// make it possible to set the SameSite attribute on cookies
|
|
Ext.define('Proxmox.Cookies', {
|
|
override: 'Ext.util.Cookies',
|
|
|
|
set: function (name, value, expires, path, domain, secure, samesite) {
|
|
let attrs = [];
|
|
|
|
if (expires) {
|
|
attrs.push('expires=' + expires.toUTCString());
|
|
}
|
|
|
|
if (path === undefined) {
|
|
// mimic original function's behaviour
|
|
attrs.push('path=/');
|
|
} else if (path) {
|
|
attrs.push('path=' + path);
|
|
}
|
|
|
|
if (domain) {
|
|
attrs.push('domain=' + domain);
|
|
}
|
|
|
|
if (secure === true) {
|
|
attrs.push('secure');
|
|
}
|
|
|
|
if (samesite && ['lax', 'none', 'strict'].includes(samesite.toLowerCase())) {
|
|
attrs.push('samesite=' + samesite);
|
|
}
|
|
|
|
document.cookie = name + '=' + escape(value) + '; ' + attrs.join('; ');
|
|
},
|
|
});
|
|
|
|
// Prevents html entities to be interpreted by the browser in grid columns by
|
|
// default. Custom renderer have to take care of this by themselves.
|
|
//
|
|
// Reset the renderer for the 'check', 'boolean', 'number' and 'date' columns
|
|
// back to undefined, since they rely on the renderer being undefined or
|
|
// returning raw HTML.
|
|
Ext.define('Proxmox.grid.column.Column', {
|
|
override: 'Ext.grid.column.Column',
|
|
|
|
renderer: Ext.htmlEncode,
|
|
});
|
|
|
|
Ext.define('Proxmox.grid.column.Check', {
|
|
override: 'Ext.grid.column.Check',
|
|
|
|
renderer: undefined,
|
|
});
|
|
|
|
Ext.define('Proxmox.grid.column.Boolean', {
|
|
override: 'Ext.grid.column.Boolean',
|
|
|
|
renderer: undefined,
|
|
});
|
|
|
|
Ext.define('Proxmox.grid.column.Number', {
|
|
override: 'Ext.grid.column.Number',
|
|
|
|
renderer: undefined,
|
|
});
|
|
|
|
Ext.define('Proxmox.grid.column.Date', {
|
|
override: 'Ext.grid.column.Date',
|
|
|
|
renderer: undefined,
|
|
});
|
|
|
|
// force alert boxes to be rendered with an Error Icon
|
|
// since Ext.Msg is an object and not a prototype, we need to override it
|
|
// after the framework has been initiated
|
|
Ext.onReady(function () {
|
|
Ext.override(Ext.Msg, {
|
|
alert: function (title, message, fn, scope) {
|
|
if (Ext.isString(title)) {
|
|
let config = {
|
|
title: title,
|
|
message: message,
|
|
icon: this.ERROR,
|
|
buttons: this.OK,
|
|
fn: fn,
|
|
scope: scope,
|
|
minWidth: this.minWidth,
|
|
};
|
|
return this.show(config);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
|
|
// add allowfullscreen to render template to allow the noVNC/xterm.js embedded UIs to go fullscreen
|
|
//
|
|
// The rest is the same as in the separate ux package (extjs/build/packages/ux/classic/ux-debug.js),
|
|
// which we do not load as it's rather big and most of the widgets there are not useful for our UIs
|
|
Ext.define('Ext.ux.IFrame', {
|
|
extend: 'Ext.Component',
|
|
|
|
alias: 'widget.uxiframe',
|
|
|
|
loadMask: 'Loading...',
|
|
|
|
src: 'about:blank',
|
|
|
|
renderTpl: [
|
|
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
|
|
],
|
|
|
|
childEls: ['iframeEl'],
|
|
|
|
initComponent: function () {
|
|
this.callParent();
|
|
|
|
this.frameName = this.frameName || this.id + '-frame';
|
|
},
|
|
|
|
initEvents: function () {
|
|
let me = this;
|
|
|
|
me.callParent();
|
|
me.iframeEl.on('load', me.onLoad, me);
|
|
},
|
|
|
|
initRenderData: function () {
|
|
return Ext.apply(this.callParent(), {
|
|
src: this.src,
|
|
frameName: this.frameName,
|
|
});
|
|
},
|
|
|
|
getBody: function () {
|
|
let doc = this.getDoc();
|
|
|
|
return doc.body || doc.documentElement;
|
|
},
|
|
|
|
getDoc: function () {
|
|
try {
|
|
return this.getWin().document;
|
|
} catch (_ex) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
getWin: function () {
|
|
let me = this,
|
|
name = me.frameName,
|
|
win = Ext.isIE ? me.iframeEl.dom.contentWindow : window.frames[name];
|
|
|
|
return win;
|
|
},
|
|
|
|
getFrame: function () {
|
|
let me = this;
|
|
|
|
return me.iframeEl.dom;
|
|
},
|
|
|
|
onLoad: function () {
|
|
let me = this,
|
|
doc = me.getDoc();
|
|
|
|
if (doc) {
|
|
this.el.unmask();
|
|
this.fireEvent('load', this);
|
|
} else if (me.src) {
|
|
this.el.unmask();
|
|
this.fireEvent('error', this);
|
|
}
|
|
},
|
|
|
|
load: function (src) {
|
|
let me = this,
|
|
text = me.loadMask,
|
|
frame = me.getFrame();
|
|
|
|
if (me.fireEvent('beforeload', me, src) !== false) {
|
|
if (text && me.el) {
|
|
me.el.mask(text);
|
|
}
|
|
|
|
frame.src = me.src = src || me.src;
|
|
}
|
|
},
|
|
});
|