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

{title}

' }, { xtype: 'polar', height: 120, border: false, itemId: 'chart', series: [{ type: 'gauge', value: 0, colors: ['#f5f5f5'], sectors: [0], donut: 90, needleLength: 100, totalAngle: Math.PI }], sprites: [{ id: 'valueSprite', type: 'text', text: '', textAlign: 'center', textBaseline: 'bottom', x: 125, y: 110, fontSize: 30 }] }, { xtype: 'box', itemId: 'text' } ], header: false, border: false, warningThreshold: 0.6, criticalThreshold: 0.9, warningColor: '#fc0', criticalColor: '#FF6C59', defaultColor: '#c2ddf2', backgroundColor: '#f5f5f5', initialValue: 0, updateValue: function(value, text) { var me = this; var color = me.defaultColor; var attr = {}; if (value >= me.criticalThreshold) { color = me.criticalColor; } else if (value >= me.warningThreshold) { color = me.warningColor; } me.chart.series[0].setColors([color, me.backgroundColor]); me.chart.series[0].setValue(value*100); me.valueSprite.setText(' '+(value*100).toFixed(0) + '%'); attr.x = me.chart.getWidth()/2; attr.y = me.chart.getHeight()-20; if (me.spriteFontSize) { attr.fontSize = me.spriteFontSize; } me.valueSprite.setAttributes(attr, true); if (text !== undefined) { me.text.setHtml(text); } }, initComponent: function() { var me = this; me.callParent(); if (me.title) { me.getComponent('title').update({title: me.title}); } me.text = me.getComponent('text'); me.chart = me.getComponent('chart'); me.valueSprite = me.chart.getSurface('chart').get('valueSprite'); } }); // fixme: how can we avoid those lint errors? /*jslint confusion: true */ Ext.define('Proxmox.window.Edit', { extend: 'Ext.window.Window', alias: 'widget.proxmoxWindowEdit', // autoLoad trigger a load() after component creation autoLoad: false, resizable: false, // use this tio atimatically generate a title like // Create: subject: undefined, // set isCreate to true if you want a Create button (instead // OK and RESET) isCreate: false, // set to true if you want an Add button (instead of Create) isAdd: false, // set to true if you want an Remove button (instead of Create) isRemove: false, // custom submitText submitText: undefined, backgroundDelay: 0, // needed for finding the reference to submitbutton // because we do not have a controller referenceHolder: true, defaultButton: 'submitbutton', // finds the first form field defaultFocus: 'field[disabled=false][hidden=false]', showProgress: false, showTaskViewer: false, // gets called if we have a progress bar or taskview and it detected that // the task finished. function(success) taskDone: Ext.emptyFn, // gets called when the api call is finished, right at the beginning // function(success, response, options) apiCallDone: Ext.emptyFn, // assign a reference from docs, to add a help button docked to the // bottom of the window. If undefined we magically fall back to the // onlineHelp of our first item, if set. onlineHelp: undefined, isValid: function() { var me = this; var form = me.formPanel.getForm(); return form.isValid(); }, getValues: function(dirtyOnly) { var me = this; var values = {}; var form = me.formPanel.getForm(); form.getFields().each(function(field) { if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); } }); Ext.Array.each(me.query('inputpanel'), function(panel) { Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); }); return values; }, setValues: function(values) { var me = this; var form = me.formPanel.getForm(); Ext.iterate(values, function(fieldId, val) { var field = form.findField(fieldId); if (field && !field.up('inputpanel')) { field.setValue(val); if (form.trackResetOnLoad) { field.resetOriginalValue(); } } }); Ext.Array.each(me.query('inputpanel'), function(panel) { panel.setValues(values); }); }, submit: function() { var me = this; var form = me.formPanel.getForm(); var values = me.getValues(); Ext.Object.each(values, function(name, val) { if (values.hasOwnProperty(name)) { if (Ext.isArray(val) && !val.length) { values[name] = ''; } } }); if (me.digest) { values.digest = me.digest; } if (me.backgroundDelay) { values.background_delay = me.backgroundDelay; } var url = me.url; if (me.method === 'DELETE') { url = url + "?" + Ext.Object.toQueryString(values); values = undefined; } Proxmox.Utils.API2Request({ url: url, waitMsgTarget: me, method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'), params: values, failure: function(response, options) { me.apiCallDone(false, response, options); if (response.result && response.result.errors) { form.markInvalid(response.result.errors); } Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, options) { var hasProgressBar = (me.backgroundDelay || me.showProgress || me.showTaskViewer) && response.result.data ? true : false; me.apiCallDone(true, response, options); if (hasProgressBar) { // stay around so we can trigger our close events // when background action is completed me.hide(); var upid = response.result.data; var viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress'; var win = Ext.create('Proxmox.window.Task' + viewerClass, { upid: upid, taskDone: me.taskDone, listeners: { destroy: function () { me.close(); } } }); win.show(); } else { me.close(); } } }); }, load: function(options) { var me = this; var form = me.formPanel.getForm(); options = options || {}; var newopts = Ext.apply({ waitMsgTarget: me }, options); var createWrapper = function(successFn) { Ext.apply(newopts, { url: me.url, method: 'GET', success: function(response, opts) { form.clearInvalid(); me.digest = response.result.data.digest; if (successFn) { successFn(response, opts); } else { me.setValues(response.result.data); } // hack: fix ExtJS bug Ext.Array.each(me.query('radiofield'), function(f) { f.resetOriginalValue(); }); }, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() { me.close(); }); } }); }; createWrapper(options.success); Proxmox.Utils.API2Request(newopts); }, initComponent : function() { var me = this; if (!me.url) { throw "no url specified"; } if (me.create) {throw "deprecated parameter, use isCreate";} var items = Ext.isArray(me.items) ? me.items : [ me.items ]; me.items = undefined; me.formPanel = Ext.create('Ext.form.Panel', { url: me.url, method: me.method || 'PUT', trackResetOnLoad: true, bodyPadding: 10, border: false, defaults: Ext.apply({}, me.defaults, { border: false }), fieldDefaults: Ext.apply({}, me.fieldDefaults, { labelWidth: 100, anchor: '100%' }), items: items }); var inputPanel = me.formPanel.down('inputpanel'); var form = me.formPanel.getForm(); var submitText; if (me.isCreate) { if (me.submitText) { submitText = me.submitText; } else if (me.isAdd) { submitText = gettext('Add'); } else if (me.isRemove) { submitText = gettext('Remove'); } else { submitText = gettext('Create'); } } else { submitText = me.submitText || gettext('OK'); } var submitBtn = Ext.create('Ext.Button', { reference: 'submitbutton', text: submitText, disabled: !me.isCreate, handler: function() { me.submit(); } }); var resetBtn = Ext.create('Ext.Button', { text: 'Reset', disabled: true, handler: function(){ form.reset(); } }); var set_button_status = function() { var valid = form.isValid(); var dirty = form.isDirty(); submitBtn.setDisabled(!valid || !(dirty || me.isCreate)); resetBtn.setDisabled(!dirty); if (inputPanel && inputPanel.hasAdvanced) { // we want to show the advanced options // as soon as some of it is not valid var advancedItems = me.down('#advancedContainer').query('field'); var valid = true; advancedItems.forEach(function(field) { if (!field.isValid()) { valid = false; } }); if (!valid) { inputPanel.setAdvancedVisible(true); me.down('#advancedcb').setValue(true); } } }; form.on('dirtychange', set_button_status); form.on('validitychange', set_button_status); var colwidth = 300; if (me.fieldDefaults && me.fieldDefaults.labelWidth) { colwidth += me.fieldDefaults.labelWidth - 100; } var twoColumn = inputPanel && (inputPanel.column1 || inputPanel.column2); if (me.subject && !me.title) { me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd); } if (me.isCreate) { me.buttons = [ submitBtn ] ; } else { me.buttons = [ submitBtn, resetBtn ]; } if (inputPanel && inputPanel.hasAdvanced) { var sp = Ext.state.Manager.getProvider(); var advchecked = sp.get('proxmox-advanced-cb'); inputPanel.setAdvancedVisible(advchecked); me.buttons.unshift( { xtype: 'proxmoxcheckbox', itemId: 'advancedcb', boxLabelAlign: 'before', boxLabel: gettext('Advanced'), stateId: 'proxmox-advanced-cb', value: advchecked, listeners: { change: function(cb, val) { inputPanel.setAdvancedVisible(val); sp.set('proxmox-advanced-cb', val); } } } ); } var onlineHelp = me.onlineHelp; if (!onlineHelp && inputPanel && inputPanel.onlineHelp) { onlineHelp = inputPanel.onlineHelp; } if (onlineHelp) { var helpButton = Ext.create('Proxmox.button.Help'); me.buttons.unshift(helpButton, '->'); Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp); } Ext.applyIf(me, { modal: true, width: twoColumn ? colwidth*2 : colwidth, border: false, items: [ me.formPanel ] }); me.callParent(); // always mark invalid fields me.on('afterlayout', function() { // on touch devices, the isValid function // triggers a layout, which triggers an isValid // and so on // to prevent this we disable the layouting here // and enable it afterwards me.suspendLayout = true; me.isValid(); me.suspendLayout = false; }); if (me.autoLoad) { me.load(); } } }); Ext.define('Proxmox.window.PasswordEdit', { extend: 'Proxmox.window.Edit', alias: 'proxmoxWindowPasswordEdit', subject: gettext('Password'), url: '/api2/extjs/access/password', fieldDefaults: { labelWidth: 120 }, items: [ { xtype: 'textfield', inputType: 'password', fieldLabel: gettext('Password'), minLength: 5, allowBlank: false, name: 'password', listeners: { change: function(field){ field.next().validate(); }, blur: function(field){ field.next().validate(); } } }, { xtype: 'textfield', inputType: 'password', fieldLabel: gettext('Confirm password'), name: 'verifypassword', allowBlank: false, vtype: 'password', initialPassField: 'password', submitValue: false }, { xtype: 'hiddenfield', name: 'userid' } ], initComponent : function() { var me = this; if (!me.userid) { throw "no userid specified"; } me.callParent(); me.down('[name=userid]').setValue(me.userid); } }); Ext.define('Proxmox.window.TaskProgress', { extend: 'Ext.window.Window', alias: 'widget.proxmoxTaskProgress', taskDone: Ext.emptyFn, initComponent: function() { var me = this; if (!me.upid) { throw "no task specified"; } var task = Proxmox.Utils.parse_task_upid(me.upid); var statstore = Ext.create('Proxmox.data.ObjectStore', { url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", interval: 1000, rows: { status: { defaultValue: 'unknown' }, exitstatus: { defaultValue: 'unknown' } } }); me.on('destroy', statstore.stopUpdate); var getObjectValue = function(key, defaultValue) { var rec = statstore.getById(key); if (rec) { return rec.data.value; } return defaultValue; }; var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' }); me.mon(statstore, 'load', function() { var status = getObjectValue('status'); if (status === 'stopped') { var exitstatus = getObjectValue('exitstatus'); if (exitstatus == 'OK') { pbar.reset(); pbar.updateText("Done!"); Ext.Function.defer(me.close, 1000, me); } else { me.close(); Ext.Msg.alert('Task failed', exitstatus); } me.taskDone(exitstatus == 'OK'); } }); var descr = Proxmox.Utils.format_task_description(task.type, task.id); Ext.apply(me, { title: gettext('Task') + ': ' + descr, width: 300, layout: 'auto', modal: true, bodyPadding: 5, items: pbar, buttons: [ { text: gettext('Details'), handler: function() { var win = Ext.create('Proxmox.window.TaskViewer', { taskDone: me.taskDone, upid: me.upid }); win.show(); me.close(); } } ] }); me.callParent(); statstore.startUpdate(); pbar.wait(); } }); // fixme: how can we avoid those lint errors? /*jslint confusion: true */ Ext.define('Proxmox.window.TaskViewer', { extend: 'Ext.window.Window', alias: 'widget.proxmoxTaskViewer', extraTitle: '', // string to prepend after the generic task title taskDone: Ext.emptyFn, initComponent: function() { var me = this; if (!me.upid) { throw "no task specified"; } var task = Proxmox.Utils.parse_task_upid(me.upid); var statgrid; var rows = { status: { header: gettext('Status'), defaultValue: 'unknown', renderer: function(value) { if (value != 'stopped') { return value; } var es = statgrid.getObjectValue('exitstatus'); if (es) { return value + ': ' + es; } } }, exitstatus: { visible: false }, type: { header: gettext('Task type'), required: true }, user: { header: gettext('User name'), required: true }, node: { header: gettext('Node'), required: true }, pid: { header: gettext('Process ID'), required: true }, starttime: { header: gettext('Start Time'), required: true, renderer: Proxmox.Utils.render_timestamp }, upid: { header: gettext('Unique task ID') } }; var statstore = Ext.create('Proxmox.data.ObjectStore', { url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", interval: 1000, rows: rows }); me.on('destroy', statstore.stopUpdate); var stop_task = function() { Proxmox.Utils.API2Request({ url: "/nodes/" + task.node + "/tasks/" + me.upid, waitMsgTarget: me, method: 'DELETE', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); }; var stop_btn1 = new Ext.Button({ text: gettext('Stop'), disabled: true, handler: stop_task }); var stop_btn2 = new Ext.Button({ text: gettext('Stop'), disabled: true, handler: stop_task }); statgrid = Ext.create('Proxmox.grid.ObjectGrid', { title: gettext('Status'), layout: 'fit', tbar: [ stop_btn1 ], rstore: statstore, rows: rows, border: false }); var logView = Ext.create('Proxmox.panel.LogView', { title: gettext('Output'), tbar: [ stop_btn2 ], border: false, url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log" }); me.mon(statstore, 'load', function() { var status = statgrid.getObjectValue('status'); if (status === 'stopped') { logView.scrollToEnd = false; logView.requestUpdate(); statstore.stopUpdate(); me.taskDone(statgrid.getObjectValue('exitstatus') == 'OK'); } stop_btn1.setDisabled(status !== 'running'); stop_btn2.setDisabled(status !== 'running'); }); statstore.startUpdate(); Ext.apply(me, { title: "Task viewer: " + task.desc + me.extraTitle, width: 800, height: 400, layout: 'fit', modal: true, items: [{ xtype: 'tabpanel', region: 'center', items: [ logView, statgrid ] }] }); me.callParent(); logView.fireEvent('show', logView); } }); Ext.define('apt-pkglist', { extend: 'Ext.data.Model', fields: [ 'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin' ], idProperty: 'Package' }); Ext.define('Proxmox.node.APT', { extend: 'Ext.grid.GridPanel', xtype: 'proxmoxNodeAPT', upgradeBtn: undefined, columns: [ { header: gettext('Package'), width: 200, sortable: true, dataIndex: 'Package' }, { text: gettext('Version'), columns: [ { header: gettext('current'), width: 100, sortable: false, dataIndex: 'OldVersion' }, { header: gettext('new'), width: 100, sortable: false, dataIndex: 'Version' } ] }, { header: gettext('Description'), sortable: false, dataIndex: 'Title', flex: 1 } ], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var store = Ext.create('Ext.data.Store', { model: 'apt-pkglist', groupField: 'Origin', proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + "/apt/update" }, sorters: [ { property : 'Package', direction: 'ASC' } ] }); var groupingFeature = Ext.create('Ext.grid.feature.Grouping', { groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', enableGroupingMenu: false }); var rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', { getAdditionalData: function (data, rowIndex, record, orig) { var headerCt = this.view.headerCt; var colspan = headerCt.getColumnCount(); // Usually you would style the my-body-class in CSS file return { rowBody: '
' + Ext.String.htmlEncode(data.Description) + '
', rowBodyColspan: colspan }; } }); var reload = function() { store.load(); }; Proxmox.Utils.monStoreErrors(me, store, true); var apt_command = function(cmd){ Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/apt/" + cmd, method: 'POST', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, opts) { var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid }); win.show(); me.mon(win, 'close', reload); } }); }; var sm = Ext.create('Ext.selection.RowModel', {}); var update_btn = new Ext.Button({ text: gettext('Refresh'), handler: function(){ Proxmox.Utils.checked_command(function() { apt_command('update'); }); } }); var show_changelog = function(rec) { if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { return; } var view = Ext.createWidget('component', { autoScroll: true, style: { 'background-color': 'white', 'white-space': 'pre', 'font-family': 'monospace', padding: '5px' } }); var win = Ext.create('Ext.window.Window', { title: gettext('Changelog') + ": " + rec.data.Package, width: 800, height: 400, layout: 'fit', modal: true, items: [ view ] }); Proxmox.Utils.API2Request({ waitMsgTarget: me, url: "/nodes/" + me.nodename + "/apt/changelog", params: { name: rec.data.Package, version: rec.data.Version }, method: 'GET', failure: function(response, opts) { win.close(); Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, opts) { win.show(); view.update(Ext.htmlEncode(response.result.data)); } }); }; var changelog_btn = new Proxmox.button.Button({ text: gettext('Changelog'), selModel: sm, disabled: true, enableFn: function(rec) { if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { return false; } return true; }, handler: function(b, e, rec) { show_changelog(rec); } }); if (me.upgradeBtn) { me.tbar = [ update_btn, me.upgradeBtn, changelog_btn ]; } else { me.tbar = [ update_btn, changelog_btn ]; } Ext.apply(me, { store: store, stateful: true, stateId: 'grid-update', selModel: sm, viewConfig: { stripeRows: false, emptyText: '
' + gettext('No updates available.') + '
' }, features: [ groupingFeature, rowBodyFeature ], listeners: { activate: reload, itemdblclick: function(v, rec) { show_changelog(rec); } } }); me.callParent(); } }); Ext.define('Proxmox.node.NetworkEdit', { extend: 'Proxmox.window.Edit', alias: ['widget.proxmoxNodeNetworkEdit'], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.iftype) { throw "no network device type specified"; } me.isCreate = !me.iface; var iface_vtype; if (me.iftype === 'bridge') { iface_vtype = 'BridgeName'; } else if (me.iftype === 'bond') { iface_vtype = 'BondName'; } else if (me.iftype === 'eth' && !me.isCreate) { iface_vtype = 'InterfaceName'; } else if (me.iftype === 'vlan' && !me.isCreate) { iface_vtype = 'InterfaceName'; } else if (me.iftype === 'OVSBridge') { iface_vtype = 'BridgeName'; } else if (me.iftype === 'OVSBond') { iface_vtype = 'BondName'; } else if (me.iftype === 'OVSIntPort') { iface_vtype = 'InterfaceName'; } else if (me.iftype === 'OVSPort') { iface_vtype = 'InterfaceName'; } else { console.log(me.iftype); throw "unknown network device type specified"; } me.subject = Proxmox.Utils.render_network_iface_type(me.iftype); var column2 = []; if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || me.iftype === 'OVSBond')) { column2.push({ xtype: 'proxmoxcheckbox', fieldLabel: gettext('Autostart'), name: 'autostart', uncheckedValue: 0, checked: me.isCreate ? true : undefined }); } if (me.iftype === 'bridge') { column2.push({ xtype: 'proxmoxcheckbox', fieldLabel: gettext('VLAN aware'), name: 'bridge_vlan_aware', deleteEmpty: !me.isCreate }); column2.push({ xtype: 'textfield', fieldLabel: gettext('Bridge ports'), name: 'bridge_ports' }); } else if (me.iftype === 'OVSBridge') { column2.push({ xtype: 'textfield', fieldLabel: gettext('Bridge ports'), name: 'ovs_ports' }); column2.push({ xtype: 'textfield', fieldLabel: gettext('OVS options'), name: 'ovs_options' }); } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') { column2.push({ xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), allowBlank: false, nodename: me.nodename, bridgeType: 'OVSBridge', name: 'ovs_bridge' }); column2.push({ xtype: 'pveVlanField', deleteEmpty: !me.isCreate, name: 'ovs_tag', value: '' }); column2.push({ xtype: 'textfield', fieldLabel: gettext('OVS options'), name: 'ovs_options' }); } else if (me.iftype === 'bond') { column2.push({ xtype: 'textfield', fieldLabel: gettext('Slaves'), name: 'slaves' }); var policySelector = Ext.createWidget('bondPolicySelector', { fieldLabel: gettext('Hash policy'), name: 'bond_xmit_hash_policy', deleteEmpty: !me.isCreate, disabled: true }); column2.push({ xtype: 'bondModeSelector', fieldLabel: gettext('Mode'), name: 'bond_mode', value: me.isCreate ? 'balance-rr' : undefined, listeners: { change: function(f, value) { if (value === 'balance-xor' || value === '802.3ad') { policySelector.setDisabled(false); } else { policySelector.setDisabled(true); policySelector.setValue(''); } } }, allowBlank: false }); column2.push(policySelector); } else if (me.iftype === 'OVSBond') { column2.push({ xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), allowBlank: false, nodename: me.nodename, bridgeType: 'OVSBridge', name: 'ovs_bridge' }); column2.push({ xtype: 'pveVlanField', deleteEmpty: !me.isCreate, name: 'ovs_tag', value: '' }); column2.push({ xtype: 'textfield', fieldLabel: gettext('OVS options'), name: 'ovs_options' }); } column2.push({ xtype: 'textfield', fieldLabel: gettext('Comment'), allowBlank: true, nodename: me.nodename, name: 'comments' }); var url; var method; if (me.isCreate) { url = "/api2/extjs/nodes/" + me.nodename + "/network"; method = 'POST'; } else { url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface; method = 'PUT'; } var column1 = [ { xtype: 'hiddenfield', name: 'type', value: me.iftype }, { xtype: me.isCreate ? 'textfield' : 'displayfield', fieldLabel: gettext('Name'), name: 'iface', value: me.iface, vtype: iface_vtype, allowBlank: false } ]; if (me.iftype === 'OVSBond') { column1.push( { xtype: 'bondModeSelector', fieldLabel: gettext('Mode'), name: 'bond_mode', openvswitch: true, value: me.isCreate ? 'active-backup' : undefined, allowBlank: false }, { xtype: 'textfield', fieldLabel: gettext('Slaves'), name: 'ovs_bonds' } ); } else { column1.push( { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('IP address'), vtype: 'IPAddress', name: 'address' }, { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('Subnet mask'), vtype: 'IPAddress', name: 'netmask', validator: function(value) { /*jslint confusion: true */ if (!me.items) { return true; } var address = me.down('field[name=address]').getValue(); if (value !== '') { if (address === '') { return "Subnet mask requires option 'IP address'"; } } else { if (address !== '') { return "Option 'IP address' requires a subnet mask"; } } return true; } }, { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('Gateway'), vtype: 'IPAddress', name: 'gateway' }, { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('IPv6 address'), vtype: 'IP6Address', name: 'address6' }, { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('Prefix length'), vtype: 'IP6PrefixLength', name: 'netmask6', value: '', allowBlank: true, validator: function(value) { /*jslint confusion: true */ if (!me.items) { return true; } var address = me.down('field[name=address6]').getValue(); if (value !== '') { if (address === '') { return "IPv6 prefix length requires option 'IPv6 address'"; } } else { if (address !== '') { return "Option 'IPv6 address' requires an IPv6 prefix length"; } } return true; } }, { xtype: 'proxmoxtextfield', deleteEmpty: !me.isCreate, fieldLabel: gettext('Gateway'), vtype: 'IP6Address', name: 'gateway6' } ); } Ext.applyIf(me, { url: url, method: method, items: { xtype: 'inputpanel', column1: column1, column2: column2 } }); me.callParent(); if (me.isCreate) { me.down('field[name=iface]').setValue(me.iface_default); } else { me.load({ success: function(response, options) { var data = response.result.data; if (data.type !== me.iftype) { var msg = "Got unexpected device type"; Ext.Msg.alert(gettext('Error'), msg, function() { me.close(); }); return; } me.setValues(data); me.isValid(); // trigger validation } }); } } }); Ext.define('proxmox-networks', { extend: 'Ext.data.Model', fields: [ 'iface', 'type', 'active', 'autostart', 'bridge_ports', 'slaves', 'address', 'netmask', 'gateway', 'address6', 'netmask6', 'gateway6', 'comments' ], idProperty: 'iface' }); Ext.define('Proxmox.node.NetworkView', { extend: 'Ext.panel.Panel', alias: ['widget.proxmoxNodeNetworkView'], // defines what types of network devices we want to create // order is always the same types: ['bridge', 'bond', 'ovs'], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var baseUrl = '/nodes/' + me.nodename + '/network'; var store = Ext.create('Ext.data.Store', { model: 'proxmox-networks', proxy: { type: 'proxmox', url: '/api2/json' + baseUrl }, sorters: [ { property : 'iface', direction: 'ASC' } ] }); var reload = function() { var changeitem = me.down('#changes'); Proxmox.Utils.API2Request({ url: baseUrl, failure: function(response, opts) { store.loadData({}); Proxmox.Utils.setErrorMask(me, response.htmlStatus); changeitem.update(''); changeitem.setHidden(true); }, success: function(response, opts) { var result = Ext.decode(response.responseText); store.loadData(result.data); var changes = result.changes; if (changes === undefined || changes === '') { changes = gettext("No changes"); changeitem.setHidden(true); } else { changeitem.update("
" + Ext.htmlEncode(changes) + "
"); changeitem.setHidden(false); } } }); }; var run_editor = function() { var grid = me.down('gridpanel'); var sm = grid.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { return; } var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iface: rec.data.iface, iftype: rec.data.type }); win.show(); win.on('destroy', reload); }; var edit_btn = new Ext.Button({ text: gettext('Edit'), disabled: true, handler: run_editor }); var del_btn = new Ext.Button({ text: gettext('Remove'), disabled: true, handler: function(){ var grid = me.down('gridpanel'); var sm = grid.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { return; } var iface = rec.data.iface; Proxmox.Utils.API2Request({ url: baseUrl + '/' + iface, method: 'DELETE', waitMsgTarget: me, callback: function() { reload(); }, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } }); var set_button_status = function() { var grid = me.down('gridpanel'); var sm = grid.getSelectionModel(); var rec = sm.getSelection()[0]; edit_btn.setDisabled(!rec); del_btn.setDisabled(!rec); }; var render_ports = function(value, metaData, record) { if (value === 'bridge') { return record.data.bridge_ports; } else if (value === 'bond') { return record.data.slaves; } else if (value === 'OVSBridge') { return record.data.ovs_ports; } else if (value === 'OVSBond') { return record.data.ovs_bonds; } }; var find_next_iface_id = function(prefix) { var next; for (next = 0; next <= 9999; next++) { if (!store.getById(prefix + next.toString())) { break; } } return prefix + next.toString(); }; var menu_items = []; if (me.types.indexOf('bridge') !== -1) { menu_items.push({ text: Proxmox.Utils.render_network_iface_type('bridge'), handler: function() { var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iftype: 'bridge', iface_default: find_next_iface_id('vmbr') }); win.on('destroy', reload); win.show(); } }); } if (me.types.indexOf('bond') !== -1) { menu_items.push({ text: Proxmox.Utils.render_network_iface_type('bond'), handler: function() { var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iftype: 'bond', iface_default: find_next_iface_id('bond') }); win.on('destroy', reload); win.show(); } }); } if (me.types.indexOf('ovs') !== -1) { if (menu_items.length > 0) { menu_items.push({ xtype: 'menuseparator' }); } menu_items.push( { text: Proxmox.Utils.render_network_iface_type('OVSBridge'), handler: function() { var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iftype: 'OVSBridge', iface_default: find_next_iface_id('vmbr') }); win.on('destroy', reload); win.show(); } }, { text: Proxmox.Utils.render_network_iface_type('OVSBond'), handler: function() { var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iftype: 'OVSBond', iface_default: find_next_iface_id('bond') }); win.on('destroy', reload); win.show(); } }, { text: Proxmox.Utils.render_network_iface_type('OVSIntPort'), handler: function() { var win = Ext.create('Proxmox.node.NetworkEdit', { nodename: me.nodename, iftype: 'OVSIntPort' }); win.on('destroy', reload); win.show(); } } ); } Ext.apply(me, { layout: 'border', tbar: [ { text: gettext('Create'), menu: { plain: true, items: menu_items } }, ' ', { text: gettext('Revert'), handler: function() { Proxmox.Utils.API2Request({ url: baseUrl, method: 'DELETE', waitMsgTarget: me, callback: function() { reload(); }, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } }, edit_btn, del_btn ], items: [ { xtype: 'gridpanel', stateful: true, stateId: 'grid-node-network', store: store, region: 'center', border: false, columns: [ { header: gettext('Name'), sortable: true, dataIndex: 'iface' }, { header: gettext('Type'), sortable: true, width: 120, renderer: Proxmox.Utils.render_network_iface_type, dataIndex: 'type' }, { xtype: 'booleancolumn', header: gettext('Active'), width: 80, sortable: true, dataIndex: 'active', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText, }, { xtype: 'booleancolumn', header: gettext('Autostart'), width: 80, sortable: true, dataIndex: 'autostart', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText }, { xtype: 'booleancolumn', header: gettext('VLAN aware'), width: 80, sortable: true, dataIndex: 'bridge_vlan_aware', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText }, { header: gettext('Ports/Slaves'), dataIndex: 'type', renderer: render_ports }, { header: gettext('IP address'), sortable: true, width: 120, dataIndex: 'address', renderer: function(value, metaData, rec) { if (rec.data.address && rec.data.address6) { return rec.data.address + "
" + rec.data.address6 + '/' + rec.data.netmask6; } else if (rec.data.address6) { return rec.data.address6 + '/' + rec.data.netmask6; } else { return rec.data.address; } } }, { header: gettext('Subnet mask'), width: 120, sortable: true, dataIndex: 'netmask' }, { header: gettext('Gateway'), width: 120, sortable: true, dataIndex: 'gateway', renderer: function(value, metaData, rec) { if (rec.data.gateway && rec.data.gateway6) { return rec.data.gateway + "
" + rec.data.gateway6; } else if (rec.data.gateway6) { return rec.data.gateway6; } else { return rec.data.gateway; } } }, { header: gettext('Comment'), dataIndex: 'comments', flex: 1, renderer: Ext.String.htmlEncode } ], listeners: { selectionchange: set_button_status, itemdblclick: run_editor } }, { border: false, region: 'south', autoScroll: true, hidden: true, itemId: 'changes', tbar: [ gettext('Pending changes') + ' (' + gettext('Please reboot to activate changes') + ')' ], split: true, bodyPadding: 5, flex: 0.6, html: gettext("No changes") } ], }); me.callParent(); reload(); } }); Ext.define('Proxmox.node.DNSEdit', { extend: 'Proxmox.window.Edit', alias: ['widget.proxmoxNodeDNSEdit'], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.items = [ { xtype: 'textfield', fieldLabel: gettext('Search domain'), name: 'search', allowBlank: false }, { xtype: 'proxmoxtextfield', fieldLabel: gettext('DNS server') + " 1", vtype: 'IP64Address', skipEmptyText: true, name: 'dns1' }, { xtype: 'proxmoxtextfield', fieldLabel: gettext('DNS server') + " 2", vtype: 'IP64Address', skipEmptyText: true, name: 'dns2' }, { xtype: 'proxmoxtextfield', fieldLabel: gettext('DNS server') + " 3", vtype: 'IP64Address', skipEmptyText: true, name: 'dns3' } ]; Ext.applyIf(me, { subject: gettext('DNS'), url: "/api2/extjs/nodes/" + me.nodename + "/dns", fieldDefaults: { labelWidth: 120 } }); me.callParent(); me.load(); } }); Ext.define('Proxmox.node.HostsView', { extend: 'Ext.panel.Panel', xtype: 'proxmoxNodeHostsView', reload: function() { var me = this; me.store.load(); }, tbar: [ { text: gettext('Save'), disabled: true, itemId: 'savebtn', handler: function() { var me = this.up('panel'); Proxmox.Utils.API2Request({ params: { digest: me.digest, data: me.down('#hostsfield').getValue() }, method: 'POST', url: '/nodes/' + me.nodename + '/hosts', waitMsgTarget: me, success: function(response, opts) { me.reload(); }, failure: function(response, opts) { Ext.Msg.alert('Error', response.htmlStatus); } }); } }, { text: gettext('Revert'), disabled: true, itemId: 'resetbtn', handler: function() { var me = this.up('panel'); me.down('#hostsfield').reset(); } } ], layout: 'fit', items: [ { xtype: 'textarea', itemId: 'hostsfield', fieldStyle: { 'font-family': 'monospace', 'white-space': 'pre' }, listeners: { dirtychange: function(ta, dirty) { var me = this.up('panel'); me.down('#savebtn').setDisabled(!dirty); me.down('#resetbtn').setDisabled(!dirty); } } } ], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.store = Ext.create('Ext.data.Store', { proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + "/hosts", } }); me.callParent(); Proxmox.Utils.monStoreErrors(me, me.store); me.mon(me.store, 'load', function(store, records, success) { if (!success || records.length < 1) { return; } me.digest = records[0].data.digest; var data = records[0].data.data; me.down('#hostsfield').setValue(data); me.down('#hostsfield').resetOriginalValue(); }); me.reload(); } }); Ext.define('Proxmox.node.DNSView', { extend: 'Proxmox.grid.ObjectGrid', alias: ['widget.proxmoxNodeDNSView'], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var run_editor = function() { var win = Ext.create('Proxmox.node.DNSEdit', { nodename: me.nodename }); win.show(); }; Ext.apply(me, { url: "/api2/json/nodes/" + me.nodename + "/dns", cwidth1: 130, interval: 1000, run_editor: run_editor, rows: { search: { header: 'Search domain', required: true, renderer: Ext.htmlEncode }, dns1: { header: gettext('DNS server') + " 1", required: true, renderer: Ext.htmlEncode }, dns2: { header: gettext('DNS server') + " 2", renderer: Ext.htmlEncode }, dns3: { header: gettext('DNS server') + " 3", renderer: Ext.htmlEncode } }, tbar: [ { text: gettext("Edit"), handler: run_editor } ], listeners: { itemdblclick: run_editor } }); me.callParent(); me.on('activate', me.rstore.startUpdate); me.on('deactivate', me.rstore.stopUpdate); me.on('destroy', me.rstore.stopUpdate); } }); Ext.define('Proxmox.node.Tasks', { extend: 'Ext.grid.GridPanel', alias: ['widget.proxmoxNodeTasks'], stateful: true, stateId: 'grid-node-tasks', loadMask: true, sortableColumns: false, vmidFilter: 0, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var store = Ext.create('Ext.data.BufferedStore', { pageSize: 500, autoLoad: true, remoteFilter: true, model: 'proxmox-tasks', proxy: { type: 'proxmox', startParam: 'start', limitParam: 'limit', url: "/api2/json/nodes/" + me.nodename + "/tasks" } }); var userfilter = ''; var filter_errors = 0; var updateProxyParams = function() { var params = { errors: filter_errors }; if (userfilter) { params.userfilter = userfilter; } if (me.vmidFilter) { params.vmid = me.vmidFilter; } store.proxy.extraParams = params; }; updateProxyParams(); var reload_task = Ext.create('Ext.util.DelayedTask',function() { updateProxyParams(); store.reload(); }); var run_task_viewer = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { return; } var win = Ext.create('Proxmox.window.TaskViewer', { upid: rec.data.upid }); win.show(); }; var view_btn = new Ext.Button({ text: gettext('View'), disabled: true, handler: run_task_viewer }); Proxmox.Utils.monStoreErrors(me, store, true); Ext.apply(me, { store: store, viewConfig: { trackOver: false, stripeRows: false, // does not work with getRowClass() getRowClass: function(record, index) { var status = record.get('status'); if (status && status != 'OK') { return "proxmox-invalid-row"; } } }, tbar: [ view_btn, '->', gettext('User name') +':', ' ', { xtype: 'textfield', width: 200, value: userfilter, enableKeyEvents: true, listeners: { keyup: function(field, e) { userfilter = field.getValue(); reload_task.delay(500); } } }, ' ', gettext('Only Errors') + ':', ' ', { xtype: 'checkbox', hideLabel: true, checked: filter_errors, listeners: { change: function(field, checked) { filter_errors = checked ? 1 : 0; reload_task.delay(10); } } }, ' ' ], columns: [ { header: gettext("Start Time"), dataIndex: 'starttime', width: 100, renderer: function(value) { return Ext.Date.format(value, "M d H:i:s"); } }, { header: gettext("End Time"), dataIndex: 'endtime', width: 100, renderer: function(value, metaData, record) { return Ext.Date.format(value,"M d H:i:s"); } }, { header: gettext("Node"), dataIndex: 'node', width: 100 }, { header: gettext("User name"), dataIndex: 'user', width: 150 }, { header: gettext("Description"), dataIndex: 'upid', flex: 1, renderer: Proxmox.Utils.render_upid }, { header: gettext("Status"), dataIndex: 'status', width: 200, renderer: function(value, metaData, record) { if (value == 'OK') { return 'OK'; } // metaData.attr = 'style="color:red;"'; return "ERROR: " + value; } } ], listeners: { itemdblclick: run_task_viewer, selectionchange: function(v, selections) { view_btn.setDisabled(!(selections && selections[0])); }, show: function() { reload_task.delay(10); }, destroy: function() { reload_task.cancel(); } } }); me.callParent(); } }); Ext.define('proxmox-services', { extend: 'Ext.data.Model', fields: [ 'service', 'name', 'desc', 'state' ], idProperty: 'service' }); Ext.define('Proxmox.node.ServiceView', { extend: 'Ext.grid.GridPanel', alias: ['widget.proxmoxNodeServiceView'], startOnlyServices: {}, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var rstore = Ext.create('Proxmox.data.UpdateStore', { interval: 1000, storeid: 'proxmox-services' + me.nodename, model: 'proxmox-services', proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + "/services" } }); var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore, sortAfterUpdate: true, sorters: [ { property : 'name', direction: 'ASC' } ] }); var view_service_log = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; var win = Ext.create('Ext.window.Window', { title: gettext('Syslog') + ': ' + rec.data.service, modal: true, items: { xtype: 'proxmoxLogView', width: 800, height: 400, url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" + rec.data.service, log_select_timespan: 1 } }); win.show(); }; var service_cmd = function(cmd) { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd, method: 'POST', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); me.loading = true; }, success: function(response, opts) { rstore.startUpdate(); var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); win.show(); } }); }; var start_btn = new Ext.Button({ text: gettext('Start'), disabled: true, handler: function(){ service_cmd("start"); } }); var stop_btn = new Ext.Button({ text: gettext('Stop'), disabled: true, handler: function(){ service_cmd("stop"); } }); var restart_btn = new Ext.Button({ text: gettext('Restart'), disabled: true, handler: function(){ service_cmd("restart"); } }); var syslog_btn = new Ext.Button({ text: gettext('Syslog'), disabled: true, handler: view_service_log }); var set_button_status = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { start_btn.disable(); stop_btn.disable(); restart_btn.disable(); syslog_btn.disable(); return; } var service = rec.data.service; var state = rec.data.state; syslog_btn.enable(); if (me.startOnlyServices[service]) { if (state == 'running') { start_btn.disable(); restart_btn.enable(); } else { start_btn.enable(); restart_btn.disable(); } stop_btn.disable(); } else { if (state == 'running') { start_btn.disable(); restart_btn.enable(); stop_btn.enable(); } else { start_btn.enable(); restart_btn.disable(); stop_btn.disable(); } } }; me.mon(store, 'refresh', set_button_status); Proxmox.Utils.monStoreErrors(me, rstore); Ext.apply(me, { store: store, stateful: false, tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ], columns: [ { header: gettext('Name'), flex: 1, sortable: true, dataIndex: 'name' }, { header: gettext('Status'), width: 100, sortable: true, dataIndex: 'state' }, { header: gettext('Description'), renderer: Ext.String.htmlEncode, dataIndex: 'desc', flex: 2 } ], listeners: { selectionchange: set_button_status, itemdblclick: view_service_log, activate: rstore.startUpdate, destroy: rstore.stopUpdate } }); me.callParent(); } }); Ext.define('Proxmox.node.TimeEdit', { extend: 'Proxmox.window.Edit', alias: ['widget.proxmoxNodeTimeEdit'], subject: gettext('Time zone'), width: 400, autoLoad: true, fieldDefaults: { labelWidth: 70 }, items: { xtype: 'combo', fieldLabel: gettext('Time zone'), name: 'timezone', queryMode: 'local', store: Ext.create('Proxmox.data.TimezoneStore'), displayField: 'zone', forceSelection: true, editable: false, allowBlank: false }, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.url = "/api2/extjs/nodes/" + me.nodename + "/time"; me.callParent(); } }); Ext.define('Proxmox.node.TimeView', { extend: 'Proxmox.grid.ObjectGrid', alias: ['widget.proxmoxNodeTimeView'], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var tzoffset = (new Date()).getTimezoneOffset()*60000; var renderlocaltime = function(value) { var servertime = new Date((value * 1000) + tzoffset); return Ext.Date.format(servertime, 'Y-m-d H:i:s'); }; var run_editor = function() { var win = Ext.create('Proxmox.node.TimeEdit', { nodename: me.nodename }); win.show(); }; Ext.apply(me, { url: "/api2/json/nodes/" + me.nodename + "/time", cwidth1: 150, interval: 1000, run_editor: run_editor, rows: { timezone: { header: gettext('Time zone'), required: true }, localtime: { header: gettext('Server time'), required: true, renderer: renderlocaltime } }, tbar: [ { text: gettext("Edit"), handler: run_editor } ], listeners: { itemdblclick: run_editor } }); me.callParent(); me.on('activate', me.rstore.startUpdate); me.on('deactivate', me.rstore.stopUpdate); me.on('destroy', me.rstore.stopUpdate); } });