From e7675074c04ad3cb30ea67eff6cc2c27f1bdbac6 Mon Sep 17 00:00:00 2001 From: Weilbyte Date: Sun, 26 May 2019 19:35:33 +0200 Subject: [PATCH] Modified proxmoxlib.js Proxmoxlib.js modified for 5.4-3 --- serverside/jsmod/5.4-3/proxmoxlib.js | 6757 ++++++++++++++++++++++++++ 1 file changed, 6757 insertions(+) create mode 100644 serverside/jsmod/5.4-3/proxmoxlib.js 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); + } +});