diff --git a/serverside/jsmod/5.4-3/proxmoxlib.js b/serverside/jsmod/5.4-3/proxmoxlib.js
new file mode 100644
index 0000000..6e316d7
--- /dev/null
+++ b/serverside/jsmod/5.4-3/proxmoxlib.js
@@ -0,0 +1,6757 @@
+// 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: '#7289DA',
+ backgroundColor: '#2C2F33',
+
+ 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);
+ }
+});