You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6758 lines
165 KiB
6758 lines
165 KiB
6 years ago
|
// 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 <a target="_blank" href="{0}">www.proxmox.com</a> 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 += "<br>";
|
||
|
Ext.Object.each(result.errors, function(prop, desc) {
|
||
|
msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
|
||
|
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' + "<br>" + gettext('Valid CIDR Range') + ': 8-32',
|
||
|
IPCIDRAddressMask: /[\d\.\/]/i,
|
||
|
|
||
|
IP6Address: function(v) {
|
||
|
return Proxmox.Utils.IP6_match.test(v);
|
||
|
},
|
||
|
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
|
||
|
IP6AddressMask: /[A-Fa-f0-9:]/,
|
||
|
|
||
|
IP6CIDRAddress: function(v) {
|
||
|
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' + "<br>" + gettext('Valid CIDR Range') + ': 8-128',
|
||
|
IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/,
|
||
|
|
||
|
IP6PrefixLength: function(v) {
|
||
|
return v >= 0 && v <= 128;
|
||
|
},
|
||
|
IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
|
||
|
IP6PrefixLengthMask: /[0-9]/,
|
||
|
|
||
|
IP64Address: function(v) {
|
||
|
return Proxmox.Utils.IP64_match.test(v);
|
||
|
},
|
||
|
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
|
||
|
IP64AddressMask: /[A-Fa-f0-9\.:]/,
|
||
|
|
||
|
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') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
|
||
|
|
||
|
BondName: function(v) {
|
||
|
return (/^bond\d{1,4}$/).test(v);
|
||
|
},
|
||
|
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
|
||
|
|
||
|
InterfaceName: function(v) {
|
||
|
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
|
||
|
},
|
||
|
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "<br />" +
|
||
|
gettext("Minimum characters") + ": 2" + "<br />" +
|
||
|
gettext("Maximum characters") + ": 21" + "<br />" +
|
||
|
gettext("Must start with") + ": 'a-z'",
|
||
|
|
||
|
StorageId: function(v) {
|
||
|
return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v);
|
||
|
},
|
||
|
StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "<br />" +
|
||
|
gettext("Minimum characters") + ": 2" + "<br />" +
|
||
|
gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
|
||
|
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
|
||
|
|
||
|
ConfigId: function(v) {
|
||
|
return (/^[a-z][a-z0-9\_]+$/i).test(v);
|
||
|
},
|
||
|
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "<br />" +
|
||
|
gettext("Minimum characters") + ": 2" + "<br />" +
|
||
|
gettext("Must start with") + ": " + gettext("letter"),
|
||
|
|
||
|
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);
|
||
|
//<debug>
|
||
|
// tell the spec runner to ignore this element when checking if the dom is clean
|
||
|
el.dom.setAttribute('data-sticky', true);
|
||
|
//</debug>
|
||
|
}
|
||
|
|
||
|
return el;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// '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: [
|
||
|
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
|
||
|
],
|
||
|
childEls: ['iframeEl'],
|
||
|
|
||
|
initComponent: function () {
|
||
|
this.callParent();
|
||
|
|
||
|
this.frameName = this.frameName || this.id + '-frame';
|
||
|
},
|
||
|
|
||
|
initEvents : function() {
|
||
|
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 = '<div style="text-decoration: line-through;">'+ current +'</div>';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (pending) {
|
||
|
return current + '<div style="color:red">' + pending + '</div>';
|
||
|
} 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('<br>'), 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 +
|
||
|
'<br>' + 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: '<h3>{title}</h3>'
|
||
|
},
|
||
|
{
|
||
|
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>
|
||
|
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: '<div style="padding: 1em">' +
|
||
|
Ext.String.htmlEncode(data.Description) +
|
||
|
'</div>',
|
||
|
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: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>'
|
||
|
},
|
||
|
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("<pre>" + Ext.htmlEncode(changes) + "</pre>");
|
||
|
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 + "<br>"
|
||
|
+ 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 + "<br>" + 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);
|
||
|
}
|
||
|
});
|