// 1.0-25
Ext.ns('Proxmox');
Ext.ns('Proxmox.Setup');
if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) {
throw "Proxmox library not initialized";
}
// avoid errors related to Accessible Rich Internet Applications
// (access for people with disabilities)
// TODO reenable after all components are upgraded
Ext.enableAria = false;
Ext.enableAriaButtons = false;
Ext.enableAriaPanels = false;
// avoid errors when running without development tools
if (!Ext.isDefined(Ext.global.console)) {
var console = {
dir: function() {},
log: function() {}
};
}
Ext.Ajax.defaultHeaders = {
'Accept': 'application/json'
};
Ext.Ajax.on('beforerequest', function(conn, options) {
if (Proxmox.CSRFPreventionToken) {
if (!options.headers) {
options.headers = {};
}
options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
}
});
Ext.define('Proxmox.Utils', { utilities: {
// this singleton contains miscellaneous utilities
yesText: gettext('Yes'),
noText: gettext('No'),
enabledText: gettext('Enabled'),
disabledText: gettext('Disabled'),
noneText: gettext('none'),
errorText: gettext('Error'),
unknownText: gettext('Unknown'),
defaultText: gettext('Default'),
daysText: gettext('days'),
dayText: gettext('day'),
runningText: gettext('running'),
stoppedText: gettext('stopped'),
neverText: gettext('never'),
totalText: gettext('Total'),
usedText: gettext('Used'),
directoryText: gettext('Directory'),
stateText: gettext('State'),
groupText: gettext('Group'),
language_map: {
zh_CN: 'Chinese (Simplified)',
zh_TW: 'Chinese (Traditional)',
ca: 'Catalan',
da: 'Danish',
en: 'English',
eu: 'Euskera (Basque)',
fr: 'French',
de: 'German',
it: 'Italian',
es: 'Spanish',
ja: 'Japanese',
nb: 'Norwegian (Bokmal)',
nn: 'Norwegian (Nynorsk)',
fa: 'Persian (Farsi)',
pl: 'Polish',
pt_BR: 'Portuguese (Brazil)',
ru: 'Russian',
sl: 'Slovenian',
sv: 'Swedish',
tr: 'Turkish'
},
render_language: function (value) {
if (!value) {
return Proxmox.Utils.defaultText + ' (English)';
}
var text = Proxmox.Utils.language_map[value];
if (text) {
return text + ' (' + value + ')';
}
return value;
},
language_array: function() {
var data = [['__default__', Proxmox.Utils.render_language('')]];
Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
data.push([key, Proxmox.Utils.render_language(value)]);
});
return data;
},
getNoSubKeyHtml: function(url) {
// url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
return Ext.String.format('You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', url || 'http://www.proxmox.com');
},
format_boolean_with_default: function(value) {
if (Ext.isDefined(value) && value !== '__default__') {
return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
}
return Proxmox.Utils.defaultText;
},
format_boolean: function(value) {
return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
},
format_neg_boolean: function(value) {
return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
},
format_enabled_toggle: function(value) {
return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText;
},
format_expire: function(date) {
if (!date) {
return Proxmox.Utils.neverText;
}
return Ext.Date.format(date, "Y-m-d");
},
format_duration_long: function(ut) {
var days = Math.floor(ut / 86400);
ut -= days*86400;
var hours = Math.floor(ut / 3600);
ut -= hours*3600;
var mins = Math.floor(ut / 60);
ut -= mins*60;
var hours_str = '00' + hours.toString();
hours_str = hours_str.substr(hours_str.length - 2);
var mins_str = "00" + mins.toString();
mins_str = mins_str.substr(mins_str.length - 2);
var ut_str = "00" + ut.toString();
ut_str = ut_str.substr(ut_str.length - 2);
if (days) {
var ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText;
return days.toString() + ' ' + ds + ' ' +
hours_str + ':' + mins_str + ':' + ut_str;
} else {
return hours_str + ':' + mins_str + ':' + ut_str;
}
},
format_subscription_level: function(level) {
if (level === 'c') {
return 'Community';
} else if (level === 'b') {
return 'Basic';
} else if (level === 's') {
return 'Standard';
} else if (level === 'p') {
return 'Premium';
} else {
return Proxmox.Utils.noneText;
}
},
compute_min_label_width: function(text, width) {
if (width === undefined) { width = 100; }
var tm = new Ext.util.TextMetrics();
var min = tm.getWidth(text + ':');
return min < width ? width : min;
},
setAuthData: function(data) {
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
Proxmox.UserName = data.username;
Proxmox.LoggedOut = data.LoggedOut;
// creates a session cookie (expire = null)
// that way the cookie gets deleted after the browser window is closed
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
},
authOK: function() {
if (Proxmox.LoggedOut) {
return undefined;
}
return (Proxmox.UserName !== '') && Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name);
},
authClear: function() {
if (Proxmox.LoggedOut) {
return undefined;
}
Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name);
},
// comp.setLoading() is buggy in ExtJS 4.0.7, so we
// use el.mask() instead
setErrorMask: function(comp, msg) {
var el = comp.el;
if (!el) {
return;
}
if (!msg) {
el.unmask();
} else {
if (msg === true) {
el.mask(gettext("Loading..."));
} else {
el.mask(msg);
}
}
},
monStoreErrors: function(me, store, clearMaskBeforeLoad) {
if (clearMaskBeforeLoad) {
me.mon(store, 'beforeload', function(s, operation, eOpts) {
Proxmox.Utils.setErrorMask(me, false);
});
} else {
me.mon(store, 'beforeload', function(s, operation, eOpts) {
if (!me.loadCount) {
me.loadCount = 0; // make sure it is numeric
Proxmox.Utils.setErrorMask(me, true);
}
});
}
// only works with 'proxmox' proxy
me.mon(store.proxy, 'afterload', function(proxy, request, success) {
me.loadCount++;
if (success) {
Proxmox.Utils.setErrorMask(me, false);
return;
}
var msg;
/*jslint nomen: true */
var operation = request._operation;
var error = operation.getError();
if (error.statusText) {
msg = error.statusText + ' (' + error.status + ')';
} else {
msg = gettext('Connection error');
}
Proxmox.Utils.setErrorMask(me, msg);
});
},
extractRequestError: function(result, verbose) {
var msg = gettext('Successful');
if (!result.success) {
msg = gettext("Unknown error");
if (result.message) {
msg = result.message;
if (result.status) {
msg += ' (' + result.status + ')';
}
}
if (verbose && Ext.isObject(result.errors)) {
msg += "
";
Ext.Object.each(result.errors, function(prop, desc) {
msg += "
" + Ext.htmlEncode(prop) + ": " +
Ext.htmlEncode(desc);
});
}
}
return msg;
},
// Ext.Ajax.request
API2Request: function(reqOpts) {
var newopts = Ext.apply({
waitMsg: gettext('Please wait...')
}, reqOpts);
if (!newopts.url.match(/^\/api2/)) {
newopts.url = '/api2/extjs' + newopts.url;
}
delete newopts.callback;
var createWrapper = function(successFn, callbackFn, failureFn) {
Ext.apply(newopts, {
success: function(response, options) {
if (options.waitMsgTarget) {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
options.waitMsgTarget.setLoading(false);
}
}
var result = Ext.decode(response.responseText);
response.result = result;
if (!result.success) {
response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
Ext.callback(callbackFn, options.scope, [options, false, response]);
Ext.callback(failureFn, options.scope, [response, options]);
return;
}
Ext.callback(callbackFn, options.scope, [options, true, response]);
Ext.callback(successFn, options.scope, [response, options]);
},
failure: function(response, options) {
if (options.waitMsgTarget) {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
options.waitMsgTarget.setLoading(false);
}
}
response.result = {};
try {
response.result = Ext.decode(response.responseText);
} catch(e) {}
var msg = gettext('Connection error') + ' - server offline?';
if (response.aborted) {
msg = gettext('Connection error') + ' - aborted.';
} else if (response.timedout) {
msg = gettext('Connection error') + ' - Timeout.';
} else if (response.status && response.statusText) {
msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText;
}
response.htmlStatus = msg;
Ext.callback(callbackFn, options.scope, [options, false, response]);
Ext.callback(failureFn, options.scope, [response, options]);
}
});
};
createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure);
var target = newopts.waitMsgTarget;
if (target) {
if (Proxmox.Utils.toolkit === 'touch') {
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} );
} else {
// Note: ExtJS bug - this does not work when component is not rendered
target.setLoading(newopts.waitMsg);
}
}
Ext.Ajax.request(newopts);
},
checked_command: function(orig_cmd) {
Proxmox.Utils.API2Request({
url: '/nodes/localhost/subscription',
method: 'GET',
//waitMsgTarget: me,
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, opts) {
var data = response.result.data;
if (data.status !== 'Active') {
Ext.Msg.show({
title: gettext('No valid subscription'),
icon: Ext.Msg.WARNING,
msg: Proxmox.Utils.getNoSubKeyHtml(data.url),
buttons: Ext.Msg.OK,
callback: function(btn) {
if (btn !== 'ok') {
return;
}
orig_cmd();
}
});
} else {
orig_cmd();
}
}
});
},
assemble_field_data: function(values, data) {
if (Ext.isObject(data)) {
Ext.Object.each(data, function(name, val) {
if (values.hasOwnProperty(name)) {
var bucket = values[name];
if (!Ext.isArray(bucket)) {
bucket = values[name] = [bucket];
}
if (Ext.isArray(val)) {
values[name] = bucket.concat(val);
} else {
bucket.push(val);
}
} else {
values[name] = val;
}
});
}
},
dialog_title: function(subject, create, isAdd) {
if (create) {
if (isAdd) {
return gettext('Add') + ': ' + subject;
} else {
return gettext('Create') + ': ' + subject;
}
} else {
return gettext('Edit') + ': ' + subject;
}
},
network_iface_types: {
eth: gettext("Network Device"),
bridge: 'Linux Bridge',
bond: 'Linux Bond',
vlan: 'Linux VLAN',
OVSBridge: 'OVS Bridge',
OVSBond: 'OVS Bond',
OVSPort: 'OVS Port',
OVSIntPort: 'OVS IntPort'
},
render_network_iface_type: function(value) {
return Proxmox.Utils.network_iface_types[value] ||
Proxmox.Utils.unknownText;
},
task_desc_table: {
acmenewcert: [ 'SRV', gettext('Order Certificate') ],
acmeregister: [ 'ACME Account', gettext('Register') ],
acmedeactivate: [ 'ACME Account', gettext('Deactivate') ],
acmeupdate: [ 'ACME Account', gettext('Update') ],
acmerefresh: [ 'ACME Account', gettext('Refresh') ],
acmerenew: [ 'SRV', gettext('Renew Certificate') ],
acmerevoke: [ 'SRV', gettext('Revoke Certificate') ],
'move_volume': [ 'CT', gettext('Move Volume') ],
clustercreate: [ '', gettext('Create Cluster') ],
clusterjoin: [ '', gettext('Join Cluster') ],
diskinit: [ 'Disk', gettext('Initialize Disk with GPT') ],
vncproxy: [ 'VM/CT', gettext('Console') ],
spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ],
vncshell: [ '', gettext('Shell') ],
spiceshell: [ '', gettext('Shell') + ' (Spice)' ],
qmsnapshot: [ 'VM', gettext('Snapshot') ],
qmrollback: [ 'VM', gettext('Rollback') ],
qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ],
qmcreate: [ 'VM', gettext('Create') ],
qmrestore: [ 'VM', gettext('Restore') ],
qmdestroy: [ 'VM', gettext('Destroy') ],
qmigrate: [ 'VM', gettext('Migrate') ],
qmclone: [ 'VM', gettext('Clone') ],
qmmove: [ 'VM', gettext('Move disk') ],
qmtemplate: [ 'VM', gettext('Convert to template') ],
qmstart: [ 'VM', gettext('Start') ],
qmstop: [ 'VM', gettext('Stop') ],
qmreset: [ 'VM', gettext('Reset') ],
qmshutdown: [ 'VM', gettext('Shutdown') ],
qmsuspend: [ 'VM', gettext('Hibernate') ],
qmpause: [ 'VM', gettext('Pause') ],
qmresume: [ 'VM', gettext('Resume') ],
qmconfig: [ 'VM', gettext('Configure') ],
vzsnapshot: [ 'CT', gettext('Snapshot') ],
vzrollback: [ 'CT', gettext('Rollback') ],
vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ],
vzcreate: ['CT', gettext('Create') ],
vzrestore: ['CT', gettext('Restore') ],
vzdestroy: ['CT', gettext('Destroy') ],
vzmigrate: [ 'CT', gettext('Migrate') ],
vzclone: [ 'CT', gettext('Clone') ],
vztemplate: [ 'CT', gettext('Convert to template') ],
vzstart: ['CT', gettext('Start') ],
vzstop: ['CT', gettext('Stop') ],
vzmount: ['CT', gettext('Mount') ],
vzumount: ['CT', gettext('Unmount') ],
vzshutdown: ['CT', gettext('Shutdown') ],
vzsuspend: [ 'CT', gettext('Suspend') ],
vzresume: [ 'CT', gettext('Resume') ],
hamigrate: [ 'HA', gettext('Migrate') ],
hastart: [ 'HA', gettext('Start') ],
hastop: [ 'HA', gettext('Stop') ],
srvstart: ['SRV', gettext('Start') ],
srvstop: ['SRV', gettext('Stop') ],
srvrestart: ['SRV', gettext('Restart') ],
srvreload: ['SRV', gettext('Reload') ],
cephcreatemgr: ['Ceph Manager', gettext('Create') ],
cephdestroymgr: ['Ceph Manager', gettext('Destroy') ],
cephcreatemon: ['Ceph Monitor', gettext('Create') ],
cephdestroymon: ['Ceph Monitor', gettext('Destroy') ],
cephcreateosd: ['Ceph OSD', gettext('Create') ],
cephdestroyosd: ['Ceph OSD', gettext('Destroy') ],
cephcreatepool: ['Ceph Pool', gettext('Create') ],
cephdestroypool: ['Ceph Pool', gettext('Destroy') ],
cephfscreate: ['CephFS', gettext('Create') ],
cephcreatemds: ['Ceph Metadata Server', gettext('Create') ],
cephdestroymds: ['Ceph Metadata Server', gettext('Destroy') ],
imgcopy: ['', gettext('Copy data') ],
imgdel: ['', gettext('Erase data') ],
unknownimgdel: ['', gettext('Destroy image from unknown guest') ],
download: ['', gettext('Download') ],
vzdump: ['VM/CT', gettext('Backup') ],
aptupdate: ['', gettext('Update package database') ],
startall: [ '', gettext('Start all VMs and Containers') ],
stopall: [ '', gettext('Stop all VMs and Containers') ],
migrateall: [ '', gettext('Migrate all VMs and Containers') ],
dircreate: [ gettext('Directory Storage'), gettext('Create') ],
lvmcreate: [ gettext('LVM Storage'), gettext('Create') ],
lvmthincreate: [ gettext('LVM-Thin Storage'), gettext('Create') ],
zfscreate: [ gettext('ZFS Storage'), gettext('Create') ]
},
format_task_description: function(type, id) {
var farray = Proxmox.Utils.task_desc_table[type];
var text;
if (!farray) {
text = type;
if (id) {
type += ' ' + id;
}
return text;
}
var prefix = farray[0];
text = farray[1];
if (prefix) {
return prefix + ' ' + id + ' - ' + text;
}
return text;
},
format_size: function(size) {
/*jslint confusion: true */
var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
var num = 0;
while (size >= 1024 && ((num++)+1) < units.length) {
size = size / 1024;
}
return size.toFixed((num > 0)?2:0) + " " + units[num] + "B";
},
render_upid: function(value, metaData, record) {
var type = record.data.type;
var id = record.data.id;
return Proxmox.Utils.format_task_description(type, id);
},
render_uptime: function(value) {
var uptime = value;
if (uptime === undefined) {
return '';
}
if (uptime <= 0) {
return '-';
}
return Proxmox.Utils.format_duration_long(uptime);
},
parse_task_upid: function(upid) {
var task = {};
var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/);
if (!res) {
throw "unable to parse upid '" + upid + "'";
}
task.node = res[1];
task.pid = parseInt(res[2], 16);
task.pstart = parseInt(res[3], 16);
task.starttime = parseInt(res[4], 16);
task.type = res[5];
task.id = res[6];
task.user = res[7];
task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
return task;
},
render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
var servertime = new Date(value * 1000);
return Ext.Date.format(servertime, 'Y-m-d H:i:s');
},
openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
var url = Ext.Object.toQueryString({
console: vmtype, // kvm, lxc, upgrade or shell
xtermjs: 1,
vmid: vmid,
vmname: vmname,
node: nodename,
cmd: cmd,
});
var nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
if (nw) {
nw.focus();
}
}
},
singleton: true,
constructor: function() {
var me = this;
Ext.apply(me, me.utilities);
var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/([0-9]{1,2})$");
var IPV6_REGEXP = "(?:" +
"(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
"(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
"(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" +
"(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" +
")";
me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/([0-9]{1,3})$");
me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
var DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))";
me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$");
me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$");
me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$");
}
});
// 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) {
var 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' + "
" + 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) {
var 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' + "
" + 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\.:]/,
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 (/^vmbr\d{1,4}$/).test(v);
},
BridgeNameText: gettext('Format') + ': vmbrN, where 0 <= N <= 9999',
BondName: function(v) {
return (/^bond\d{1,4}$/).test(v);
},
BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999',
InterfaceName: function(v) {
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
},
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "
" +
gettext("Minimum characters") + ": 2" + "
" +
gettext("Maximum characters") + ": 21" + "
" +
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', '-', '_', '.'" + "
" +
gettext("Minimum characters") + ": 2" + "
" +
gettext("Must start with") + ": 'A-Z', 'a-z'
" +
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
",
ConfigId: function(v) {
return (/^[a-z][a-z0-9\_]+$/i).test(v);
},
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "
" +
gettext("Minimum characters") + ": 2" + "
" +
gettext("Must start with") + ": " + gettext("letter"),
HttpProxy: function(v) {
return (/^http:\/\/.*$/).test(v);
},
HttpProxyText: gettext('Example') + ": http://username:password@host:port/",
DnsName: function(v) {
return Proxmox.Utils.DnsName_match.test(v);
},
DnsNameText: gettext('This is not a valid DNS name'),
// workaround for https://www.sencha.com/forum/showthread.php?302150
proxmoxMail: function(v) {
return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).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.'),
HostList: function(v) {
var list = v.split(/[\ \,\;]+/);
var 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) {
var pwd = field.up('form').down(
'[name=' + field.initialPassField + ']');
return (val == pwd.getValue());
}
return true;
},
passwordText: gettext('Passwords do not match')
});
// Firefox 52+ Touchscreen bug
// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
Ext.define('EXTJS_23846.Element', {
override: 'Ext.dom.Element'
}, function(Element) {
var supports = Ext.supports,
proto = Element.prototype,
eventMap = proto.eventMap,
additiveEvents = proto.additiveEvents;
if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
eventMap.touchstart = 'mousedown';
eventMap.touchmove = 'mousemove';
eventMap.touchend = 'mouseup';
eventMap.touchcancel = 'mouseup';
additiveEvents.mousedown = 'mousedown';
additiveEvents.mousemove = 'mousemove';
additiveEvents.mouseup = 'mouseup';
additiveEvents.touchstart = 'touchstart';
additiveEvents.touchmove = 'touchmove';
additiveEvents.touchend = 'touchend';
additiveEvents.touchcancel = 'touchcancel';
additiveEvents.pointerdown = 'mousedown';
additiveEvents.pointermove = 'mousemove';
additiveEvents.pointerup = 'mouseup';
additiveEvents.pointercancel = 'mouseup';
}
});
Ext.define('EXTJS_23846.Gesture', {
override: 'Ext.event.publisher.Gesture'
}, function(Gesture) {
var me = Gesture.instance;
if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
me.registerEvents();
}
});
// 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
});
// 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 () {
var 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);
//
// tell the spec runner to ignore this element when checking if the dom is clean
el.dom.setAttribute('data-sticky', true);
//
}
return el;
}
});
// 'Enter' in Textareas and aria multiline fields should not activate the
// defaultbutton, fixed in extjs 6.0.2
Ext.define('PVE.panel.Panel', {
override: 'Ext.panel.Panel',
fireDefaultButton: function(e) {
if (e.target.getAttribute('aria-multiline') === 'true' ||
e.target.tagName === "TEXTAREA") {
return true;
}
return this.callParent(arguments);
}
});
// 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
var me = this;
me.callParent();
// clear and set when not the same
var value = me.getValue();
if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) {
me.clearValue();
me.setValue(me.originalValue);
}
}
});
// 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),
refocusRow, refocusCol;
if (focusPosition) {
// Separate this from the instance that the nav model is using.
focusPosition = focusPosition.clone();
// Exit actionable mode.
// We must inform any Actionables that they must relinquish control.
// Tabbability must be reset.
if (actionableMode) {
me.ownerGrid.setActionableMode(false);
}
// Blur the focused descendant, but do not trigger focusLeave.
me.el.dom.focus();
// Exiting actionable mode navigates to the owning cell, so in either focus mode we must
// clear the navigation position
navModel.setPosition();
// 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 inthe store), or the row index.
return function() {
// If we still have data, attempt to refocus in the same mode.
if (store.getCount()) {
// 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(focusPosition.rowIdx, me.all.getCount() - 1);
refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1);
focusPosition = new Ext.grid.CellContext(me).setPosition(
store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol);
if (actionableMode) {
me.ownerGrid.setActionableMode(true, focusPosition);
} else {
me.cellFocused = true;
// we sometimes want to scroll back to where we were
var x = me.getScrollX();
var y = me.getScrollY();
// Pass "preventNavigation" as true so that that does not cause selection.
navModel.setPosition(focusPosition, null, null, null, true);
if (!me.jumpToFocus) {
me.scrollTo(x,y);
}
}
}
// No rows - focus associated column header
else {
focusPosition.column.focus();
}
};
}
return Ext.emptyFn;
}
});
// should be fixed with ExtJS 6.0.2, see:
// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
Ext.define('Proxmox.Datepicker', {
override: 'Ext.picker.Date',
hideMode: 'visibility'
});
// 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;
},
});
// this should be fixed with ExtJS 6.0.2
// make mousescrolling work in firefox in the containers overflowhandler
Ext.define(null, {
override: 'Ext.layout.container.boxOverflow.Scroller',
createWheelListener: function() {
var me = this;
if (Ext.isFirefox) {
me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true});
} else {
me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true});
}
},
// special wheel handler for firefox. differs from the default onMouseWheel
// handler by using deltaY instead of wheelDeltaY and no normalizing,
// because it is already
onMouseWheelFirefox: function(e) {
e.stopEvent();
var delta = e.browserEvent.deltaY || 0;
this.scrollBy(delta * this.wheelIncrement, false);
}
});
// 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() {
/*jslint confusion: true */
Ext.override(Ext.Msg, {
alert: function(title, message, fn, scope) {
if (Ext.isString(title)) {
var config = {
title: title,
message: message,
icon: this.ERROR,
buttons: this.OK,
fn: fn,
scope : scope,
minWidth: this.minWidth
};
return this.show(config);
}
}
});
/*jslint confusion: false */
});
Ext.define('Ext.ux.IFrame', {
extend: 'Ext.Component',
alias: 'widget.uxiframe',
loadMask: 'Loading...',
src: 'about:blank',
renderTpl: [
''
],
childEls: ['iframeEl'],
initComponent: function () {
this.callParent();
this.frameName = this.frameName || this.id + '-frame';
},
initEvents : function() {
var 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() {
var doc = this.getDoc();
return doc.body || doc.documentElement;
},
getDoc: function() {
try {
return this.getWin().document;
} catch (ex) {
return null;
}
},
getWin: function() {
var me = this,
name = me.frameName,
win = Ext.isIE
? me.iframeEl.dom.contentWindow
: window.frames[name];
return win;
},
getFrame: function() {
var me = this;
return me.iframeEl.dom;
},
beforeDestroy: function () {
this.cleanupListeners(true);
this.callParent();
},
cleanupListeners: function(destroying){
var doc, prop;
if (this.rendered) {
try {
doc = this.getDoc();
if (doc) {
/*jslint nomen: true*/
Ext.get(doc).un(this._docListeners);
/*jslint nomen: false*/
if (destroying && doc.hasOwnProperty) {
for (prop in doc) {
if (doc.hasOwnProperty(prop)) {
delete doc[prop];
}
}
}
}
} catch(e) { }
}
},
onLoad: function() {
var me = this,
doc = me.getDoc(),
fn = me.onRelayedEvent;
if (doc) {
try {
// These events need to be relayed from the inner document (where they stop
// bubbling) up to the outer document. This has to be done at the DOM level so
// the event reaches listeners on elements like the document body. The effected
// mechanisms that depend on this bubbling behavior are listed to the right
// of the event.
/*jslint nomen: true*/
Ext.get(doc).on(
me._docListeners = {
mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
mousemove: fn, // window resize drag detection
mouseup: fn, // window resize termination
click: fn, // not sure, but just to be safe
dblclick: fn, // not sure again
scope: me
}
);
/*jslint nomen: false*/
} catch(e) {
// cannot do this xss
}
// We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
},
onRelayedEvent: function (event) {
// relay event from the iframe's document to the document that owns the iframe...
var iframeEl = this.iframeEl,
// Get the left-based iframe position
iframeXY = iframeEl.getTrueXY(),
originalEventXY = event.getXY(),
// Get the left-based XY position.
// This is because the consumer of the injected event will
// perform its own RTL normalization.
eventXY = event.getTrueXY();
// the event from the inner document has XY relative to that document's origin,
// so adjust it to use the origin of the iframe in the outer document:
event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
event.injectEvent(iframeEl); // blame the iframe for the event...
event.xy = originalEventXY; // restore the original XY (just for safety)
},
load: function (src) {
var 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);
}
}
});
Ext.define('Proxmox.Mixin.CBind', {
extend: 'Ext.Mixin',
mixinConfig: {
before: {
initComponent: 'cloneTemplates'
}
},
cloneTemplates: function() {
var me = this;
if (typeof(me.cbindData) == "function") {
me.cbindData = me.cbindData(me.initialConfig) || {};
}
var getConfigValue = function(cname) {
if (cname in me.initialConfig) {
return me.initialConfig[cname];
}
if (cname in me.cbindData) {
return me.cbindData[cname];
}
if (cname in me) {
return me[cname];
}
throw "unable to get cbind data for '" + cname + "'";
};
var applyCBind = function(obj) {
var cbind = obj.cbind, prop, cdata, cvalue, match, found;
if (!cbind) return;
for (prop in cbind) {
cdata = cbind[prop];
found = false;
if (match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata)) {
var cvalue = getConfigValue(match[2]);
if (match[1]) cvalue = !cvalue;
obj[prop] = cvalue;
found = true;
} else if (match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata)) {
var keys = match[2].split('.');
var cvalue = getConfigValue(keys.shift());
keys.forEach(function(k) {
if (k in cvalue) {
cvalue = cvalue[k];
} else {
throw "unable to get cbind data for '" + match[2] + "'";
}
});
if (match[1]) cvalue = !cvalue;
obj[prop] = cvalue;
found = true;
} else {
obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, function(match, cname) {
var cvalue = getConfigValue(cname);
found = true;
return cvalue;
});
}
if (!found) {
throw "unable to parse cbind template '" + cdata + "'";
}
}
};
if (me.cbind) {
applyCBind(me);
}
var cloneTemplateArray = function(org) {
var copy, i, found, el, elcopy, arrayLength;
arrayLength = org.length;
found = false;
for (i = 0; i < arrayLength; i++) {
el = org[i];
if (el.constructor == Object && el.xtype) {
found = true;
break;
}
}
if (!found) return org; // no need to copy
copy = [];
for (i = 0; i < arrayLength; i++) {
el = org[i];
if (el.constructor == Object && el.xtype) {
elcopy = cloneTemplateObject(el);
if (elcopy.cbind) {
applyCBind(elcopy);
}
copy.push(elcopy);
} else if (el.constructor == Array) {
elcopy = cloneTemplateArray(el);
copy.push(elcopy);
} else {
copy.push(el);
}
}
return copy;
};
var cloneTemplateObject = function(org) {
var res = {}, prop, el, copy;
for (prop in org) {
el = org[prop];
if (el.constructor == Object && el.xtype) {
copy = cloneTemplateObject(el);
if (copy.cbind) {
applyCBind(copy);
}
res[prop] = copy;
} else if (el.constructor == Array) {
copy = cloneTemplateArray(el);
res[prop] = copy;
} else {
res[prop] = el;
}
}
return res;
};
var condCloneProperties = function() {
var prop, el, i, tmp;
for (prop in me) {
el = me[prop];
if (el === undefined || el === null) continue;
if (typeof(el) === 'object' && el.constructor == Object) {
if (el.xtype && prop != 'config') {
me[prop] = cloneTemplateObject(el);
}
} else if (el.constructor == Array) {
tmp = cloneTemplateArray(el);
me[prop] = tmp;
}
}
};
condCloneProperties();
}
});
/* A reader to store a single JSON Object (hash) into a storage.
* Also accepts an array containing a single hash.
*
* So it can read:
*
* example1: {data1: "xyz", data2: "abc"}
* returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
*
* example2: [ {data1: "xyz", data2: "abc"} ]
* returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
*
* If you set 'readArray', the reader expexts the object as array:
*
* example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }]
* returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}]
*
* Note: The records can contain additional properties (like 'p2' above) when you use 'readArray'
*
* Additional feature: specify allowed properties with default values with 'rows' object
*
* var rows = {
* memory: {
* required: true,
* defaultValue: 512
* }
* }
*
*/
Ext.define('Proxmox.data.reader.JsonObject', {
extend: 'Ext.data.reader.Json',
alias : 'reader.jsonobject',
readArray: false,
rows: undefined,
constructor: function(config) {
var me = this;
Ext.apply(me, config || {});
me.callParent([config]);
},
getResponseData: function(response) {
var me = this;
var data = [];
try {
var result = Ext.decode(response.responseText);
// get our data items inside the server response
var root = result[me.getRootProperty()];
if (me.readArray) {
var rec_hash = {};
Ext.Array.each(root, function(rec) {
if (Ext.isDefined(rec.key)) {
rec_hash[rec.key] = rec;
}
});
if (me.rows) {
Ext.Object.each(me.rows, function(key, rowdef) {
var rec = rec_hash[key];
if (Ext.isDefined(rec)) {
if (!Ext.isDefined(rec.value)) {
rec.value = rowdef.defaultValue;
}
data.push(rec);
} else if (Ext.isDefined(rowdef.defaultValue)) {
data.push({key: key, value: rowdef.defaultValue} );
} else if (rowdef.required) {
data.push({key: key, value: undefined });
}
});
} else {
Ext.Array.each(root, function(rec) {
if (Ext.isDefined(rec.key)) {
data.push(rec);
}
});
}
} else {
var org_root = root;
if (Ext.isArray(org_root)) {
if (root.length == 1) {
root = org_root[0];
} else {
root = {};
}
}
if (me.rows) {
Ext.Object.each(me.rows, function(key, rowdef) {
if (Ext.isDefined(root[key])) {
data.push({key: key, value: root[key]});
} else if (Ext.isDefined(rowdef.defaultValue)) {
data.push({key: key, value: rowdef.defaultValue});
} else if (rowdef.required) {
data.push({key: key, value: undefined});
}
});
} else {
Ext.Object.each(root, function(key, value) {
data.push({key: key, value: value });
});
}
}
}
catch (ex) {
Ext.Error.raise({
response: response,
json: response.responseText,
parseError: ex,
msg: 'Unable to parse the JSON returned by the server: ' + ex.toString()
});
}
return data;
}
});
Ext.define('Proxmox.RestProxy', {
extend: 'Ext.data.RestProxy',
alias : 'proxy.proxmox',
pageParam : null,
startParam: null,
limitParam: null,
groupParam: null,
sortParam: null,
filterParam: null,
noCache : false,
afterRequest: function(request, success) {
this.fireEvent('afterload', this, request, success);
return;
},
constructor: function(config) {
Ext.applyIf(config, {
reader: {
type: 'json',
rootProperty: config.root || 'data'
}
});
this.callParent([config]);
}
}, function() {
Ext.define('KeyValue', {
extend: "Ext.data.Model",
fields: [ 'key', 'value' ],
idProperty: 'key'
});
Ext.define('KeyValuePendingDelete', {
extend: "Ext.data.Model",
fields: [ 'key', 'value', 'pending', 'delete' ],
idProperty: 'key'
});
Ext.define('proxmox-tasks', {
extend: 'Ext.data.Model',
fields: [
{ name: 'starttime', type : 'date', dateFormat: 'timestamp' },
{ name: 'endtime', type : 'date', dateFormat: 'timestamp' },
{ name: 'pid', type: 'int' },
'node', 'upid', 'user', 'status', 'type', 'id'
],
idProperty: 'upid'
});
Ext.define('proxmox-cluster-log', {
extend: 'Ext.data.Model',
fields: [
{ name: 'uid' , type: 'int' },
{ name: 'time', type : 'date', dateFormat: 'timestamp' },
{ name: 'pri', type: 'int' },
{ name: 'pid', type: 'int' },
'node', 'user', 'tag', 'msg',
{
name: 'id',
convert: function(value, record) {
var info = record.data;
var text;
if (value) {
return value;
}
// compute unique ID
return info.uid + ':' + info.node;
}
}
],
idProperty: 'id'
});
});
/* Extends the Ext.data.Store type
* with startUpdate() and stopUpdate() methods
* to refresh the store data in the background
* Components using this store directly will flicker
* due to the redisplay of the element ater 'config.interval' ms
*
* Note that you have to call yourself startUpdate() for the background load
* to begin
*/
Ext.define('Proxmox.data.UpdateStore', {
extend: 'Ext.data.Store',
alias: 'store.update',
isStopped: true,
autoStart: false,
destroy: function() {
var me = this;
me.stopUpdate();
me.callParent();
},
constructor: function(config) {
var me = this;
config = config || {};
if (!config.interval) {
config.interval = 3000;
}
if (!config.storeid) {
throw "no storeid specified";
}
var load_task = new Ext.util.DelayedTask();
var run_load_task = function() {
if (me.isStopped) {
return;
}
if (Proxmox.Utils.authOK()) {
var start = new Date();
me.load(function() {
var runtime = (new Date()) - start;
var interval = config.interval + runtime*2;
load_task.delay(interval, run_load_task);
});
} else {
load_task.delay(200, run_load_task);
}
};
Ext.apply(config, {
startUpdate: function() {
me.isStopped = false;
// run_load_task(); this makes problems with chrome
load_task.delay(1, run_load_task);
},
stopUpdate: function() {
me.isStopped = true;
load_task.cancel();
}
});
me.callParent([config]);
me.load_task = load_task;
if (me.autoStart) {
me.startUpdate();
}
}
});
/*
* The DiffStore is a in-memory store acting as proxy between a real store
* instance and a component.
* Its purpose is to redisplay the component *only* if the data has been changed
* inside the real store, to avoid the annoying visual flickering of using
* the real store directly.
*
* Implementation:
* The DiffStore monitors via mon() the 'load' events sent by the real store.
* On each 'load' event, the DiffStore compares its own content with the target
* store (call to cond_add_item()) and then fires a 'refresh' event.
* The 'refresh' event will automatically trigger a view refresh on the component
* who binds to this store.
*/
/* Config properties:
* rstore: the realstore which will autorefresh its content from the API
* Only works if rstore has a model and use 'idProperty'
* sortAfterUpdate: sort the diffstore before rendering the view
*/
Ext.define('Proxmox.data.DiffStore', {
extend: 'Ext.data.Store',
alias: 'store.diff',
sortAfterUpdate: false,
constructor: function(config) {
var me = this;
config = config || {};
if (!config.rstore) {
throw "no rstore specified";
}
if (!config.rstore.model) {
throw "no rstore model specified";
}
var rstore = config.rstore;
Ext.apply(config, {
model: rstore.model,
proxy: { type: 'memory' }
});
me.callParent([config]);
var first_load = true;
var cond_add_item = function(data, id) {
var olditem = me.getById(id);
if (olditem) {
olditem.beginEdit();
Ext.Array.each(me.model.prototype.fields, function(field) {
if (olditem.data[field.name] !== data[field.name]) {
olditem.set(field.name, data[field.name]);
}
});
olditem.endEdit(true);
olditem.commit();
} else {
var newrec = Ext.create(me.model, data);
var pos = (me.appendAtStart && !first_load) ? 0 : me.data.length;
me.insert(pos, newrec);
}
};
var loadFn = function(s, records, success) {
if (!success) {
return;
}
me.suspendEvents();
// getSource returns null if data is not filtered
// if it is filtered it returns all records
var allItems = me.getData().getSource() || me.getData();
// remove vanished items
allItems.each(function(olditem) {
var item = rstore.getById(olditem.getId());
if (!item) {
me.remove(olditem);
}
});
rstore.each(function(item) {
cond_add_item(item.data, item.getId());
});
me.filter();
if (me.sortAfterUpdate) {
me.sort();
}
first_load = false;
me.resumeEvents();
me.fireEvent('refresh', me);
me.fireEvent('datachanged', me);
};
if (rstore.isLoaded()) {
// if store is already loaded,
// insert items instantly
loadFn(rstore, [], true);
}
me.mon(rstore, 'load', loadFn);
}
});
/* This store encapsulates data items which are organized as an Array of key-values Objects
* ie data[0] contains something like {key: "keyboard", value: "da"}
*
* Designed to work with the KeyValue model and the JsonObject data reader
*/
Ext.define('Proxmox.data.ObjectStore', {
extend: 'Proxmox.data.UpdateStore',
getRecord: function() {
var me = this;
var record = Ext.create('Ext.data.Model');
me.getData().each(function(item) {
record.set(item.data.key, item.data.value);
});
record.commit(true);
return record;
},
constructor: function(config) {
var me = this;
config = config || {};
if (!config.storeid) {
config.storeid = 'proxmox-store-' + (++Ext.idSeed);
}
Ext.applyIf(config, {
model: 'KeyValue',
proxy: {
type: 'proxmox',
url: config.url,
extraParams: config.extraParams,
reader: {
type: 'jsonobject',
rows: config.rows,
readArray: config.readArray,
rootProperty: config.root || 'data'
}
}
});
me.callParent([config]);
}
});
/* Extends the Proxmox.data.UpdateStore type
*
*
*/
Ext.define('Proxmox.data.RRDStore', {
extend: 'Proxmox.data.UpdateStore',
alias: 'store.proxmoxRRDStore',
setRRDUrl: function(timeframe, cf) {
var me = this;
if (!timeframe) {
timeframe = me.timeframe;
}
if (!cf) {
cf = me.cf;
}
me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf;
},
proxy: {
type: 'proxmox'
},
timeframe: 'hour',
cf: 'AVERAGE',
constructor: function(config) {
var me = this;
config = config || {};
// set default interval to 30seconds
if (!config.interval) {
config.interval = 30000;
}
// set a new storeid
if (!config.storeid) {
config.storeid = 'rrdstore-' + (++Ext.idSeed);
}
// rrdurl is required
if (!config.rrdurl) {
throw "no rrdurl specified";
}
var stateid = 'proxmoxRRDTypeSelection';
var sp = Ext.state.Manager.getProvider();
var stateinit = sp.get(stateid);
if (stateinit) {
if(stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn){
me.timeframe = stateinit.timeframe;
me.rrdcffn = stateinit.cf;
}
}
me.callParent([config]);
me.setRRDUrl();
me.mon(sp, 'statechange', function(prov, key, state){
if (key === stateid) {
if (state && state.id) {
if (state.timeframe !== me.timeframe || state.cf !== me.cf) {
me.timeframe = state.timeframe;
me.cf = state.cf;
me.setRRDUrl();
me.reload();
}
}
}
});
}
});
Ext.define('Timezone', {
extend: 'Ext.data.Model',
fields: ['zone']
});
Ext.define('Proxmox.data.TimezoneStore', {
extend: 'Ext.data.Store',
model: 'Timezone',
data: [
['Africa/Abidjan'],
['Africa/Accra'],
['Africa/Addis_Ababa'],
['Africa/Algiers'],
['Africa/Asmara'],
['Africa/Bamako'],
['Africa/Bangui'],
['Africa/Banjul'],
['Africa/Bissau'],
['Africa/Blantyre'],
['Africa/Brazzaville'],
['Africa/Bujumbura'],
['Africa/Cairo'],
['Africa/Casablanca'],
['Africa/Ceuta'],
['Africa/Conakry'],
['Africa/Dakar'],
['Africa/Dar_es_Salaam'],
['Africa/Djibouti'],
['Africa/Douala'],
['Africa/El_Aaiun'],
['Africa/Freetown'],
['Africa/Gaborone'],
['Africa/Harare'],
['Africa/Johannesburg'],
['Africa/Kampala'],
['Africa/Khartoum'],
['Africa/Kigali'],
['Africa/Kinshasa'],
['Africa/Lagos'],
['Africa/Libreville'],
['Africa/Lome'],
['Africa/Luanda'],
['Africa/Lubumbashi'],
['Africa/Lusaka'],
['Africa/Malabo'],
['Africa/Maputo'],
['Africa/Maseru'],
['Africa/Mbabane'],
['Africa/Mogadishu'],
['Africa/Monrovia'],
['Africa/Nairobi'],
['Africa/Ndjamena'],
['Africa/Niamey'],
['Africa/Nouakchott'],
['Africa/Ouagadougou'],
['Africa/Porto-Novo'],
['Africa/Sao_Tome'],
['Africa/Tripoli'],
['Africa/Tunis'],
['Africa/Windhoek'],
['America/Adak'],
['America/Anchorage'],
['America/Anguilla'],
['America/Antigua'],
['America/Araguaina'],
['America/Argentina/Buenos_Aires'],
['America/Argentina/Catamarca'],
['America/Argentina/Cordoba'],
['America/Argentina/Jujuy'],
['America/Argentina/La_Rioja'],
['America/Argentina/Mendoza'],
['America/Argentina/Rio_Gallegos'],
['America/Argentina/Salta'],
['America/Argentina/San_Juan'],
['America/Argentina/San_Luis'],
['America/Argentina/Tucuman'],
['America/Argentina/Ushuaia'],
['America/Aruba'],
['America/Asuncion'],
['America/Atikokan'],
['America/Bahia'],
['America/Bahia_Banderas'],
['America/Barbados'],
['America/Belem'],
['America/Belize'],
['America/Blanc-Sablon'],
['America/Boa_Vista'],
['America/Bogota'],
['America/Boise'],
['America/Cambridge_Bay'],
['America/Campo_Grande'],
['America/Cancun'],
['America/Caracas'],
['America/Cayenne'],
['America/Cayman'],
['America/Chicago'],
['America/Chihuahua'],
['America/Costa_Rica'],
['America/Cuiaba'],
['America/Curacao'],
['America/Danmarkshavn'],
['America/Dawson'],
['America/Dawson_Creek'],
['America/Denver'],
['America/Detroit'],
['America/Dominica'],
['America/Edmonton'],
['America/Eirunepe'],
['America/El_Salvador'],
['America/Fortaleza'],
['America/Glace_Bay'],
['America/Godthab'],
['America/Goose_Bay'],
['America/Grand_Turk'],
['America/Grenada'],
['America/Guadeloupe'],
['America/Guatemala'],
['America/Guayaquil'],
['America/Guyana'],
['America/Halifax'],
['America/Havana'],
['America/Hermosillo'],
['America/Indiana/Indianapolis'],
['America/Indiana/Knox'],
['America/Indiana/Marengo'],
['America/Indiana/Petersburg'],
['America/Indiana/Tell_City'],
['America/Indiana/Vevay'],
['America/Indiana/Vincennes'],
['America/Indiana/Winamac'],
['America/Inuvik'],
['America/Iqaluit'],
['America/Jamaica'],
['America/Juneau'],
['America/Kentucky/Louisville'],
['America/Kentucky/Monticello'],
['America/La_Paz'],
['America/Lima'],
['America/Los_Angeles'],
['America/Maceio'],
['America/Managua'],
['America/Manaus'],
['America/Marigot'],
['America/Martinique'],
['America/Matamoros'],
['America/Mazatlan'],
['America/Menominee'],
['America/Merida'],
['America/Mexico_City'],
['America/Miquelon'],
['America/Moncton'],
['America/Monterrey'],
['America/Montevideo'],
['America/Montreal'],
['America/Montserrat'],
['America/Nassau'],
['America/New_York'],
['America/Nipigon'],
['America/Nome'],
['America/Noronha'],
['America/North_Dakota/Center'],
['America/North_Dakota/New_Salem'],
['America/Ojinaga'],
['America/Panama'],
['America/Pangnirtung'],
['America/Paramaribo'],
['America/Phoenix'],
['America/Port-au-Prince'],
['America/Port_of_Spain'],
['America/Porto_Velho'],
['America/Puerto_Rico'],
['America/Rainy_River'],
['America/Rankin_Inlet'],
['America/Recife'],
['America/Regina'],
['America/Resolute'],
['America/Rio_Branco'],
['America/Santa_Isabel'],
['America/Santarem'],
['America/Santiago'],
['America/Santo_Domingo'],
['America/Sao_Paulo'],
['America/Scoresbysund'],
['America/Shiprock'],
['America/St_Barthelemy'],
['America/St_Johns'],
['America/St_Kitts'],
['America/St_Lucia'],
['America/St_Thomas'],
['America/St_Vincent'],
['America/Swift_Current'],
['America/Tegucigalpa'],
['America/Thule'],
['America/Thunder_Bay'],
['America/Tijuana'],
['America/Toronto'],
['America/Tortola'],
['America/Vancouver'],
['America/Whitehorse'],
['America/Winnipeg'],
['America/Yakutat'],
['America/Yellowknife'],
['Antarctica/Casey'],
['Antarctica/Davis'],
['Antarctica/DumontDUrville'],
['Antarctica/Macquarie'],
['Antarctica/Mawson'],
['Antarctica/McMurdo'],
['Antarctica/Palmer'],
['Antarctica/Rothera'],
['Antarctica/South_Pole'],
['Antarctica/Syowa'],
['Antarctica/Vostok'],
['Arctic/Longyearbyen'],
['Asia/Aden'],
['Asia/Almaty'],
['Asia/Amman'],
['Asia/Anadyr'],
['Asia/Aqtau'],
['Asia/Aqtobe'],
['Asia/Ashgabat'],
['Asia/Baghdad'],
['Asia/Bahrain'],
['Asia/Baku'],
['Asia/Bangkok'],
['Asia/Beirut'],
['Asia/Bishkek'],
['Asia/Brunei'],
['Asia/Choibalsan'],
['Asia/Chongqing'],
['Asia/Colombo'],
['Asia/Damascus'],
['Asia/Dhaka'],
['Asia/Dili'],
['Asia/Dubai'],
['Asia/Dushanbe'],
['Asia/Gaza'],
['Asia/Harbin'],
['Asia/Ho_Chi_Minh'],
['Asia/Hong_Kong'],
['Asia/Hovd'],
['Asia/Irkutsk'],
['Asia/Jakarta'],
['Asia/Jayapura'],
['Asia/Jerusalem'],
['Asia/Kabul'],
['Asia/Kamchatka'],
['Asia/Karachi'],
['Asia/Kashgar'],
['Asia/Kathmandu'],
['Asia/Kolkata'],
['Asia/Krasnoyarsk'],
['Asia/Kuala_Lumpur'],
['Asia/Kuching'],
['Asia/Kuwait'],
['Asia/Macau'],
['Asia/Magadan'],
['Asia/Makassar'],
['Asia/Manila'],
['Asia/Muscat'],
['Asia/Nicosia'],
['Asia/Novokuznetsk'],
['Asia/Novosibirsk'],
['Asia/Omsk'],
['Asia/Oral'],
['Asia/Phnom_Penh'],
['Asia/Pontianak'],
['Asia/Pyongyang'],
['Asia/Qatar'],
['Asia/Qyzylorda'],
['Asia/Rangoon'],
['Asia/Riyadh'],
['Asia/Sakhalin'],
['Asia/Samarkand'],
['Asia/Seoul'],
['Asia/Shanghai'],
['Asia/Singapore'],
['Asia/Taipei'],
['Asia/Tashkent'],
['Asia/Tbilisi'],
['Asia/Tehran'],
['Asia/Thimphu'],
['Asia/Tokyo'],
['Asia/Ulaanbaatar'],
['Asia/Urumqi'],
['Asia/Vientiane'],
['Asia/Vladivostok'],
['Asia/Yakutsk'],
['Asia/Yekaterinburg'],
['Asia/Yerevan'],
['Atlantic/Azores'],
['Atlantic/Bermuda'],
['Atlantic/Canary'],
['Atlantic/Cape_Verde'],
['Atlantic/Faroe'],
['Atlantic/Madeira'],
['Atlantic/Reykjavik'],
['Atlantic/South_Georgia'],
['Atlantic/St_Helena'],
['Atlantic/Stanley'],
['Australia/Adelaide'],
['Australia/Brisbane'],
['Australia/Broken_Hill'],
['Australia/Currie'],
['Australia/Darwin'],
['Australia/Eucla'],
['Australia/Hobart'],
['Australia/Lindeman'],
['Australia/Lord_Howe'],
['Australia/Melbourne'],
['Australia/Perth'],
['Australia/Sydney'],
['Europe/Amsterdam'],
['Europe/Andorra'],
['Europe/Athens'],
['Europe/Belgrade'],
['Europe/Berlin'],
['Europe/Bratislava'],
['Europe/Brussels'],
['Europe/Bucharest'],
['Europe/Budapest'],
['Europe/Chisinau'],
['Europe/Copenhagen'],
['Europe/Dublin'],
['Europe/Gibraltar'],
['Europe/Guernsey'],
['Europe/Helsinki'],
['Europe/Isle_of_Man'],
['Europe/Istanbul'],
['Europe/Jersey'],
['Europe/Kaliningrad'],
['Europe/Kiev'],
['Europe/Lisbon'],
['Europe/Ljubljana'],
['Europe/London'],
['Europe/Luxembourg'],
['Europe/Madrid'],
['Europe/Malta'],
['Europe/Mariehamn'],
['Europe/Minsk'],
['Europe/Monaco'],
['Europe/Moscow'],
['Europe/Oslo'],
['Europe/Paris'],
['Europe/Podgorica'],
['Europe/Prague'],
['Europe/Riga'],
['Europe/Rome'],
['Europe/Samara'],
['Europe/San_Marino'],
['Europe/Sarajevo'],
['Europe/Simferopol'],
['Europe/Skopje'],
['Europe/Sofia'],
['Europe/Stockholm'],
['Europe/Tallinn'],
['Europe/Tirane'],
['Europe/Uzhgorod'],
['Europe/Vaduz'],
['Europe/Vatican'],
['Europe/Vienna'],
['Europe/Vilnius'],
['Europe/Volgograd'],
['Europe/Warsaw'],
['Europe/Zagreb'],
['Europe/Zaporozhye'],
['Europe/Zurich'],
['Indian/Antananarivo'],
['Indian/Chagos'],
['Indian/Christmas'],
['Indian/Cocos'],
['Indian/Comoro'],
['Indian/Kerguelen'],
['Indian/Mahe'],
['Indian/Maldives'],
['Indian/Mauritius'],
['Indian/Mayotte'],
['Indian/Reunion'],
['Pacific/Apia'],
['Pacific/Auckland'],
['Pacific/Chatham'],
['Pacific/Chuuk'],
['Pacific/Easter'],
['Pacific/Efate'],
['Pacific/Enderbury'],
['Pacific/Fakaofo'],
['Pacific/Fiji'],
['Pacific/Funafuti'],
['Pacific/Galapagos'],
['Pacific/Gambier'],
['Pacific/Guadalcanal'],
['Pacific/Guam'],
['Pacific/Honolulu'],
['Pacific/Johnston'],
['Pacific/Kiritimati'],
['Pacific/Kosrae'],
['Pacific/Kwajalein'],
['Pacific/Majuro'],
['Pacific/Marquesas'],
['Pacific/Midway'],
['Pacific/Nauru'],
['Pacific/Niue'],
['Pacific/Norfolk'],
['Pacific/Noumea'],
['Pacific/Pago_Pago'],
['Pacific/Palau'],
['Pacific/Pitcairn'],
['Pacific/Pohnpei'],
['Pacific/Port_Moresby'],
['Pacific/Rarotonga'],
['Pacific/Saipan'],
['Pacific/Tahiti'],
['Pacific/Tarawa'],
['Pacific/Tongatapu'],
['Pacific/Wake'],
['Pacific/Wallis']
]
});
Ext.define('Proxmox.form.field.Integer',{
extend: 'Ext.form.field.Number',
alias: 'widget.proxmoxintegerfield',
config: {
deleteEmpty: false
},
allowDecimals: false,
allowExponential: false,
step: 1,
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== undefined && val !== null && val !== '') {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data['delete'] = me.getName();
}
}
return data;
}
});
Ext.define('Proxmox.form.field.Textfield', {
extend: 'Ext.form.field.Text',
alias: ['widget.proxmoxtextfield'],
config: {
skipEmptyText: true,
deleteEmpty: false,
},
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data['delete'] = me.getName();
}
}
return data;
},
getSubmitValue: function() {
var me = this;
var value = this.processRawValue(this.getRawValue());
if (value !== '') {
return value;
}
return me.getSkipEmptyText() ? null: value;
},
setAllowBlank: function(allowBlank) {
this.allowBlank = allowBlank;
}
});
Ext.define('Proxmox.DateTimeField', {
extend: 'Ext.form.FieldContainer',
xtype: 'promxoxDateTimeField',
layout: 'hbox',
referenceHolder: true,
submitFormat: 'U',
getValue: function() {
var me = this;
var d = me.lookupReference('dateentry').getValue();
if (d === undefined || d === null) { return null; }
var t = me.lookupReference('timeentry').getValue();
if (t === undefined || t === null) { return null; }
var offset = (t.getHours()*3600+t.getMinutes()*60)*1000;
return new Date(d.getTime() + offset);
},
getSubmitValue: function() {
var me = this;
var format = me.submitFormat;
var value = me.getValue();
return value ? Ext.Date.format(value, format) : null;
},
items: [
{
xtype: 'datefield',
editable: false,
reference: 'dateentry',
flex: 1,
format: 'Y-m-d'
},
{
xtype: 'timefield',
reference: 'timeentry',
format: 'H:i',
width: 80,
value: '00:00',
increment: 60
}
],
initComponent: function() {
var me = this;
me.callParent();
var value = me.value || new Date();
me.lookupReference('dateentry').setValue(value);
me.lookupReference('timeentry').setValue(value);
me.relayEvents(me.lookupReference('dateentry'), ['change']);
me.relayEvents(me.lookupReference('timeentry'), ['change']);
}
});
Ext.define('Proxmox.form.Checkbox', {
extend: 'Ext.form.field.Checkbox',
alias: ['widget.proxmoxcheckbox'],
config: {
defaultValue: undefined,
deleteDefaultValue: false,
deleteEmpty: false
},
inputValue: '1',
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
if ((val == me.getDefaultValue()) && me.getDeleteDefaultValue()) {
data['delete'] = me.getName();
} else {
data[me.getName()] = val;
}
} else if (me.getDeleteEmpty()) {
data = {};
data['delete'] = me.getName();
}
}
return data;
},
// also accept integer 1 as true
setRawValue: function(value) {
var me = this;
if (value === 1) {
me.callParent([true]);
} else {
me.callParent([value]);
}
}
});
/* Key-Value ComboBox
*
* config properties:
* comboItems: an array of Key - Value pairs
* deleteEmpty: if set to true (default), an empty value received from the
* comboBox will reset the property to its default value
*/
Ext.define('Proxmox.form.KVComboBox', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.proxmoxKVComboBox',
config: {
deleteEmpty: true
},
comboItems: undefined,
displayField: 'value',
valueField: 'key',
queryMode: 'local',
// overide framework function to implement deleteEmpty behaviour
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue) {
val = me.getSubmitValue();
if (val !== null && val !== '' && val !== '__default__') {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data['delete'] = me.getName();
}
}
return data;
},
validator: function(val) {
var me = this;
if (me.editable || val === null || val === '') {
return true;
}
if (me.store.getCount() > 0) {
var values = me.multiSelect ? val.split(me.delimiter) : [val];
var items = me.store.getData().collect('value', 'data');
if (Ext.Array.every(values, function(value) {
return Ext.Array.contains(items, value);
})) {
return true;
}
}
// returns a boolean or string
/*jslint confusion: true */
return "value '" + val + "' not allowed!";
},
initComponent: function() {
var me = this;
me.store = Ext.create('Ext.data.ArrayStore', {
model: 'KeyValue',
data : me.comboItems
});
if (me.initialConfig.editable === undefined) {
me.editable = false;
}
me.callParent();
}
});
Ext.define('Proxmox.form.LanguageSelector', {
extend: 'Proxmox.form.KVComboBox',
xtype: 'proxmoxLanguageSelector',
comboItems: Proxmox.Utils.language_array()
});
/*
* ComboGrid component: a ComboBox where the dropdown menu (the
* "Picker") is a Grid with Rows and Columns expects a listConfig
* object with a columns property roughly based on the GridPicker from
* https://www.sencha.com/forum/showthread.php?299909
*
*/
Ext.define('Proxmox.form.ComboGrid', {
extend: 'Ext.form.field.ComboBox',
alias: ['widget.proxmoxComboGrid'],
// this value is used as default value after load()
preferredValue: undefined,
// hack: allow to select empty value
// seems extjs does not allow that when 'editable == false'
onKeyUp: function(e, t) {
var me = this;
var key = e.getKey();
if (!me.editable && me.allowBlank && !me.multiSelect &&
(key == e.BACKSPACE || key == e.DELETE)) {
me.setValue('');
}
me.callParent(arguments);
},
// needed to trigger onKeyUp etc.
enableKeyEvents: true,
editable: false,
// override ExtJS method
// if the field has multiSelect enabled, the store is not loaded, and
// the displayfield == valuefield, it saves the rawvalue as an array
// but the getRawValue method is only defined in the textfield class
// (which has not to deal with arrays) an returns the string in the
// field (not an array)
//
// so if we have multiselect enabled, return the rawValue (which
// should be an array) and else we do callParent so
// it should not impact any other use of the class
getRawValue: function() {
var me = this;
if (me.multiSelect) {
return me.rawValue;
} else {
return me.callParent();
}
},
// override ExtJS protected method
onBindStore: function(store, initial) {
var me = this,
picker = me.picker,
extraKeySpec,
valueCollectionConfig;
// We're being bound, not unbound...
if (store) {
// If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
if (store.autoCreated) {
me.queryMode = 'local';
me.valueField = me.displayField = 'field1';
if (!store.expanded) {
me.displayField = 'field2';
}
// displayTpl config will need regenerating with the autogenerated displayField name 'field1'
me.setDisplayTpl(null);
}
if (!Ext.isDefined(me.valueField)) {
me.valueField = me.displayField;
}
// Add a byValue index to the store so that we can efficiently look up records by the value field
// when setValue passes string value(s).
// The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
// are found, they are all returned by the get call.
// This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
// if unique is true, CollectionKey keeps the *last* matching value.
extraKeySpec = {
byValue: {
rootProperty: 'data',
unique: false
}
};
extraKeySpec.byValue.property = me.valueField;
store.setExtraKeys(extraKeySpec);
if (me.displayField === me.valueField) {
store.byText = store.byValue;
} else {
extraKeySpec.byText = {
rootProperty: 'data',
unique: false
};
extraKeySpec.byText.property = me.displayField;
store.setExtraKeys(extraKeySpec);
}
// We hold a collection of the values which have been selected, keyed by this field's valueField.
// This collection also functions as the selected items collection for the BoundList's selection model
valueCollectionConfig = {
rootProperty: 'data',
extraKeys: {
byInternalId: {
property: 'internalId'
},
byValue: {
property: me.valueField,
rootProperty: 'data'
}
},
// Whenever this collection is changed by anyone, whether by this field adding to it,
// or the BoundList operating, we must refresh our value.
listeners: {
beginupdate: me.onValueCollectionBeginUpdate,
endupdate: me.onValueCollectionEndUpdate,
scope: me
}
};
// This becomes our collection of selected records for the Field.
me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
// We use the selected Collection as our value collection and the basis
// for rendering the tag list.
//proxmox override: since the picker is represented by a grid panel,
// we changed here the selection to RowModel
me.pickerSelectionModel = new Ext.selection.RowModel({
mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
// There are situations when a row is selected on mousedown but then the mouse is dragged to another row
// and released. In these situations, the event target for the click event won't be the row where the mouse
// was released but the boundview. The view will then determine that it should fire a container click, and
// the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will
// prevent the model from deselecting.
deselectOnContainerClick: false,
enableInitialSelection: false,
pruneRemoved: false,
selected: me.valueCollection,
store: store,
listeners: {
scope: me,
lastselectedchanged: me.updateBindSelection
}
});
if (!initial) {
me.resetToDefault();
}
if (picker) {
picker.setSelectionModel(me.pickerSelectionModel);
if (picker.getStore() !== store) {
picker.bindStore(store);
}
}
}
},
// copied from ComboBox
createPicker: function() {
var me = this;
var picker;
var pickerCfg = Ext.apply({
// proxmox overrides: display a grid for selection
xtype: 'gridpanel',
id: me.pickerId,
pickerField: me,
floating: true,
hidden: true,
store: me.store,
displayField: me.displayField,
preserveScrollOnRefresh: true,
pageSize: me.pageSize,
tpl: me.tpl,
selModel: me.pickerSelectionModel,
focusOnToFront: false
}, me.listConfig, me.defaultListConfig);
picker = me.picker || Ext.widget(pickerCfg);
if (picker.getStore() !== me.store) {
picker.bindStore(me.store);
}
if (me.pageSize) {
picker.pagingToolbar.on('beforechange', me.onPageChange, me);
}
// proxmox overrides: pass missing method in gridPanel to its view
picker.refresh = function() {
picker.getSelectionModel().select(me.valueCollection.getRange());
picker.getView().refresh();
};
picker.getNodeByRecord = function() {
picker.getView().getNodeByRecord(arguments);
};
// We limit the height of the picker to fit in the space above
// or below this field unless the picker has its own ideas about that.
if (!picker.initialConfig.maxHeight) {
picker.on({
beforeshow: me.onBeforePickerShow,
scope: me
});
}
picker.getSelectionModel().on({
beforeselect: me.onBeforeSelect,
beforedeselect: me.onBeforeDeselect,
focuschange: me.onFocusChange,
selectionChange: function (sm, selectedRecords) {
var me = this;
if (selectedRecords.length) {
me.setValue(selectedRecords);
me.fireEvent('select', me, selectedRecords);
}
},
scope: me
});
// hack for extjs6
// when the clicked item is the same as the previously selected,
// it does not select the item
// instead we hide the picker
if (!me.multiSelect) {
picker.on('itemclick', function (sm,record) {
if (picker.getSelection()[0] === record) {
picker.hide();
}
});
}
// when our store is not yet loaded, we increase
// the height of the gridpanel, so that we can see
// the loading mask
//
// we save the minheight to reset it after the load
picker.on('show', function() {
if (me.enableLoadMask) {
me.savedMinHeight = picker.getMinHeight();
picker.setMinHeight(100);
}
});
picker.getNavigationModel().navigateOnSpace = false;
return picker;
},
initComponent: function() {
var me = this;
Ext.apply(me, {
queryMode: 'local',
matchFieldWidth: false
});
Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug
Ext.applyIf(me.listConfig, { width: 400 });
me.callParent();
// Create the picker at an early stage, so it is available to store the previous selection
if (!me.picker) {
me.createPicker();
}
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 listner 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(me) {
if (!me.isExpanded && !me.skip_expand_on_focus) {
me.expand();
}
me.skip_expand_on_focus = false;
});
}
me.mon(me.store, 'beforeload', function() {
if (!me.isDisabled()) {
me.enableLoadMask = true;
}
});
// hack: autoSelect does not work
me.mon(me.store, 'load', function(store, r, success, o) {
if (success) {
me.clearInvalid();
if (me.enableLoadMask) {
delete me.enableLoadMask;
// if the picker exists,
// we reset its minheight to the saved var/0
// we have to update the layout, otherwise the height
// gets not recalculated
if (me.picker) {
me.picker.setMinHeight(me.savedMinHeight || 0);
delete me.savedMinHeight;
me.picker.updateLayout();
}
}
var def = me.getValue() || me.preferredValue;
if (def) {
me.setValue(def, true); // sync with grid
}
var found = false;
if (def) {
if (Ext.isArray(def)) {
Ext.Array.each(def, function(v) {
if (store.findRecord(me.valueField, v)) {
found = true;
return false; // break
}
});
} else {
found = store.findRecord(me.valueField, def);
}
}
if (!found) {
var rec = me.store.first();
if (me.autoSelect && rec && rec.data) {
def = rec.data[me.valueField];
me.setValue(def, true);
} else {
me.setValue(me.editable ? def : '', true);
}
}
}
});
}
});
Ext.define('Proxmox.form.RRDTypeSelector', {
extend: 'Ext.form.field.ComboBox',
alias: ['widget.proxmoxRRDTypeSelector'],
displayField: 'text',
valueField: 'id',
editable: false,
queryMode: 'local',
value: 'hour',
stateEvents: [ 'select' ],
stateful: true,
stateId: 'proxmoxRRDTypeSelection',
store: {
type: 'array',
fields: [ 'id', 'timeframe', 'cf', 'text' ],
data : [
[ 'hour', 'hour', 'AVERAGE',
gettext('Hour') + ' (' + gettext('average') +')' ],
[ 'hourmax', 'hour', 'MAX',
gettext('Hour') + ' (' + gettext('maximum') + ')' ],
[ 'day', 'day', 'AVERAGE',
gettext('Day') + ' (' + gettext('average') + ')' ],
[ 'daymax', 'day', 'MAX',
gettext('Day') + ' (' + gettext('maximum') + ')' ],
[ 'week', 'week', 'AVERAGE',
gettext('Week') + ' (' + gettext('average') + ')' ],
[ 'weekmax', 'week', 'MAX',
gettext('Week') + ' (' + gettext('maximum') + ')' ],
[ 'month', 'month', 'AVERAGE',
gettext('Month') + ' (' + gettext('average') + ')' ],
[ 'monthmax', 'month', 'MAX',
gettext('Month') + ' (' + gettext('maximum') + ')' ],
[ 'year', 'year', 'AVERAGE',
gettext('Year') + ' (' + gettext('average') + ')' ],
[ 'yearmax', 'year', 'MAX',
gettext('Year') + ' (' + gettext('maximum') + ')' ]
]
},
// save current selection in the state Provider so RRDView can read it
getState: function() {
var ind = this.getStore().findExact('id', this.getValue());
var rec = this.getStore().getAt(ind);
if (!rec) {
return;
}
return {
id: rec.data.id,
timeframe: rec.data.timeframe,
cf: rec.data.cf
};
},
// set selection based on last saved state
applyState : function(state) {
if (state && state.id) {
this.setValue(state.id);
}
}
});
Ext.define('Proxmox.form.BondModeSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.bondModeSelector'],
openvswitch: false,
initComponent: function() {
var me = this;
if (me.openvswitch) {
me.comboItems = [
['active-backup', 'active-backup'],
['balance-slb', 'balance-slb'],
['lacp-balance-slb', 'LACP (balance-slb)'],
['lacp-balance-tcp', 'LACP (balance-tcp)']
];
} else {
me.comboItems = [
['balance-rr', 'balance-rr'],
['active-backup', 'active-backup'],
['balance-xor', 'balance-xor'],
['broadcast', 'broadcast'],
['802.3ad', 'LACP (802.3ad)'],
['balance-tlb', 'balance-tlb'],
['balance-alb', 'balance-alb']
];
}
me.callParent();
}
});
Ext.define('Proxmox.form.BondPolicySelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.bondPolicySelector'],
comboItems: [
['layer2', 'layer2'],
['layer2+3', 'layer2+3'],
['layer3+4', 'layer3+4']
]
});
/* Button features:
* - observe selection changes to enable/disable the button using enableFn()
* - pop up confirmation dialog using confirmMsg()
*/
Ext.define('Proxmox.button.Button', {
extend: 'Ext.button.Button',
alias: 'widget.proxmoxButton',
// the selection model to observe
selModel: undefined,
// if 'false' handler will not be called (button disabled)
enableFn: function(record) { },
// function(record) or text
confirmMsg: false,
// take special care in confirm box (select no as default).
dangerous: false,
initComponent: function() {
/*jslint confusion: true */
var me = this;
if (me.handler) {
// Note: me.realHandler may be a string (see named scopes)
var realHandler = me.handler;
me.handler = function(button, event) {
var rec, msg;
if (me.selModel) {
rec = me.selModel.getSelection()[0];
if (!rec || (me.enableFn(rec) === false)) {
return;
}
}
if (me.confirmMsg) {
msg = me.confirmMsg;
if (Ext.isFunction(me.confirmMsg)) {
msg = me.confirmMsg(rec);
}
Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
Ext.Msg.show({
title: gettext('Confirm'),
icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
msg: msg,
buttons: Ext.Msg.YESNO,
defaultFocus: me.dangerous ? 'no' : 'yes',
callback: function(btn) {
if (btn !== 'yes') {
return;
}
Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
}
});
} else {
Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
}
};
}
me.callParent();
var grid;
if (!me.selModel && me.selModel !== null) {
grid = me.up('grid');
if (grid && grid.selModel) {
me.selModel = grid.selModel;
}
}
if (me.waitMsgTarget === true) {
grid = me.up('grid');
if (grid) {
me.waitMsgTarget = grid;
} else {
throw "unable to find waitMsgTarget";
}
}
if (me.selModel) {
me.mon(me.selModel, "selectionchange", function() {
var rec = me.selModel.getSelection()[0];
if (!rec || (me.enableFn(rec) === false)) {
me.setDisabled(true);
} else {
me.setDisabled(false);
}
});
}
}
});
Ext.define('Proxmox.button.StdRemoveButton', {
extend: 'Proxmox.button.Button',
alias: 'widget.proxmoxStdRemoveButton',
text: gettext('Remove'),
disabled: true,
config: {
baseurl: undefined
},
getUrl: function(rec) {
var me = this;
return me.baseurl + '/' + rec.getId();
},
// also works with names scopes
callback: function(options, success, response) {},
getRecordName: function(rec) { return rec.getId() },
confirmMsg: function (rec) {
var me = this;
var name = me.getRecordName(rec);
return Ext.String.format(
gettext('Are you sure you want to remove entry {0}'),
"'" + name + "'");
},
handler: function(btn, event, rec) {
var me = this;
Proxmox.Utils.API2Request({
url: me.getUrl(rec),
method: 'DELETE',
waitMsgTarget: me.waitMsgTarget,
callback: function(options, success, response) {
Ext.callback(me.callback, me.scope, [options, success, response], 0, me);
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
});
/* help button pointing to an online documentation
for components contained in a modal window
*/
/*global
proxmoxOnlineHelpInfo
*/
Ext.define('Proxmox.button.Help', {
extend: 'Ext.button.Button',
xtype: 'proxmoxHelpButton',
text: gettext('Help'),
// make help button less flashy by styling it like toolbar buttons
iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle',
cls: 'x-btn-default-toolbar-small proxmox-inline-button',
hidden: true,
listenToGlobalEvent: true,
controller: {
xclass: 'Ext.app.ViewController',
listen: {
global: {
proxmoxShowHelp: 'onProxmoxShowHelp',
proxmoxHideHelp: 'onProxmoxHideHelp'
}
},
onProxmoxShowHelp: function(helpLink) {
var me = this.getView();
if (me.listenToGlobalEvent === true) {
me.setOnlineHelp(helpLink);
me.show();
}
},
onProxmoxHideHelp: function() {
var me = this.getView();
if (me.listenToGlobalEvent === true) {
me.hide();
}
}
},
getOnlineHelpInfo: function (ref) {
var helpMap;
if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
helpMap = proxmoxOnlineHelpInfo;
} else if (typeof pveOnlineHelpInfo !== 'undefined') {
// be backward compatible with older pve-doc-generators
helpMap = pveOnlineHelpInfo;
} else {
throw "no global OnlineHelpInfo map declared";
}
return helpMap[ref];
},
// this sets the link and the tooltip text
setOnlineHelp:function(blockid) {
var me = this;
var info = me.getOnlineHelpInfo(blockid);
if (info) {
me.onlineHelp = blockid;
var title = info.title;
if (info.subtitle) {
title += ' - ' + info.subtitle;
}
me.setTooltip(title);
}
},
// helper to set the onlineHelp via a config object
setHelpConfig: function(config) {
var me = this;
me.setOnlineHelp(config.onlineHelp);
},
handler: function() {
var me = this;
var docsURI;
if (me.onlineHelp) {
var info = me.getOnlineHelpInfo(me.onlineHelp);
if (info) {
docsURI = window.location.origin + info.link;
}
}
if (docsURI) {
window.open(docsURI);
} else {
Ext.Msg.alert(gettext('Help'), gettext('No Help available'));
}
},
initComponent: function() {
/*jslint confusion: true */
var me = this;
me.callParent();
if (me.onlineHelp) {
me.setOnlineHelp(me.onlineHelp); // set tooltip
}
}
});
/* Renders a list of key values objets
mandatory config parameters:
rows: an object container where each propery is a key-value object we want to render
var rows = {
keyboard: {
header: gettext('Keyboard Layout'),
editor: 'Your.KeyboardEdit',
required: true
},
optional:
disabled: setting this parameter to true will disable selection and focus on the
proxmoxObjectGrid as well as greying out input elements.
Useful for a readonly tabular display
*/
Ext.define('Proxmox.grid.ObjectGrid', {
extend: 'Ext.grid.GridPanel',
alias: ['widget.proxmoxObjectGrid'],
disabled: false,
hideHeaders: true,
monStoreErrors: false,
add_combobox_row: function(name, text, opts) {
var me = this;
opts = opts || {};
me.rows = me.rows || {};
me.rows[name] = {
required: true,
defaultValue: opts.defaultValue,
header: text,
renderer: opts.renderer,
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 100
},
items: {
xtype: 'proxmoxKVComboBox',
name: name,
comboItems: opts.comboItems,
value: opts.defaultValue,
deleteEmpty: opts.deleteEmpty ? true : false,
emptyText: opts.defaultValue,
labelWidth: Proxmox.Utils.compute_min_label_width(
text, opts.labelWidth),
fieldLabel: text
}
}
};
},
add_text_row: function(name, text, opts) {
var me = this;
opts = opts || {};
me.rows = me.rows || {};
me.rows[name] = {
required: true,
defaultValue: opts.defaultValue,
header: text,
renderer: opts.renderer,
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 100
},
items: {
xtype: 'proxmoxtextfield',
name: name,
deleteEmpty: opts.deleteEmpty ? true : false,
emptyText: opts.defaultValue,
labelWidth: Proxmox.Utils.compute_min_label_width(
text, opts.labelWidth),
vtype: opts.vtype,
fieldLabel: text
}
}
};
},
add_boolean_row: function(name, text, opts) {
var me = this;
opts = opts || {};
me.rows = me.rows || {};
me.rows[name] = {
required: true,
defaultValue: opts.defaultValue || 0,
header: text,
renderer: opts.renderer || Proxmox.Utils.format_boolean,
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 100
},
items: {
xtype: 'proxmoxcheckbox',
name: name,
uncheckedValue: 0,
defaultValue: opts.defaultValue || 0,
checked: opts.defaultValue ? true : false,
deleteDefaultValue: opts.deleteDefaultValue ? true : false,
labelWidth: Proxmox.Utils.compute_min_label_width(
text, opts.labelWidth),
fieldLabel: text
}
}
};
},
add_integer_row: function(name, text, opts) {
var me = this;
opts = opts || {}
me.rows = me.rows || {};
me.rows[name] = {
required: true,
defaultValue: opts.defaultValue,
header: text,
renderer: opts.renderer,
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 100
},
items: {
xtype: 'proxmoxintegerfield',
name: name,
minValue: opts.minValue,
maxValue: opts.maxValue,
emptyText: gettext('Default'),
deleteEmpty: opts.deleteEmpty ? true : false,
value: opts.defaultValue,
labelWidth: Proxmox.Utils.compute_min_label_width(
text, opts.labelWidth),
fieldLabel: text
}
}
};
},
editorConfig: {}, // default config passed to editor
run_editor: function() {
var me = this;
var sm = me.getSelectionModel();
var rec = sm.getSelection()[0];
if (!rec) {
return;
}
var rows = me.rows;
var rowdef = rows[rec.data.key];
if (!rowdef.editor) {
return;
}
var win;
var config;
if (Ext.isString(rowdef.editor)) {
config = Ext.apply({
confid: rec.data.key,
}, me.editorConfig);
win = Ext.create(rowdef.editor, config);
} else {
config = Ext.apply({
confid: rec.data.key,
}, me.editorConfig);
Ext.apply(config, rowdef.editor);
win = Ext.createWidget(rowdef.editor.xtype, config);
win.load();
}
win.show();
win.on('destroy', me.reload, me);
},
reload: function() {
var me = this;
me.rstore.load();
},
getObjectValue: function(key, defaultValue) {
var me = this;
var rec = me.store.getById(key);
if (rec) {
return rec.data.value;
}
return defaultValue;
},
renderKey: function(key, metaData, record, rowIndex, colIndex, store) {
var me = this;
var rows = me.rows;
var rowdef = (rows && rows[key]) ? rows[key] : {};
return rowdef.header || key;
},
renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
var me = this;
var rows = me.rows;
var key = record.data.key;
var rowdef = (rows && rows[key]) ? rows[key] : {};
var renderer = rowdef.renderer;
if (renderer) {
return renderer(value, metaData, record, rowIndex, colIndex, store);
}
return value;
},
listeners: {
itemkeydown: function(view, record, item, index, e) {
if (e.getKey() === e.ENTER) {
this.pressedIndex = index;
}
},
itemkeyup: function(view, record, item, index, e) {
if (e.getKey() === e.ENTER && index == this.pressedIndex) {
this.run_editor();
}
this.pressedIndex = undefined;
}
},
initComponent : function() {
var me = this;
var rows = me.rows;
if (!me.rstore) {
if (!me.url) {
throw "no url specified";
}
me.rstore = Ext.create('Proxmox.data.ObjectStore', {
url: me.url,
interval: me.interval,
extraParams: me.extraParams,
rows: me.rows
});
}
var rstore = me.rstore;
var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore,
sorters: [],
filters: []
});
if (rows) {
Ext.Object.each(rows, function(key, rowdef) {
if (Ext.isDefined(rowdef.defaultValue)) {
store.add({ key: key, value: rowdef.defaultValue });
} else if (rowdef.required) {
store.add({ key: key, value: undefined });
}
});
}
if (me.sorterFn) {
store.sorters.add(Ext.create('Ext.util.Sorter', {
sorterFn: me.sorterFn
}));
}
store.filters.add(Ext.create('Ext.util.Filter', {
filterFn: function(item) {
if (rows) {
var rowdef = rows[item.data.key];
if (!rowdef || (rowdef.visible === false)) {
return false;
}
}
return true;
}
}));
Proxmox.Utils.monStoreErrors(me, rstore);
Ext.applyIf(me, {
store: store,
stateful: false,
columns: [
{
header: gettext('Name'),
width: me.cwidth1 || 200,
dataIndex: 'key',
renderer: me.renderKey
},
{
flex: 1,
header: gettext('Value'),
dataIndex: 'value',
renderer: me.renderValue
}
]
});
me.callParent();
if (me.monStoreErrors) {
Proxmox.Utils.monStoreErrors(me, me.store);
}
}
});
Ext.define('Proxmox.grid.PendingObjectGrid', {
extend: 'Proxmox.grid.ObjectGrid',
alias: ['widget.proxmoxPendingObjectGrid'],
getObjectValue: function(key, defaultValue, pending) {
var me = this;
var rec = me.store.getById(key);
if (rec) {
var value = rec.data.value;
if (pending) {
if (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') {
value = rec.data.pending;
} else if (rec.data['delete'] === 1) {
value = defaultValue;
}
}
if (Ext.isDefined(value) && (value !== '')) {
return value;
} else {
return defaultValue;
}
}
return defaultValue;
},
hasPendingChanges: function(key) {
var me = this;
var rows = me.rows;
var rowdef = (rows && rows[key]) ? rows[key] : {};
var keys = rowdef.multiKey || [ key ];
var pending = false;
Ext.Array.each(keys, function(k) {
var rec = me.store.getById(k);
if (rec && rec.data && (
(Ext.isDefined(rec.data.pending) && rec.data.pending !== '') ||
rec.data['delete'] === 1
)) {
pending = true;
return false; // break
}
});
return pending;
},
renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
var me = this;
var rows = me.rows;
var key = record.data.key;
var rowdef = (rows && rows[key]) ? rows[key] : {};
var renderer = rowdef.renderer;
var current = '';
var pendingdelete = '';
var pending = '';
if (renderer) {
current = renderer(value, metaData, record, rowIndex, colIndex, store, false);
if (me.hasPendingChanges(key)) {
pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true);
}
if (pending == current) {
pending = undefined;
}
} else {
current = value || '';
pending = record.data.pending;
}
if (record.data['delete']) {
var delete_all = true;
if (rowdef.multiKey) {
Ext.Array.each(rowdef.multiKey, function(k) {
var rec = me.store.getById(k);
if (rec && rec.data && rec.data['delete'] !== 1) {
delete_all = false;
return false; // break
}
});
}
if (delete_all) {
pending = '
'+ current +'
';
}
}
if (pending) {
return current + '' + pending + '
';
} else {
return current;
}
},
initComponent : function() {
var me = this;
var rows = me.rows;
if (!me.rstore) {
if (!me.url) {
throw "no url specified";
}
me.rstore = Ext.create('Proxmox.data.ObjectStore', {
model: 'KeyValuePendingDelete',
readArray: true,
url: me.url,
interval: me.interval,
extraParams: me.extraParams,
rows: me.rows
});
}
me.callParent();
}
});
Ext.define('Proxmox.panel.InputPanel', {
extend: 'Ext.panel.Panel',
alias: ['widget.inputpanel'],
listeners: {
activate: function() {
// notify owning container that it should display a help button
if (this.onlineHelp) {
Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
}
},
deactivate: function() {
if (this.onlineHelp) {
Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
}
}
},
border: false,
// override this with an URL to a relevant chapter of the pve manual
// setting this will display a help button in our parent panel
onlineHelp: undefined,
// will be set if the inputpanel has advanced items
hasAdvanced: false,
// if the panel has advanced items,
// this will determine if they are shown by default
showAdvanced: false,
// overwrite this to modify submit data
onGetValues: function(values) {
return values;
},
getValues: function(dirtyOnly) {
var me = this;
if (Ext.isFunction(me.onGetValues)) {
dirtyOnly = false;
}
var values = {};
Ext.Array.each(me.query('[isFormField]'), function(field) {
if (!dirtyOnly || field.isDirty()) {
Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
}
});
return me.onGetValues(values);
},
setAdvancedVisible: function(visible) {
var me = this;
var advItems = me.getComponent('advancedContainer');
if (advItems) {
advItems.setVisible(visible);
}
},
setValues: function(values) {
var me = this;
var form = me.up('form');
Ext.iterate(values, function(fieldId, val) {
var field = me.query('[isFormField][name=' + fieldId + ']')[0];
if (field) {
field.setValue(val);
if (form.trackResetOnLoad) {
field.resetOriginalValue();
}
}
});
},
initComponent: function() {
var me = this;
var items;
if (me.items) {
me.columns = 1;
items = [
{
columnWidth: 1,
layout: 'anchor',
items: me.items
}
];
me.items = undefined;
} else if (me.column4) {
me.columns = 4;
items = [
{
columnWidth: 0.25,
padding: '0 10 0 0',
layout: 'anchor',
items: me.column1
},
{
columnWidth: 0.25,
padding: '0 10 0 0',
layout: 'anchor',
items: me.column2
},
{
columnWidth: 0.25,
padding: '0 10 0 0',
layout: 'anchor',
items: me.column3
},
{
columnWidth: 0.25,
padding: '0 0 0 10',
layout: 'anchor',
items: me.column4
}
];
if (me.columnB) {
items.push({
columnWidth: 1,
padding: '10 0 0 0',
layout: 'anchor',
items: me.columnB
});
}
} else if (me.column1) {
me.columns = 2;
items = [
{
columnWidth: 0.5,
padding: '0 10 0 0',
layout: 'anchor',
items: me.column1
},
{
columnWidth: 0.5,
padding: '0 0 0 10',
layout: 'anchor',
items: me.column2 || [] // allow empty column
}
];
if (me.columnB) {
items.push({
columnWidth: 1,
padding: '10 0 0 0',
layout: 'anchor',
items: me.columnB
});
}
} else {
throw "unsupported config";
}
var advItems;
if (me.advancedItems) {
advItems = [
{
columnWidth: 1,
layout: 'anchor',
items: me.advancedItems
}
];
me.advancedItems = undefined;
} else if (me.advancedColumn1) {
advItems = [
{
columnWidth: 0.5,
padding: '0 10 0 0',
layout: 'anchor',
items: me.advancedColumn1
},
{
columnWidth: 0.5,
padding: '0 0 0 10',
layout: 'anchor',
items: me.advancedColumn2 || [] // allow empty column
}
];
me.advancedColumn1 = undefined;
me.advancedColumn2 = undefined;
if (me.advancedColumnB) {
advItems.push({
columnWidth: 1,
padding: '10 0 0 0',
layout: 'anchor',
items: me.advancedColumnB
});
me.advancedColumnB = undefined;
}
}
if (advItems) {
me.hasAdvanced = true;
advItems.unshift({
columnWidth: 1,
xtype: 'box',
hidden: false,
border: true,
autoEl: {
tag: 'hr'
}
});
items.push({
columnWidth: 1,
xtype: 'container',
itemId: 'advancedContainer',
hidden: !me.showAdvanced,
layout: 'column',
defaults: {
border: false
},
items: advItems
});
}
if (me.useFieldContainer) {
Ext.apply(me, {
layout: 'fit',
items: Ext.apply(me.useFieldContainer, {
layout: 'column',
defaultType: 'container',
items: items
})
});
} else {
Ext.apply(me, {
layout: 'column',
defaultType: 'container',
items: items
});
}
me.callParent();
}
});
/*
* Display log entries in a panel with scrollbar
* The log entries are automatically refreshed via a background task,
* with newest entries comming at the bottom
*/
Ext.define('Proxmox.panel.LogView', {
extend: 'Ext.panel.Panel',
xtype: 'proxmoxLogView',
pageSize: 500,
viewBuffer: 50,
lineHeight: 16,
scrollToEnd: true,
// callback for load failure, used for ceph
failCallback: undefined,
controller: {
xclass: 'Ext.app.ViewController',
updateParams: function() {
var me = this;
var viewModel = me.getViewModel();
var since = viewModel.get('since');
var until = viewModel.get('until');
if (viewModel.get('hide_timespan')) {
return;
}
if (since > until) {
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
return;
}
viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
me.getView().loadTask.delay(200);
},
scrollPosBottom: function() {
var view = this.getView();
var pos = view.getScrollY();
var maxPos = view.getScrollable().getMaxPosition().y;
return maxPos - pos;
},
updateView: function(text, first, total) {
var me = this;
var view = me.getView();
var viewModel = me.getViewModel();
var content = me.lookup('content');
var data = viewModel.get('data');
if (first === data.first && total === data.total && text.length === data.textlen) {
return; // same content, skip setting and scrolling
}
viewModel.set('data', {
first: first,
total: total,
textlen: text.length
});
var scrollPos = me.scrollPosBottom();
content.update(text);
if (view.scrollToEnd && scrollPos <= 0) {
// we use setTimeout to work around scroll handling on touchscreens
setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
}
},
doLoad: function() {
var me = this;
var view = me.getView();
var viewModel = me.getViewModel();
Proxmox.Utils.API2Request({
url: me.getView().url,
params: viewModel.get('params'),
method: 'GET',
success: function(response) {
Proxmox.Utils.setErrorMask(me, false);
var total = response.result.total;
var lines = new Array();
var first = Infinity;
Ext.Array.each(response.result.data, function(line) {
if (first > line.n) {
first = line.n;
}
lines[line.n - 1] = Ext.htmlEncode(line.t);
});
lines.length = total;
me.updateView(lines.join('
'), first - 1, total);
},
failure: function(response) {
if (view.failCallback) {
view.failCallback(response);
} else {
var msg = response.htmlStatus;
Proxmox.Utils.setErrorMask(me, msg);
}
}
});
},
onScroll: function(x, y) {
var me = this;
var view = me.getView();
var viewModel = me.getViewModel();
var lineHeight = view.lineHeight;
var line = view.getScrollY()/lineHeight;
var start = viewModel.get('params.start');
var limit = viewModel.get('params.limit');
var viewLines = view.getHeight()/lineHeight;
var viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
var viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
if (viewStart < start || viewEnd > (start+limit)) {
viewModel.set('params.start',
Math.max(parseInt(line - limit/2 + 10, 10), 0));
view.loadTask.delay(200);
}
},
init: function(view) {
var me = this;
if (!view.url) {
throw "no url specified";
}
var viewModel = this.getViewModel();
var since = new Date();
since.setDate(since.getDate() - 3);
viewModel.set('until', new Date());
viewModel.set('since', since);
viewModel.set('params.limit', view.pageSize);
viewModel.set('hide_timespan', !view.log_select_timespan);
me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
me.updateParams();
view.task = Ext.TaskManager.start({
run: function() {
if (!view.isVisible() || !view.scrollToEnd) {
return;
}
if (me.scrollPosBottom() <= 1) {
view.loadTask.delay(200);
}
},
interval: 1000
});
}
},
onDestroy: function() {
var me = this;
me.loadTask.cancel();
Ext.TaskManager.stop(me.task);
},
// for user to initiate a load from outside
requestUpdate: function() {
var me = this;
me.loadTask.delay(200);
},
viewModel: {
data: {
until: null,
since: null,
hide_timespan: false,
data: {
start: 0,
total: 0,
textlen: 0
},
params: {
start: 0,
limit: 500,
}
}
},
layout: 'auto',
bodyPadding: 5,
scrollable: {
x: 'auto',
y: 'auto',
listeners: {
// we have to have this here, since we cannot listen to events
// of the scroller in the viewcontroller (extjs bug?), nor does
// the panel have a 'scroll' event'
scroll: {
fn: function(scroller, x, y) {
var controller = this.component.getController();
if (controller) { // on destroy, controller can be gone
controller.onScroll(x,y);
}
},
buffer: 200
},
}
},
tbar: {
bind: {
hidden: '{hide_timespan}'
},
items: [
'->',
'Since: ',
{
xtype: 'datefield',
name: 'since_date',
reference: 'since',
format: 'Y-m-d',
bind: {
value: '{since}',
maxValue: '{until}'
}
},
'Until: ',
{
xtype: 'datefield',
name: 'until_date',
reference: 'until',
format: 'Y-m-d',
bind: {
value: '{until}',
minValue: '{since}'
}
},
{
xtype: 'button',
text: 'Update',
handler: 'updateParams'
}
],
},
items: [
{
xtype: 'box',
reference: 'content',
style: {
font: 'normal 11px tahoma, arial, verdana, sans-serif',
'white-space': 'pre'
},
}
]
});
Ext.define('Proxmox.widget.RRDChart', {
extend: 'Ext.chart.CartesianChart',
alias: 'widget.proxmoxRRDChart',
unit: undefined, // bytes, bytespersecond, percent
controller: {
xclass: 'Ext.app.ViewController',
convertToUnits: function(value) {
var units = ['', 'k','M','G','T', 'P'];
var si = 0;
while(value >= 1000 && si < (units.length -1)){
value = value / 1000;
si++;
}
// javascript floating point weirdness
value = Ext.Number.correctFloat(value);
// limit to 2 decimal points
value = Ext.util.Format.number(value, "0.##");
return value.toString() + " " + units[si];
},
leftAxisRenderer: function(axis, label, layoutContext) {
var me = this;
return me.convertToUnits(label);
},
onSeriesTooltipRender: function(tooltip, record, item) {
var me = this.getView();
var suffix = '';
if (me.unit === 'percent') {
suffix = '%';
} else if (me.unit === 'bytes') {
suffix = 'B';
} else if (me.unit === 'bytespersecond') {
suffix = 'B/s';
}
var prefix = item.field;
if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) {
prefix = me.fieldTitles[me.fields.indexOf(item.field)];
}
tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix +
'
' + new Date(record.get('time')));
},
onAfterAnimation: function(chart, eopts) {
// if the undobuton is disabled,
// disable our tool
var ourUndoZoomButton = chart.tools[0];
var undoButton = chart.interactions[0].getUndoButton();
ourUndoZoomButton.setDisabled(undoButton.isDisabled());
}
},
width: 770,
height: 300,
animation: false,
interactions: [{
type: 'crosszoom'
}],
axes: [{
type: 'numeric',
position: 'left',
grid: true,
renderer: 'leftAxisRenderer',
//renderer: function(axis, label) { return label; },
minimum: 0
}, {
type: 'time',
position: 'bottom',
grid: true,
fields: ['time']
}],
legend: {
docked: 'bottom'
},
listeners: {
animationend: 'onAfterAnimation'
},
initComponent: function() {
var me = this;
var series = {};
if (!me.store) {
throw "cannot work without store";
}
if (!me.fields) {
throw "cannot work without fields";
}
me.callParent();
// add correct label for left axis
var axisTitle = "";
if (me.unit === 'percent') {
axisTitle = "%";
} else if (me.unit === 'bytes') {
axisTitle = "Bytes";
} else if (me.unit === 'bytespersecond') {
axisTitle = "Bytes/s";
} else if (me.fieldTitles && me.fieldTitles.length === 1) {
axisTitle = me.fieldTitles[0];
} else if (me.fields.length === 1) {
axisTitle = me.fields[0];
}
me.axes[0].setTitle(axisTitle);
if (!me.noTool) {
me.addTool([{
type: 'minus',
disabled: true,
tooltip: gettext('Undo Zoom'),
handler: function(){
var undoButton = me.interactions[0].getUndoButton();
if (undoButton.handler) {
undoButton.handler();
}
}
},{
type: 'restore',
tooltip: gettext('Toggle Legend'),
handler: function(){
if (me.legend) {
me.legend.setVisible(!me.legend.isVisible());
}
}
}]);
}
// add a series for each field we get
me.fields.forEach(function(item, index){
var title = item;
if (me.fieldTitles && me.fieldTitles[index]) {
title = me.fieldTitles[index];
}
me.addSeries(Ext.apply(
{
type: 'line',
xField: 'time',
yField: item,
title: title,
fill: true,
style: {
lineWidth: 1.5,
opacity: 0.60
},
marker: {
opacity: 0,
scaling: 0.01,
fx: {
duration: 200,
easing: 'easeOut'
}
},
highlightCfg: {
opacity: 1,
scaling: 1.5
},
tooltip: {
trackMouse: true,
renderer: 'onSeriesTooltipRender'
}
},
me.seriesConfig
));
});
// enable animation after the store is loaded
me.store.onAfter('load', function() {
me.setAnimation(true);
}, this, {single: true});
}
});
Ext.define('Proxmox.panel.GaugeWidget', {
extend: 'Ext.panel.Panel',
alias: 'widget.proxmoxGauge',
defaults: {
style: {
'text-align':'center'
}
},
items: [
{
xtype: 'box',
itemId: 'title',
data: {
title: ''
},
tpl: '{title}
'
},
{
xtype: 'polar',
height: 120,
border: false,
itemId: 'chart',
series: [{
type: 'gauge',
value: 0,
colors: ['#f5f5f5'],
sectors: [0],
donut: 90,
needleLength: 100,
totalAngle: Math.PI
}],
sprites: [{
id: 'valueSprite',
type: 'text',
text: '',
textAlign: 'center',
textBaseline: 'bottom',
x: 125,
y: 110,
fontSize: 30
}]
},
{
xtype: 'box',
itemId: 'text'
}
],
header: false,
border: false,
warningThreshold: 0.6,
criticalThreshold: 0.9,
warningColor: '#fc0',
criticalColor: '#FF6C59',
defaultColor: '#c2ddf2',
backgroundColor: '#f5f5f5',
initialValue: 0,
updateValue: function(value, text) {
var me = this;
var color = me.defaultColor;
var attr = {};
if (value >= me.criticalThreshold) {
color = me.criticalColor;
} else if (value >= me.warningThreshold) {
color = me.warningColor;
}
me.chart.series[0].setColors([color, me.backgroundColor]);
me.chart.series[0].setValue(value*100);
me.valueSprite.setText(' '+(value*100).toFixed(0) + '%');
attr.x = me.chart.getWidth()/2;
attr.y = me.chart.getHeight()-20;
if (me.spriteFontSize) {
attr.fontSize = me.spriteFontSize;
}
me.valueSprite.setAttributes(attr, true);
if (text !== undefined) {
me.text.setHtml(text);
}
},
initComponent: function() {
var me = this;
me.callParent();
if (me.title) {
me.getComponent('title').update({title: me.title});
}
me.text = me.getComponent('text');
me.chart = me.getComponent('chart');
me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
}
});
// fixme: how can we avoid those lint errors?
/*jslint confusion: true */
Ext.define('Proxmox.window.Edit', {
extend: 'Ext.window.Window',
alias: 'widget.proxmoxWindowEdit',
// autoLoad trigger a load() after component creation
autoLoad: false,
resizable: false,
// use this tio atimatically generate a title like
// Create:
subject: undefined,
// set isCreate to true if you want a Create button (instead
// OK and RESET)
isCreate: false,
// set to true if you want an Add button (instead of Create)
isAdd: false,
// set to true if you want an Remove button (instead of Create)
isRemove: false,
// custom submitText
submitText: undefined,
backgroundDelay: 0,
// needed for finding the reference to submitbutton
// because we do not have a controller
referenceHolder: true,
defaultButton: 'submitbutton',
// finds the first form field
defaultFocus: 'field[disabled=false][hidden=false]',
showProgress: false,
showTaskViewer: false,
// gets called if we have a progress bar or taskview and it detected that
// the task finished. function(success)
taskDone: Ext.emptyFn,
// gets called when the api call is finished, right at the beginning
// function(success, response, options)
apiCallDone: Ext.emptyFn,
// assign a reference from docs, to add a help button docked to the
// bottom of the window. If undefined we magically fall back to the
// onlineHelp of our first item, if set.
onlineHelp: undefined,
isValid: function() {
var me = this;
var form = me.formPanel.getForm();
return form.isValid();
},
getValues: function(dirtyOnly) {
var me = this;
var values = {};
var form = me.formPanel.getForm();
form.getFields().each(function(field) {
if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
}
});
Ext.Array.each(me.query('inputpanel'), function(panel) {
Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
});
return values;
},
setValues: function(values) {
var me = this;
var form = me.formPanel.getForm();
Ext.iterate(values, function(fieldId, val) {
var field = form.findField(fieldId);
if (field && !field.up('inputpanel')) {
field.setValue(val);
if (form.trackResetOnLoad) {
field.resetOriginalValue();
}
}
});
Ext.Array.each(me.query('inputpanel'), function(panel) {
panel.setValues(values);
});
},
submit: function() {
var me = this;
var form = me.formPanel.getForm();
var values = me.getValues();
Ext.Object.each(values, function(name, val) {
if (values.hasOwnProperty(name)) {
if (Ext.isArray(val) && !val.length) {
values[name] = '';
}
}
});
if (me.digest) {
values.digest = me.digest;
}
if (me.backgroundDelay) {
values.background_delay = me.backgroundDelay;
}
var url = me.url;
if (me.method === 'DELETE') {
url = url + "?" + Ext.Object.toQueryString(values);
values = undefined;
}
Proxmox.Utils.API2Request({
url: url,
waitMsgTarget: me,
method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
params: values,
failure: function(response, options) {
me.apiCallDone(false, response, options);
if (response.result && response.result.errors) {
form.markInvalid(response.result.errors);
}
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, options) {
var hasProgressBar = (me.backgroundDelay || me.showProgress || me.showTaskViewer) &&
response.result.data ? true : false;
me.apiCallDone(true, response, options);
if (hasProgressBar) {
// stay around so we can trigger our close events
// when background action is completed
me.hide();
var upid = response.result.data;
var viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress';
var win = Ext.create('Proxmox.window.Task' + viewerClass, {
upid: upid,
taskDone: me.taskDone,
listeners: {
destroy: function () {
me.close();
}
}
});
win.show();
} else {
me.close();
}
}
});
},
load: function(options) {
var me = this;
var form = me.formPanel.getForm();
options = options || {};
var newopts = Ext.apply({
waitMsgTarget: me
}, options);
var createWrapper = function(successFn) {
Ext.apply(newopts, {
url: me.url,
method: 'GET',
success: function(response, opts) {
form.clearInvalid();
me.digest = response.result.data.digest;
if (successFn) {
successFn(response, opts);
} else {
me.setValues(response.result.data);
}
// hack: fix ExtJS bug
Ext.Array.each(me.query('radiofield'), function(f) {
f.resetOriginalValue();
});
},
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() {
me.close();
});
}
});
};
createWrapper(options.success);
Proxmox.Utils.API2Request(newopts);
},
initComponent : function() {
var me = this;
if (!me.url) {
throw "no url specified";
}
if (me.create) {throw "deprecated parameter, use isCreate";}
var items = Ext.isArray(me.items) ? me.items : [ me.items ];
me.items = undefined;
me.formPanel = Ext.create('Ext.form.Panel', {
url: me.url,
method: me.method || 'PUT',
trackResetOnLoad: true,
bodyPadding: 10,
border: false,
defaults: Ext.apply({}, me.defaults, {
border: false
}),
fieldDefaults: Ext.apply({}, me.fieldDefaults, {
labelWidth: 100,
anchor: '100%'
}),
items: items
});
var inputPanel = me.formPanel.down('inputpanel');
var form = me.formPanel.getForm();
var submitText;
if (me.isCreate) {
if (me.submitText) {
submitText = me.submitText;
} else if (me.isAdd) {
submitText = gettext('Add');
} else if (me.isRemove) {
submitText = gettext('Remove');
} else {
submitText = gettext('Create');
}
} else {
submitText = me.submitText || gettext('OK');
}
var submitBtn = Ext.create('Ext.Button', {
reference: 'submitbutton',
text: submitText,
disabled: !me.isCreate,
handler: function() {
me.submit();
}
});
var resetBtn = Ext.create('Ext.Button', {
text: 'Reset',
disabled: true,
handler: function(){
form.reset();
}
});
var set_button_status = function() {
var valid = form.isValid();
var dirty = form.isDirty();
submitBtn.setDisabled(!valid || !(dirty || me.isCreate));
resetBtn.setDisabled(!dirty);
if (inputPanel && inputPanel.hasAdvanced) {
// we want to show the advanced options
// as soon as some of it is not valid
var advancedItems = me.down('#advancedContainer').query('field');
var valid = true;
advancedItems.forEach(function(field) {
if (!field.isValid()) {
valid = false;
}
});
if (!valid) {
inputPanel.setAdvancedVisible(true);
me.down('#advancedcb').setValue(true);
}
}
};
form.on('dirtychange', set_button_status);
form.on('validitychange', set_button_status);
var colwidth = 300;
if (me.fieldDefaults && me.fieldDefaults.labelWidth) {
colwidth += me.fieldDefaults.labelWidth - 100;
}
var twoColumn = inputPanel &&
(inputPanel.column1 || inputPanel.column2);
if (me.subject && !me.title) {
me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
}
if (me.isCreate) {
me.buttons = [ submitBtn ] ;
} else {
me.buttons = [ submitBtn, resetBtn ];
}
if (inputPanel && inputPanel.hasAdvanced) {
var sp = Ext.state.Manager.getProvider();
var advchecked = sp.get('proxmox-advanced-cb');
inputPanel.setAdvancedVisible(advchecked);
me.buttons.unshift(
{
xtype: 'proxmoxcheckbox',
itemId: 'advancedcb',
boxLabelAlign: 'before',
boxLabel: gettext('Advanced'),
stateId: 'proxmox-advanced-cb',
value: advchecked,
listeners: {
change: function(cb, val) {
inputPanel.setAdvancedVisible(val);
sp.set('proxmox-advanced-cb', val);
}
}
}
);
}
var onlineHelp = me.onlineHelp;
if (!onlineHelp && inputPanel && inputPanel.onlineHelp) {
onlineHelp = inputPanel.onlineHelp;
}
if (onlineHelp) {
var helpButton = Ext.create('Proxmox.button.Help');
me.buttons.unshift(helpButton, '->');
Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp);
}
Ext.applyIf(me, {
modal: true,
width: twoColumn ? colwidth*2 : colwidth,
border: false,
items: [ me.formPanel ]
});
me.callParent();
// always mark invalid fields
me.on('afterlayout', function() {
// on touch devices, the isValid function
// triggers a layout, which triggers an isValid
// and so on
// to prevent this we disable the layouting here
// and enable it afterwards
me.suspendLayout = true;
me.isValid();
me.suspendLayout = false;
});
if (me.autoLoad) {
me.load();
}
}
});
Ext.define('Proxmox.window.PasswordEdit', {
extend: 'Proxmox.window.Edit',
alias: 'proxmoxWindowPasswordEdit',
subject: gettext('Password'),
url: '/api2/extjs/access/password',
fieldDefaults: {
labelWidth: 120
},
items: [
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Password'),
minLength: 5,
allowBlank: false,
name: 'password',
listeners: {
change: function(field){
field.next().validate();
},
blur: function(field){
field.next().validate();
}
}
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Confirm password'),
name: 'verifypassword',
allowBlank: false,
vtype: 'password',
initialPassField: 'password',
submitValue: false
},
{
xtype: 'hiddenfield',
name: 'userid'
}
],
initComponent : function() {
var me = this;
if (!me.userid) {
throw "no userid specified";
}
me.callParent();
me.down('[name=userid]').setValue(me.userid);
}
});
Ext.define('Proxmox.window.TaskProgress', {
extend: 'Ext.window.Window',
alias: 'widget.proxmoxTaskProgress',
taskDone: Ext.emptyFn,
initComponent: function() {
var me = this;
if (!me.upid) {
throw "no task specified";
}
var task = Proxmox.Utils.parse_task_upid(me.upid);
var statstore = Ext.create('Proxmox.data.ObjectStore', {
url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
interval: 1000,
rows: {
status: { defaultValue: 'unknown' },
exitstatus: { defaultValue: 'unknown' }
}
});
me.on('destroy', statstore.stopUpdate);
var getObjectValue = function(key, defaultValue) {
var rec = statstore.getById(key);
if (rec) {
return rec.data.value;
}
return defaultValue;
};
var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' });
me.mon(statstore, 'load', function() {
var status = getObjectValue('status');
if (status === 'stopped') {
var exitstatus = getObjectValue('exitstatus');
if (exitstatus == 'OK') {
pbar.reset();
pbar.updateText("Done!");
Ext.Function.defer(me.close, 1000, me);
} else {
me.close();
Ext.Msg.alert('Task failed', exitstatus);
}
me.taskDone(exitstatus == 'OK');
}
});
var descr = Proxmox.Utils.format_task_description(task.type, task.id);
Ext.apply(me, {
title: gettext('Task') + ': ' + descr,
width: 300,
layout: 'auto',
modal: true,
bodyPadding: 5,
items: pbar,
buttons: [
{
text: gettext('Details'),
handler: function() {
var win = Ext.create('Proxmox.window.TaskViewer', {
taskDone: me.taskDone,
upid: me.upid
});
win.show();
me.close();
}
}
]
});
me.callParent();
statstore.startUpdate();
pbar.wait();
}
});
// fixme: how can we avoid those lint errors?
/*jslint confusion: true */
Ext.define('Proxmox.window.TaskViewer', {
extend: 'Ext.window.Window',
alias: 'widget.proxmoxTaskViewer',
extraTitle: '', // string to prepend after the generic task title
taskDone: Ext.emptyFn,
initComponent: function() {
var me = this;
if (!me.upid) {
throw "no task specified";
}
var task = Proxmox.Utils.parse_task_upid(me.upid);
var statgrid;
var rows = {
status: {
header: gettext('Status'),
defaultValue: 'unknown',
renderer: function(value) {
if (value != 'stopped') {
return value;
}
var es = statgrid.getObjectValue('exitstatus');
if (es) {
return value + ': ' + es;
}
}
},
exitstatus: {
visible: false
},
type: {
header: gettext('Task type'),
required: true
},
user: {
header: gettext('User name'),
required: true
},
node: {
header: gettext('Node'),
required: true
},
pid: {
header: gettext('Process ID'),
required: true
},
starttime: {
header: gettext('Start Time'),
required: true,
renderer: Proxmox.Utils.render_timestamp
},
upid: {
header: gettext('Unique task ID')
}
};
var statstore = Ext.create('Proxmox.data.ObjectStore', {
url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
interval: 1000,
rows: rows
});
me.on('destroy', statstore.stopUpdate);
var stop_task = function() {
Proxmox.Utils.API2Request({
url: "/nodes/" + task.node + "/tasks/" + me.upid,
waitMsgTarget: me,
method: 'DELETE',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
};
var stop_btn1 = new Ext.Button({
text: gettext('Stop'),
disabled: true,
handler: stop_task
});
var stop_btn2 = new Ext.Button({
text: gettext('Stop'),
disabled: true,
handler: stop_task
});
statgrid = Ext.create('Proxmox.grid.ObjectGrid', {
title: gettext('Status'),
layout: 'fit',
tbar: [ stop_btn1 ],
rstore: statstore,
rows: rows,
border: false
});
var logView = Ext.create('Proxmox.panel.LogView', {
title: gettext('Output'),
tbar: [ stop_btn2 ],
border: false,
url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log"
});
me.mon(statstore, 'load', function() {
var status = statgrid.getObjectValue('status');
if (status === 'stopped') {
logView.scrollToEnd = false;
logView.requestUpdate();
statstore.stopUpdate();
me.taskDone(statgrid.getObjectValue('exitstatus') == 'OK');
}
stop_btn1.setDisabled(status !== 'running');
stop_btn2.setDisabled(status !== 'running');
});
statstore.startUpdate();
Ext.apply(me, {
title: "Task viewer: " + task.desc + me.extraTitle,
width: 800,
height: 400,
layout: 'fit',
modal: true,
items: [{
xtype: 'tabpanel',
region: 'center',
items: [ logView, statgrid ]
}]
});
me.callParent();
logView.fireEvent('show', logView);
}
});
Ext.define('apt-pkglist', {
extend: 'Ext.data.Model',
fields: [ 'Package', 'Title', 'Description', 'Section', 'Arch',
'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin' ],
idProperty: 'Package'
});
Ext.define('Proxmox.node.APT', {
extend: 'Ext.grid.GridPanel',
xtype: 'proxmoxNodeAPT',
upgradeBtn: undefined,
columns: [
{
header: gettext('Package'),
width: 200,
sortable: true,
dataIndex: 'Package'
},
{
text: gettext('Version'),
columns: [
{
header: gettext('current'),
width: 100,
sortable: false,
dataIndex: 'OldVersion'
},
{
header: gettext('new'),
width: 100,
sortable: false,
dataIndex: 'Version'
}
]
},
{
header: gettext('Description'),
sortable: false,
dataIndex: 'Title',
flex: 1
}
],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var store = Ext.create('Ext.data.Store', {
model: 'apt-pkglist',
groupField: 'Origin',
proxy: {
type: 'proxmox',
url: "/api2/json/nodes/" + me.nodename + "/apt/update"
},
sorters: [
{
property : 'Package',
direction: 'ASC'
}
]
});
var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
enableGroupingMenu: false
});
var rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {
getAdditionalData: function (data, rowIndex, record, orig) {
var headerCt = this.view.headerCt;
var colspan = headerCt.getColumnCount();
// Usually you would style the my-body-class in CSS file
return {
rowBody: '' +
Ext.String.htmlEncode(data.Description) +
'
',
rowBodyColspan: colspan
};
}
});
var reload = function() {
store.load();
};
Proxmox.Utils.monStoreErrors(me, store, true);
var apt_command = function(cmd){
Proxmox.Utils.API2Request({
url: "/nodes/" + me.nodename + "/apt/" + cmd,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, opts) {
var upid = response.result.data;
var win = Ext.create('Proxmox.window.TaskViewer', {
upid: upid
});
win.show();
me.mon(win, 'close', reload);
}
});
};
var sm = Ext.create('Ext.selection.RowModel', {});
var update_btn = new Ext.Button({
text: gettext('Refresh'),
handler: function(){
Proxmox.Utils.checked_command(function() { apt_command('update'); });
}
});
var show_changelog = function(rec) {
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
return;
}
var view = Ext.createWidget('component', {
autoScroll: true,
style: {
'background-color': 'white',
'white-space': 'pre',
'font-family': 'monospace',
padding: '5px'
}
});
var win = Ext.create('Ext.window.Window', {
title: gettext('Changelog') + ": " + rec.data.Package,
width: 800,
height: 400,
layout: 'fit',
modal: true,
items: [ view ]
});
Proxmox.Utils.API2Request({
waitMsgTarget: me,
url: "/nodes/" + me.nodename + "/apt/changelog",
params: {
name: rec.data.Package,
version: rec.data.Version
},
method: 'GET',
failure: function(response, opts) {
win.close();
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, opts) {
win.show();
view.update(Ext.htmlEncode(response.result.data));
}
});
};
var changelog_btn = new Proxmox.button.Button({
text: gettext('Changelog'),
selModel: sm,
disabled: true,
enableFn: function(rec) {
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
return false;
}
return true;
},
handler: function(b, e, rec) {
show_changelog(rec);
}
});
if (me.upgradeBtn) {
me.tbar = [ update_btn, me.upgradeBtn, changelog_btn ];
} else {
me.tbar = [ update_btn, changelog_btn ];
}
Ext.apply(me, {
store: store,
stateful: true,
stateId: 'grid-update',
selModel: sm,
viewConfig: {
stripeRows: false,
emptyText: '' + gettext('No updates available.') + '
'
},
features: [ groupingFeature, rowBodyFeature ],
listeners: {
activate: reload,
itemdblclick: function(v, rec) {
show_changelog(rec);
}
}
});
me.callParent();
}
});
Ext.define('Proxmox.node.NetworkEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeNetworkEdit'],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
if (!me.iftype) {
throw "no network device type specified";
}
me.isCreate = !me.iface;
var iface_vtype;
if (me.iftype === 'bridge') {
iface_vtype = 'BridgeName';
} else if (me.iftype === 'bond') {
iface_vtype = 'BondName';
} else if (me.iftype === 'eth' && !me.isCreate) {
iface_vtype = 'InterfaceName';
} else if (me.iftype === 'vlan' && !me.isCreate) {
iface_vtype = 'InterfaceName';
} else if (me.iftype === 'OVSBridge') {
iface_vtype = 'BridgeName';
} else if (me.iftype === 'OVSBond') {
iface_vtype = 'BondName';
} else if (me.iftype === 'OVSIntPort') {
iface_vtype = 'InterfaceName';
} else if (me.iftype === 'OVSPort') {
iface_vtype = 'InterfaceName';
} else {
console.log(me.iftype);
throw "unknown network device type specified";
}
me.subject = Proxmox.Utils.render_network_iface_type(me.iftype);
var column2 = [];
if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' ||
me.iftype === 'OVSBond')) {
column2.push({
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Autostart'),
name: 'autostart',
uncheckedValue: 0,
checked: me.isCreate ? true : undefined
});
}
if (me.iftype === 'bridge') {
column2.push({
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('VLAN aware'),
name: 'bridge_vlan_aware',
deleteEmpty: !me.isCreate
});
column2.push({
xtype: 'textfield',
fieldLabel: gettext('Bridge ports'),
name: 'bridge_ports'
});
} else if (me.iftype === 'OVSBridge') {
column2.push({
xtype: 'textfield',
fieldLabel: gettext('Bridge ports'),
name: 'ovs_ports'
});
column2.push({
xtype: 'textfield',
fieldLabel: gettext('OVS options'),
name: 'ovs_options'
});
} else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') {
column2.push({
xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
allowBlank: false,
nodename: me.nodename,
bridgeType: 'OVSBridge',
name: 'ovs_bridge'
});
column2.push({
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: ''
});
column2.push({
xtype: 'textfield',
fieldLabel: gettext('OVS options'),
name: 'ovs_options'
});
} else if (me.iftype === 'bond') {
column2.push({
xtype: 'textfield',
fieldLabel: gettext('Slaves'),
name: 'slaves'
});
var policySelector = Ext.createWidget('bondPolicySelector', {
fieldLabel: gettext('Hash policy'),
name: 'bond_xmit_hash_policy',
deleteEmpty: !me.isCreate,
disabled: true
});
column2.push({
xtype: 'bondModeSelector',
fieldLabel: gettext('Mode'),
name: 'bond_mode',
value: me.isCreate ? 'balance-rr' : undefined,
listeners: {
change: function(f, value) {
if (value === 'balance-xor' ||
value === '802.3ad') {
policySelector.setDisabled(false);
} else {
policySelector.setDisabled(true);
policySelector.setValue('');
}
}
},
allowBlank: false
});
column2.push(policySelector);
} else if (me.iftype === 'OVSBond') {
column2.push({
xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
allowBlank: false,
nodename: me.nodename,
bridgeType: 'OVSBridge',
name: 'ovs_bridge'
});
column2.push({
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: ''
});
column2.push({
xtype: 'textfield',
fieldLabel: gettext('OVS options'),
name: 'ovs_options'
});
}
column2.push({
xtype: 'textfield',
fieldLabel: gettext('Comment'),
allowBlank: true,
nodename: me.nodename,
name: 'comments'
});
var url;
var method;
if (me.isCreate) {
url = "/api2/extjs/nodes/" + me.nodename + "/network";
method = 'POST';
} else {
url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface;
method = 'PUT';
}
var column1 = [
{
xtype: 'hiddenfield',
name: 'type',
value: me.iftype
},
{
xtype: me.isCreate ? 'textfield' : 'displayfield',
fieldLabel: gettext('Name'),
name: 'iface',
value: me.iface,
vtype: iface_vtype,
allowBlank: false
}
];
if (me.iftype === 'OVSBond') {
column1.push(
{
xtype: 'bondModeSelector',
fieldLabel: gettext('Mode'),
name: 'bond_mode',
openvswitch: true,
value: me.isCreate ? 'active-backup' : undefined,
allowBlank: false
},
{
xtype: 'textfield',
fieldLabel: gettext('Slaves'),
name: 'ovs_bonds'
}
);
} else {
column1.push(
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('IP address'),
vtype: 'IPAddress',
name: 'address'
},
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('Subnet mask'),
vtype: 'IPAddress',
name: 'netmask',
validator: function(value) {
/*jslint confusion: true */
if (!me.items) {
return true;
}
var address = me.down('field[name=address]').getValue();
if (value !== '') {
if (address === '') {
return "Subnet mask requires option 'IP address'";
}
} else {
if (address !== '') {
return "Option 'IP address' requires a subnet mask";
}
}
return true;
}
},
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('Gateway'),
vtype: 'IPAddress',
name: 'gateway'
},
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('IPv6 address'),
vtype: 'IP6Address',
name: 'address6'
},
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('Prefix length'),
vtype: 'IP6PrefixLength',
name: 'netmask6',
value: '',
allowBlank: true,
validator: function(value) {
/*jslint confusion: true */
if (!me.items) {
return true;
}
var address = me.down('field[name=address6]').getValue();
if (value !== '') {
if (address === '') {
return "IPv6 prefix length requires option 'IPv6 address'";
}
} else {
if (address !== '') {
return "Option 'IPv6 address' requires an IPv6 prefix length";
}
}
return true;
}
},
{
xtype: 'proxmoxtextfield',
deleteEmpty: !me.isCreate,
fieldLabel: gettext('Gateway'),
vtype: 'IP6Address',
name: 'gateway6'
}
);
}
Ext.applyIf(me, {
url: url,
method: method,
items: {
xtype: 'inputpanel',
column1: column1,
column2: column2
}
});
me.callParent();
if (me.isCreate) {
me.down('field[name=iface]').setValue(me.iface_default);
} else {
me.load({
success: function(response, options) {
var data = response.result.data;
if (data.type !== me.iftype) {
var msg = "Got unexpected device type";
Ext.Msg.alert(gettext('Error'), msg, function() {
me.close();
});
return;
}
me.setValues(data);
me.isValid(); // trigger validation
}
});
}
}
});
Ext.define('proxmox-networks', {
extend: 'Ext.data.Model',
fields: [
'iface', 'type', 'active', 'autostart',
'bridge_ports', 'slaves',
'address', 'netmask', 'gateway',
'address6', 'netmask6', 'gateway6',
'comments'
],
idProperty: 'iface'
});
Ext.define('Proxmox.node.NetworkView', {
extend: 'Ext.panel.Panel',
alias: ['widget.proxmoxNodeNetworkView'],
// defines what types of network devices we want to create
// order is always the same
types: ['bridge', 'bond', 'ovs'],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var baseUrl = '/nodes/' + me.nodename + '/network';
var store = Ext.create('Ext.data.Store', {
model: 'proxmox-networks',
proxy: {
type: 'proxmox',
url: '/api2/json' + baseUrl
},
sorters: [
{
property : 'iface',
direction: 'ASC'
}
]
});
var reload = function() {
var changeitem = me.down('#changes');
Proxmox.Utils.API2Request({
url: baseUrl,
failure: function(response, opts) {
store.loadData({});
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
changeitem.update('');
changeitem.setHidden(true);
},
success: function(response, opts) {
var result = Ext.decode(response.responseText);
store.loadData(result.data);
var changes = result.changes;
if (changes === undefined || changes === '') {
changes = gettext("No changes");
changeitem.setHidden(true);
} else {
changeitem.update("" + Ext.htmlEncode(changes) + "
");
changeitem.setHidden(false);
}
}
});
};
var run_editor = function() {
var grid = me.down('gridpanel');
var sm = grid.getSelectionModel();
var rec = sm.getSelection()[0];
if (!rec) {
return;
}
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iface: rec.data.iface,
iftype: rec.data.type
});
win.show();
win.on('destroy', reload);
};
var edit_btn = new Ext.Button({
text: gettext('Edit'),
disabled: true,
handler: run_editor
});
var del_btn = new Ext.Button({
text: gettext('Remove'),
disabled: true,
handler: function(){
var grid = me.down('gridpanel');
var sm = grid.getSelectionModel();
var rec = sm.getSelection()[0];
if (!rec) {
return;
}
var iface = rec.data.iface;
Proxmox.Utils.API2Request({
url: baseUrl + '/' + iface,
method: 'DELETE',
waitMsgTarget: me,
callback: function() {
reload();
},
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
});
var set_button_status = function() {
var grid = me.down('gridpanel');
var sm = grid.getSelectionModel();
var rec = sm.getSelection()[0];
edit_btn.setDisabled(!rec);
del_btn.setDisabled(!rec);
};
var render_ports = function(value, metaData, record) {
if (value === 'bridge') {
return record.data.bridge_ports;
} else if (value === 'bond') {
return record.data.slaves;
} else if (value === 'OVSBridge') {
return record.data.ovs_ports;
} else if (value === 'OVSBond') {
return record.data.ovs_bonds;
}
};
var find_next_iface_id = function(prefix) {
var next;
for (next = 0; next <= 9999; next++) {
if (!store.getById(prefix + next.toString())) {
break;
}
}
return prefix + next.toString();
};
var menu_items = [];
if (me.types.indexOf('bridge') !== -1) {
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('bridge'),
handler: function() {
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'bridge',
iface_default: find_next_iface_id('vmbr')
});
win.on('destroy', reload);
win.show();
}
});
}
if (me.types.indexOf('bond') !== -1) {
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('bond'),
handler: function() {
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'bond',
iface_default: find_next_iface_id('bond')
});
win.on('destroy', reload);
win.show();
}
});
}
if (me.types.indexOf('ovs') !== -1) {
if (menu_items.length > 0) {
menu_items.push({ xtype: 'menuseparator' });
}
menu_items.push(
{
text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
handler: function() {
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSBridge',
iface_default: find_next_iface_id('vmbr')
});
win.on('destroy', reload);
win.show();
}
},
{
text: Proxmox.Utils.render_network_iface_type('OVSBond'),
handler: function() {
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSBond',
iface_default: find_next_iface_id('bond')
});
win.on('destroy', reload);
win.show();
}
},
{
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
handler: function() {
var win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSIntPort'
});
win.on('destroy', reload);
win.show();
}
}
);
}
Ext.apply(me, {
layout: 'border',
tbar: [
{
text: gettext('Create'),
menu: {
plain: true,
items: menu_items
}
}, ' ',
{
text: gettext('Revert'),
handler: function() {
Proxmox.Utils.API2Request({
url: baseUrl,
method: 'DELETE',
waitMsgTarget: me,
callback: function() {
reload();
},
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
},
edit_btn,
del_btn
],
items: [
{
xtype: 'gridpanel',
stateful: true,
stateId: 'grid-node-network',
store: store,
region: 'center',
border: false,
columns: [
{
header: gettext('Name'),
sortable: true,
dataIndex: 'iface'
},
{
header: gettext('Type'),
sortable: true,
width: 120,
renderer: Proxmox.Utils.render_network_iface_type,
dataIndex: 'type'
},
{
xtype: 'booleancolumn',
header: gettext('Active'),
width: 80,
sortable: true,
dataIndex: 'active',
trueText: Proxmox.Utils.yesText,
falseText: Proxmox.Utils.noText,
undefinedText: Proxmox.Utils.noText,
},
{
xtype: 'booleancolumn',
header: gettext('Autostart'),
width: 80,
sortable: true,
dataIndex: 'autostart',
trueText: Proxmox.Utils.yesText,
falseText: Proxmox.Utils.noText,
undefinedText: Proxmox.Utils.noText
},
{
xtype: 'booleancolumn',
header: gettext('VLAN aware'),
width: 80,
sortable: true,
dataIndex: 'bridge_vlan_aware',
trueText: Proxmox.Utils.yesText,
falseText: Proxmox.Utils.noText,
undefinedText: Proxmox.Utils.noText
},
{
header: gettext('Ports/Slaves'),
dataIndex: 'type',
renderer: render_ports
},
{
header: gettext('IP address'),
sortable: true,
width: 120,
dataIndex: 'address',
renderer: function(value, metaData, rec) {
if (rec.data.address && rec.data.address6) {
return rec.data.address + "
"
+ rec.data.address6 + '/' + rec.data.netmask6;
} else if (rec.data.address6) {
return rec.data.address6 + '/' + rec.data.netmask6;
} else {
return rec.data.address;
}
}
},
{
header: gettext('Subnet mask'),
width: 120,
sortable: true,
dataIndex: 'netmask'
},
{
header: gettext('Gateway'),
width: 120,
sortable: true,
dataIndex: 'gateway',
renderer: function(value, metaData, rec) {
if (rec.data.gateway && rec.data.gateway6) {
return rec.data.gateway + "
" + rec.data.gateway6;
} else if (rec.data.gateway6) {
return rec.data.gateway6;
} else {
return rec.data.gateway;
}
}
},
{
header: gettext('Comment'),
dataIndex: 'comments',
flex: 1,
renderer: Ext.String.htmlEncode
}
],
listeners: {
selectionchange: set_button_status,
itemdblclick: run_editor
}
},
{
border: false,
region: 'south',
autoScroll: true,
hidden: true,
itemId: 'changes',
tbar: [
gettext('Pending changes') + ' (' +
gettext('Please reboot to activate changes') + ')'
],
split: true,
bodyPadding: 5,
flex: 0.6,
html: gettext("No changes")
}
],
});
me.callParent();
reload();
}
});
Ext.define('Proxmox.node.DNSEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeDNSEdit'],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
me.items = [
{
xtype: 'textfield',
fieldLabel: gettext('Search domain'),
name: 'search',
allowBlank: false
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('DNS server') + " 1",
vtype: 'IP64Address',
skipEmptyText: true,
name: 'dns1'
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('DNS server') + " 2",
vtype: 'IP64Address',
skipEmptyText: true,
name: 'dns2'
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('DNS server') + " 3",
vtype: 'IP64Address',
skipEmptyText: true,
name: 'dns3'
}
];
Ext.applyIf(me, {
subject: gettext('DNS'),
url: "/api2/extjs/nodes/" + me.nodename + "/dns",
fieldDefaults: {
labelWidth: 120
}
});
me.callParent();
me.load();
}
});
Ext.define('Proxmox.node.HostsView', {
extend: 'Ext.panel.Panel',
xtype: 'proxmoxNodeHostsView',
reload: function() {
var me = this;
me.store.load();
},
tbar: [
{
text: gettext('Save'),
disabled: true,
itemId: 'savebtn',
handler: function() {
var me = this.up('panel');
Proxmox.Utils.API2Request({
params: {
digest: me.digest,
data: me.down('#hostsfield').getValue()
},
method: 'POST',
url: '/nodes/' + me.nodename + '/hosts',
waitMsgTarget: me,
success: function(response, opts) {
me.reload();
},
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
}
});
}
},
{
text: gettext('Revert'),
disabled: true,
itemId: 'resetbtn',
handler: function() {
var me = this.up('panel');
me.down('#hostsfield').reset();
}
}
],
layout: 'fit',
items: [
{
xtype: 'textarea',
itemId: 'hostsfield',
fieldStyle: {
'font-family': 'monospace',
'white-space': 'pre'
},
listeners: {
dirtychange: function(ta, dirty) {
var me = this.up('panel');
me.down('#savebtn').setDisabled(!dirty);
me.down('#resetbtn').setDisabled(!dirty);
}
}
}
],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
me.store = Ext.create('Ext.data.Store', {
proxy: {
type: 'proxmox',
url: "/api2/json/nodes/" + me.nodename + "/hosts",
}
});
me.callParent();
Proxmox.Utils.monStoreErrors(me, me.store);
me.mon(me.store, 'load', function(store, records, success) {
if (!success || records.length < 1) {
return;
}
me.digest = records[0].data.digest;
var data = records[0].data.data;
me.down('#hostsfield').setValue(data);
me.down('#hostsfield').resetOriginalValue();
});
me.reload();
}
});
Ext.define('Proxmox.node.DNSView', {
extend: 'Proxmox.grid.ObjectGrid',
alias: ['widget.proxmoxNodeDNSView'],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var run_editor = function() {
var win = Ext.create('Proxmox.node.DNSEdit', {
nodename: me.nodename
});
win.show();
};
Ext.apply(me, {
url: "/api2/json/nodes/" + me.nodename + "/dns",
cwidth1: 130,
interval: 1000,
run_editor: run_editor,
rows: {
search: {
header: 'Search domain',
required: true,
renderer: Ext.htmlEncode
},
dns1: {
header: gettext('DNS server') + " 1",
required: true,
renderer: Ext.htmlEncode
},
dns2: {
header: gettext('DNS server') + " 2",
renderer: Ext.htmlEncode
},
dns3: {
header: gettext('DNS server') + " 3",
renderer: Ext.htmlEncode
}
},
tbar: [
{
text: gettext("Edit"),
handler: run_editor
}
],
listeners: {
itemdblclick: run_editor
}
});
me.callParent();
me.on('activate', me.rstore.startUpdate);
me.on('deactivate', me.rstore.stopUpdate);
me.on('destroy', me.rstore.stopUpdate);
}
});
Ext.define('Proxmox.node.Tasks', {
extend: 'Ext.grid.GridPanel',
alias: ['widget.proxmoxNodeTasks'],
stateful: true,
stateId: 'grid-node-tasks',
loadMask: true,
sortableColumns: false,
vmidFilter: 0,
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var store = Ext.create('Ext.data.BufferedStore', {
pageSize: 500,
autoLoad: true,
remoteFilter: true,
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',
startParam: 'start',
limitParam: 'limit',
url: "/api2/json/nodes/" + me.nodename + "/tasks"
}
});
var userfilter = '';
var filter_errors = 0;
var updateProxyParams = function() {
var params = {
errors: filter_errors
};
if (userfilter) {
params.userfilter = userfilter;
}
if (me.vmidFilter) {
params.vmid = me.vmidFilter;
}
store.proxy.extraParams = params;
};
updateProxyParams();
var reload_task = Ext.create('Ext.util.DelayedTask',function() {
updateProxyParams();
store.reload();
});
var run_task_viewer = function() {
var sm = me.getSelectionModel();
var rec = sm.getSelection()[0];
if (!rec) {
return;
}
var win = Ext.create('Proxmox.window.TaskViewer', {
upid: rec.data.upid
});
win.show();
};
var view_btn = new Ext.Button({
text: gettext('View'),
disabled: true,
handler: run_task_viewer
});
Proxmox.Utils.monStoreErrors(me, store, true);
Ext.apply(me, {
store: store,
viewConfig: {
trackOver: false,
stripeRows: false, // does not work with getRowClass()
getRowClass: function(record, index) {
var status = record.get('status');
if (status && status != 'OK') {
return "proxmox-invalid-row";
}
}
},
tbar: [
view_btn, '->', gettext('User name') +':', ' ',
{
xtype: 'textfield',
width: 200,
value: userfilter,
enableKeyEvents: true,
listeners: {
keyup: function(field, e) {
userfilter = field.getValue();
reload_task.delay(500);
}
}
}, ' ', gettext('Only Errors') + ':', ' ',
{
xtype: 'checkbox',
hideLabel: true,
checked: filter_errors,
listeners: {
change: function(field, checked) {
filter_errors = checked ? 1 : 0;
reload_task.delay(10);
}
}
}, ' '
],
columns: [
{
header: gettext("Start Time"),
dataIndex: 'starttime',
width: 100,
renderer: function(value) {
return Ext.Date.format(value, "M d H:i:s");
}
},
{
header: gettext("End Time"),
dataIndex: 'endtime',
width: 100,
renderer: function(value, metaData, record) {
return Ext.Date.format(value,"M d H:i:s");
}
},
{
header: gettext("Node"),
dataIndex: 'node',
width: 100
},
{
header: gettext("User name"),
dataIndex: 'user',
width: 150
},
{
header: gettext("Description"),
dataIndex: 'upid',
flex: 1,
renderer: Proxmox.Utils.render_upid
},
{
header: gettext("Status"),
dataIndex: 'status',
width: 200,
renderer: function(value, metaData, record) {
if (value == 'OK') {
return 'OK';
}
// metaData.attr = 'style="color:red;"';
return "ERROR: " + value;
}
}
],
listeners: {
itemdblclick: run_task_viewer,
selectionchange: function(v, selections) {
view_btn.setDisabled(!(selections && selections[0]));
},
show: function() { reload_task.delay(10); },
destroy: function() { reload_task.cancel(); }
}
});
me.callParent();
}
});
Ext.define('proxmox-services', {
extend: 'Ext.data.Model',
fields: [ 'service', 'name', 'desc', 'state' ],
idProperty: 'service'
});
Ext.define('Proxmox.node.ServiceView', {
extend: 'Ext.grid.GridPanel',
alias: ['widget.proxmoxNodeServiceView'],
startOnlyServices: {},
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var rstore = Ext.create('Proxmox.data.UpdateStore', {
interval: 1000,
storeid: 'proxmox-services' + me.nodename,
model: 'proxmox-services',
proxy: {
type: 'proxmox',
url: "/api2/json/nodes/" + me.nodename + "/services"
}
});
var store = Ext.create('Proxmox.data.DiffStore', {
rstore: rstore,
sortAfterUpdate: true,
sorters: [
{
property : 'name',
direction: 'ASC'
}
]
});
var view_service_log = function() {
var sm = me.getSelectionModel();
var rec = sm.getSelection()[0];
var win = Ext.create('Ext.window.Window', {
title: gettext('Syslog') + ': ' + rec.data.service,
modal: true,
items: {
xtype: 'proxmoxLogView',
width: 800,
height: 400,
url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
rec.data.service,
log_select_timespan: 1
}
});
win.show();
};
var service_cmd = function(cmd) {
var sm = me.getSelectionModel();
var rec = sm.getSelection()[0];
Proxmox.Utils.API2Request({
url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
me.loading = true;
},
success: function(response, opts) {
rstore.startUpdate();
var upid = response.result.data;
var win = Ext.create('Proxmox.window.TaskProgress', {
upid: upid
});
win.show();
}
});
};
var start_btn = new Ext.Button({
text: gettext('Start'),
disabled: true,
handler: function(){
service_cmd("start");
}
});
var stop_btn = new Ext.Button({
text: gettext('Stop'),
disabled: true,
handler: function(){
service_cmd("stop");
}
});
var restart_btn = new Ext.Button({
text: gettext('Restart'),
disabled: true,
handler: function(){
service_cmd("restart");
}
});
var syslog_btn = new Ext.Button({
text: gettext('Syslog'),
disabled: true,
handler: view_service_log
});
var set_button_status = function() {
var sm = me.getSelectionModel();
var rec = sm.getSelection()[0];
if (!rec) {
start_btn.disable();
stop_btn.disable();
restart_btn.disable();
syslog_btn.disable();
return;
}
var service = rec.data.service;
var state = rec.data.state;
syslog_btn.enable();
if (me.startOnlyServices[service]) {
if (state == 'running') {
start_btn.disable();
restart_btn.enable();
} else {
start_btn.enable();
restart_btn.disable();
}
stop_btn.disable();
} else {
if (state == 'running') {
start_btn.disable();
restart_btn.enable();
stop_btn.enable();
} else {
start_btn.enable();
restart_btn.disable();
stop_btn.disable();
}
}
};
me.mon(store, 'refresh', set_button_status);
Proxmox.Utils.monStoreErrors(me, rstore);
Ext.apply(me, {
store: store,
stateful: false,
tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ],
columns: [
{
header: gettext('Name'),
flex: 1,
sortable: true,
dataIndex: 'name'
},
{
header: gettext('Status'),
width: 100,
sortable: true,
dataIndex: 'state'
},
{
header: gettext('Description'),
renderer: Ext.String.htmlEncode,
dataIndex: 'desc',
flex: 2
}
],
listeners: {
selectionchange: set_button_status,
itemdblclick: view_service_log,
activate: rstore.startUpdate,
destroy: rstore.stopUpdate
}
});
me.callParent();
}
});
Ext.define('Proxmox.node.TimeEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeTimeEdit'],
subject: gettext('Time zone'),
width: 400,
autoLoad: true,
fieldDefaults: {
labelWidth: 70
},
items: {
xtype: 'combo',
fieldLabel: gettext('Time zone'),
name: 'timezone',
queryMode: 'local',
store: Ext.create('Proxmox.data.TimezoneStore'),
displayField: 'zone',
forceSelection: true,
editable: false,
allowBlank: false
},
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
me.url = "/api2/extjs/nodes/" + me.nodename + "/time";
me.callParent();
}
});
Ext.define('Proxmox.node.TimeView', {
extend: 'Proxmox.grid.ObjectGrid',
alias: ['widget.proxmoxNodeTimeView'],
initComponent : function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
var tzoffset = (new Date()).getTimezoneOffset()*60000;
var renderlocaltime = function(value) {
var servertime = new Date((value * 1000) + tzoffset);
return Ext.Date.format(servertime, 'Y-m-d H:i:s');
};
var run_editor = function() {
var win = Ext.create('Proxmox.node.TimeEdit', {
nodename: me.nodename
});
win.show();
};
Ext.apply(me, {
url: "/api2/json/nodes/" + me.nodename + "/time",
cwidth1: 150,
interval: 1000,
run_editor: run_editor,
rows: {
timezone: {
header: gettext('Time zone'),
required: true
},
localtime: {
header: gettext('Server time'),
required: true,
renderer: renderlocaltime
}
},
tbar: [
{
text: gettext("Edit"),
handler: run_editor
}
],
listeners: {
itemdblclick: run_editor
}
});
me.callParent();
me.on('activate', me.rstore.startUpdate);
me.on('deactivate', me.rstore.stopUpdate);
me.on('destroy', me.rstore.stopUpdate);
}
});