]> git.proxmox.com Git - pve-manager.git/blame - www/mobile/WidgetToolkitUtils.js
website: update external links to www.proxmox.com
[pve-manager.git] / www / mobile / WidgetToolkitUtils.js
CommitLineData
0947b5c3
TL
1// NOTE: copied from widget-toolkit's Utils.js, as we only need a few utils
2// from there, and loading the whole proxmoxlib creates more trouble..
3
4Ext.ns('Proxmox');
5Ext.ns('Proxmox.Setup');
6
7if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) {
8 throw "Proxmox library not initialized";
9}
10
11// avoid errors when running without development tools
12if (!Ext.isDefined(Ext.global.console)) {
13 let console = {
14 dir: function() {
15 // do nothing
16 },
17 log: function() {
18 // do nothing
19 },
20 warn: function() {
21 // do nothing
22 },
23 };
24 Ext.global.console = console;
25}
26
27Ext.Ajax.defaultHeaders = {
28 'Accept': 'application/json',
29};
30
31Ext.Ajax.on('beforerequest', function(conn, options) {
32 if (Proxmox.CSRFPreventionToken) {
33 if (!options.headers) {
34 options.headers = {};
35 }
36 options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
37 }
38 let storedAuth = Proxmox.Utils.getStoredAuth();
39 if (storedAuth.token) {
40 options.headers.Authorization = storedAuth.token;
41 }
42});
43
44Ext.define('Proxmox.Utils', { // a singleton
45utilities: {
46
47 yesText: gettext('Yes'),
48 noText: gettext('No'),
49 enabledText: gettext('Enabled'),
50 disabledText: gettext('Disabled'),
51 noneText: gettext('none'),
52 NoneText: gettext('None'),
53 errorText: gettext('Error'),
54 warningsText: gettext('Warnings'),
55 unknownText: gettext('Unknown'),
56 defaultText: gettext('Default'),
57 daysText: gettext('days'),
58 dayText: gettext('day'),
59 runningText: gettext('running'),
60 stoppedText: gettext('stopped'),
61 neverText: gettext('never'),
62 totalText: gettext('Total'),
63 usedText: gettext('Used'),
64 directoryText: gettext('Directory'),
65 stateText: gettext('State'),
66 groupText: gettext('Group'),
67
68 language_map: { //language map is sorted alphabetically by iso 639-1
69 ar: `العربية - ${gettext("Arabic")}`,
70 ca: `Català - ${gettext("Catalan")}`,
71 da: `Dansk - ${gettext("Danish")}`,
72 de: `Deutsch - ${gettext("German")}`,
73 en: `English - ${gettext("English")}`,
74 es: `Español - ${gettext("Spanish")}`,
75 eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
76 fa: `فارسی - ${gettext("Persian (Farsi)")}`,
77 fr: `Français - ${gettext("French")}`,
78 he: `עברית - ${gettext("Hebrew")}`,
79 it: `Italiano - ${gettext("Italian")}`,
80 ja: `日本語 - ${gettext("Japanese")}`,
81 kr: `한국어 - ${gettext("Korean")}`,
82 nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
83 nl: `Nederlands - ${gettext("Dutch")}`,
84 nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
85 pl: `Polski - ${gettext("Polish")}`,
86 pt_BR: `Português Brasileiro - ${gettext("Portuguese (Brazil)")}`,
87 ru: `Русский - ${gettext("Russian")}`,
88 sl: `Slovenščina - ${gettext("Slovenian")}`,
89 sv: `Svenska - ${gettext("Swedish")}`,
90 tr: `Türkçe - ${gettext("Turkish")}`,
91 zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`,
92 zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`,
93 },
94
95 render_language: function(value) {
96 if (!value || value === '__default__') {
97 return Proxmox.Utils.defaultText + ' (English)';
98 }
99 let text = Proxmox.Utils.language_map[value];
100 if (text) {
101 return text + ' (' + value + ')';
102 }
103 return value;
104 },
105
106 renderEnabledIcon: enabled => `<i class="fa fa-${enabled ? 'check' : 'minus'}"></i>`,
107
108 language_array: function() {
109 let data = [['__default__', Proxmox.Utils.render_language('')]];
110 Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
111 data.push([key, Proxmox.Utils.render_language(value)]);
112 });
113
114 return data;
115 },
116
117 getNoSubKeyHtml: function(url) {
0947b5c3
TL
118 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 || 'https://www.proxmox.com');
119 },
120
121 format_boolean_with_default: function(value) {
122 if (Ext.isDefined(value) && value !== '__default__') {
123 return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
124 }
125 return Proxmox.Utils.defaultText;
126 },
127
128 format_boolean: function(value) {
129 return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
130 },
131
132 format_neg_boolean: function(value) {
133 return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
134 },
135
136 format_enabled_toggle: function(value) {
137 return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText;
138 },
139
140 format_expire: function(date) {
141 if (!date) {
142 return Proxmox.Utils.neverText;
143 }
144 return Ext.Date.format(date, "Y-m-d");
145 },
146
147 // somewhat like a human would tell durations, omit zero values and do not
148 // give seconds precision if we talk days already
149 format_duration_human: function(ut) {
150 let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0;
151
152 if (ut <= 0.1) {
153 return '<0.1s';
154 }
155
156 let remaining = ut;
157 seconds = Number((remaining % 60).toFixed(1));
158 remaining = Math.trunc(remaining / 60);
159 if (remaining > 0) {
160 minutes = remaining % 60;
161 remaining = Math.trunc(remaining / 60);
162 if (remaining > 0) {
163 hours = remaining % 24;
164 remaining = Math.trunc(remaining / 24);
165 if (remaining > 0) {
166 days = remaining % 365;
167 remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years...
168 if (remaining > 0) {
169 years = remaining;
170 }
171 }
172 }
173 }
174
175 let res = [];
176 let add = (t, unit) => {
177 if (t > 0) res.push(t + unit);
178 return t > 0;
179 };
180
181 let addMinutes = !add(years, 'y');
182 let addSeconds = !add(days, 'd');
183 add(hours, 'h');
184 if (addMinutes) {
185 add(minutes, 'm');
186 if (addSeconds) {
187 add(seconds, 's');
188 }
189 }
190 return res.join(' ');
191 },
192
193 format_duration_long: function(ut) {
194 let days = Math.floor(ut / 86400);
195 ut -= days*86400;
196 let hours = Math.floor(ut / 3600);
197 ut -= hours*3600;
198 let mins = Math.floor(ut / 60);
199 ut -= mins*60;
200
201 let hours_str = '00' + hours.toString();
202 hours_str = hours_str.substr(hours_str.length - 2);
203 let mins_str = "00" + mins.toString();
204 mins_str = mins_str.substr(mins_str.length - 2);
205 let ut_str = "00" + ut.toString();
206 ut_str = ut_str.substr(ut_str.length - 2);
207
208 if (days) {
209 let ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText;
210 return days.toString() + ' ' + ds + ' ' +
211 hours_str + ':' + mins_str + ':' + ut_str;
212 } else {
213 return hours_str + ':' + mins_str + ':' + ut_str;
214 }
215 },
216
217 format_subscription_level: function(level) {
218 if (level === 'c') {
219 return 'Community';
220 } else if (level === 'b') {
221 return 'Basic';
222 } else if (level === 's') {
223 return 'Standard';
224 } else if (level === 'p') {
225 return 'Premium';
226 } else {
227 return Proxmox.Utils.noneText;
228 }
229 },
230
231 compute_min_label_width: function(text, width) {
232 if (width === undefined) { width = 100; }
233
234 let tm = new Ext.util.TextMetrics();
235 let min = tm.getWidth(text + ':');
236
237 return min < width ? width : min;
238 },
239
240 // returns username + realm
241 parse_userid: function(userid) {
242 if (!Ext.isString(userid)) {
243 return [undefined, undefined];
244 }
245
246 let match = userid.match(/^(.+)@([^@]+)$/);
247 if (match !== null) {
248 return [match[1], match[2]];
249 }
250
251 return [undefined, undefined];
252 },
253
254 render_username: function(userid) {
255 let username = Proxmox.Utils.parse_userid(userid)[0] || "";
256 return Ext.htmlEncode(username);
257 },
258
259 render_realm: function(userid) {
260 let username = Proxmox.Utils.parse_userid(userid)[1] || "";
261 return Ext.htmlEncode(username);
262 },
263
264 getStoredAuth: function() {
265 let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser'));
266 return storedAuth || {};
267 },
268
269 setAuthData: function(data) {
270 Proxmox.UserName = data.username;
271 Proxmox.LoggedOut = data.LoggedOut;
272 // creates a session cookie (expire = null)
273 // that way the cookie gets deleted after the browser window is closed
274 if (data.ticket) {
275 Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
276 Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
277 }
278
279 if (data.token) {
280 window.localStorage.setItem('ProxmoxUser', JSON.stringify(data));
281 }
282 },
283
284 authOK: function() {
285 if (Proxmox.LoggedOut) {
286 return undefined;
287 }
288 let storedAuth = Proxmox.Utils.getStoredAuth();
289 let cookie = Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name);
290 if ((Proxmox.UserName !== '' && cookie && !cookie.startsWith("PVE:tfa!")) || storedAuth.token) {
291 return cookie || storedAuth.token;
292 } else {
293 return false;
294 }
295 },
296
297 authClear: function() {
298 if (Proxmox.LoggedOut) {
299 return;
300 }
301 // ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
302 Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true);
303 window.localStorage.removeItem("ProxmoxUser");
304 },
305
306 // The End-User gets redirected back here after login on the OpenID auth. portal, and in the
307 // redirection URL the state and auth.code are passed as URL GET params, this helper parses those
308 getOpenIDRedirectionAuthorization: function() {
309 const auth = Ext.Object.fromQueryString(window.location.search);
310 if (auth.state !== undefined && auth.code !== undefined) {
311 return auth;
312 }
313 return undefined;
314 },
315
316 // comp.setLoading() is buggy in ExtJS 4.0.7, so we
317 // use el.mask() instead
318 setErrorMask: function(comp, msg) {
319 let el = comp.el;
320 if (!el) {
321 return;
322 }
323 if (!msg) {
324 el.unmask();
325 } else if (msg === true) {
326 el.mask(gettext("Loading..."));
327 } else {
328 el.mask(msg);
329 }
330 },
331
332 getResponseErrorMessage: (err) => {
333 if (!err.statusText) {
334 return gettext('Connection error');
335 }
336 let msg = [`${err.statusText} (${err.status})`];
337 if (err.response && err.response.responseText) {
338 let txt = err.response.responseText;
339 try {
340 let res = JSON.parse(txt);
341 if (res.errors && typeof res.errors === 'object') {
342 for (let [key, value] of Object.entries(res.errors)) {
343 msg.push(Ext.String.htmlEncode(`${key}: ${value}`));
344 }
345 }
346 } catch (e) {
347 // fallback to string
348 msg.push(Ext.String.htmlEncode(txt));
349 }
350 }
351 return msg.join('<br>');
352 },
353
354 monStoreErrors: function(component, store, clearMaskBeforeLoad, errorCallback) {
355 if (clearMaskBeforeLoad) {
356 component.mon(store, 'beforeload', function(s, operation, eOpts) {
357 Proxmox.Utils.setErrorMask(component, false);
358 });
359 } else {
360 component.mon(store, 'beforeload', function(s, operation, eOpts) {
361 if (!component.loadCount) {
362 component.loadCount = 0; // make sure it is nucomponent.ic
363 Proxmox.Utils.setErrorMask(component, true);
364 }
365 });
366 }
367
368 // only works with 'proxmox' proxy
369 component.mon(store.proxy, 'afterload', function(proxy, request, success) {
370 component.loadCount++;
371
372 if (success) {
373 Proxmox.Utils.setErrorMask(component, false);
374 return;
375 }
376
377 let error = request._operation.getError();
378 let msg = Proxmox.Utils.getResponseErrorMessage(error);
379 if (!errorCallback || !errorCallback(error, msg)) {
380 Proxmox.Utils.setErrorMask(component, msg);
381 }
382 });
383 },
384
385 extractRequestError: function(result, verbose) {
386 let msg = gettext('Successful');
387
388 if (!result.success) {
389 msg = gettext("Unknown error");
390 if (result.message) {
391 msg = Ext.htmlEncode(result.message);
392 if (result.status) {
393 msg += ` (${result.status})`;
394 }
395 }
396 if (verbose && Ext.isObject(result.errors)) {
397 msg += "<br>";
398 Ext.Object.each(result.errors, (prop, desc) => {
399 msg += `<br><b>${Ext.htmlEncode(prop)}</b>: ${Ext.htmlEncode(desc)}`;
400 });
401 }
402 }
403
404 return msg;
405 },
406
407 // Ext.Ajax.request
408 API2Request: function(reqOpts) {
409 let newopts = Ext.apply({
410 waitMsg: gettext('Please wait...'),
411 }, reqOpts);
412
413 // default to enable if user isn't handling the failure already explicitly
414 let autoErrorAlert = reqOpts.autoErrorAlert ??
415 (typeof reqOpts.failure !== 'function' && typeof reqOpts.callback !== 'function');
416
417 if (!newopts.url.match(/^\/api2/)) {
418 newopts.url = '/api2/extjs' + newopts.url;
419 }
420 delete newopts.callback;
421
422 let createWrapper = function(successFn, callbackFn, failureFn) {
423 Ext.apply(newopts, {
424 success: function(response, options) {
425 if (options.waitMsgTarget) {
426 if (Proxmox.Utils.toolkit === 'touch') {
427 options.waitMsgTarget.setMasked(false);
428 } else {
429 options.waitMsgTarget.setLoading(false);
430 }
431 }
432 let result = Ext.decode(response.responseText);
433 response.result = result;
434 if (!result.success) {
435 response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
436 Ext.callback(callbackFn, options.scope, [options, false, response]);
437 Ext.callback(failureFn, options.scope, [response, options]);
438 if (autoErrorAlert) {
439 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
440 }
441 return;
442 }
443 Ext.callback(callbackFn, options.scope, [options, true, response]);
444 Ext.callback(successFn, options.scope, [response, options]);
445 },
446 failure: function(response, options) {
447 if (options.waitMsgTarget) {
448 if (Proxmox.Utils.toolkit === 'touch') {
449 options.waitMsgTarget.setMasked(false);
450 } else {
451 options.waitMsgTarget.setLoading(false);
452 }
453 }
454 response.result = {};
455 try {
456 response.result = Ext.decode(response.responseText);
457 } catch (e) {
458 // ignore
459 }
460 let msg = gettext('Connection error') + ' - server offline?';
461 if (response.aborted) {
462 msg = gettext('Connection error') + ' - aborted.';
463 } else if (response.timedout) {
464 msg = gettext('Connection error') + ' - Timeout.';
465 } else if (response.status && response.statusText) {
466 msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText;
467 }
468 response.htmlStatus = msg;
469 Ext.callback(callbackFn, options.scope, [options, false, response]);
470 Ext.callback(failureFn, options.scope, [response, options]);
471 },
472 });
473 };
474
475 createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure);
476
477 let target = newopts.waitMsgTarget;
478 if (target) {
479 if (Proxmox.Utils.toolkit === 'touch') {
480 target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
481 } else {
482 // Note: ExtJS bug - this does not work when component is not rendered
483 target.setLoading(newopts.waitMsg);
484 }
485 }
486 Ext.Ajax.request(newopts);
487 },
488
489 // can be useful for catching displaying errors from the API, e.g.:
490 // Proxmox.Async.api2({
491 // ...
492 // }).catch(Proxmox.Utils.alertResponseFailure);
493 alertResponseFailure: (response) => {
494 Ext.Msg.alert(
495 gettext('Error'),
496 response.htmlStatus || response.result.message,
497 );
498 },
499
500 checked_command: function(orig_cmd) {
501 Proxmox.Utils.API2Request(
502 {
503 url: '/nodes/localhost/subscription',
504 method: 'GET',
505 failure: function(response, opts) {
506 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
507 },
508 success: function(response, opts) {
509 let res = response.result;
510 if (res === null || res === undefined || !res || res
511 .data.status.toLowerCase() !== 'active') {
512 Ext.Msg.show({
513 title: gettext('No valid subscription'),
514 icon: Ext.Msg.WARNING,
515 message: Proxmox.Utils.getNoSubKeyHtml(res.data.url),
516 buttons: Ext.Msg.OK,
517 callback: function(btn) {
518 if (btn !== 'ok') {
519 return;
520 }
521 orig_cmd();
522 },
523 });
524 } else {
525 orig_cmd();
526 }
527 },
528 },
529 );
530 },
531
532 assemble_field_data: function(values, data) {
533 if (!Ext.isObject(data)) {
534 return;
535 }
536 Ext.Object.each(data, function(name, val) {
537 if (Object.prototype.hasOwnProperty.call(values, name)) {
538 let bucket = values[name];
539 if (!Ext.isArray(bucket)) {
540 bucket = values[name] = [bucket];
541 }
542 if (Ext.isArray(val)) {
543 values[name] = bucket.concat(val);
544 } else {
545 bucket.push(val);
546 }
547 } else {
548 values[name] = val;
549 }
550 });
551 },
552
553 network_iface_types: {
554 eth: gettext("Network Device"),
555 bridge: 'Linux Bridge',
556 bond: 'Linux Bond',
557 vlan: 'Linux VLAN',
558 OVSBridge: 'OVS Bridge',
559 OVSBond: 'OVS Bond',
560 OVSPort: 'OVS Port',
561 OVSIntPort: 'OVS IntPort',
562 },
563
564 render_network_iface_type: function(value) {
565 return Proxmox.Utils.network_iface_types[value] ||
566 Proxmox.Utils.unknownText;
567 },
568
569 // NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
570 task_desc_table: {
571 aptupdate: ['', gettext('Update package database')],
572 diskinit: ['Disk', gettext('Initialize Disk with GPT')],
573 spiceshell: ['', gettext('Shell') + ' (Spice)'],
574 srvreload: ['SRV', gettext('Reload')],
575 srvrestart: ['SRV', gettext('Restart')],
576 srvstart: ['SRV', gettext('Start')],
577 srvstop: ['SRV', gettext('Stop')],
578 termproxy: ['', gettext('Console') + ' (xterm.js)'],
579 vncshell: ['', gettext('Shell')],
580 },
581
582 // to add or change existing for product specific ones
583 override_task_descriptions: function(extra) {
584 for (const [key, value] of Object.entries(extra)) {
585 Proxmox.Utils.task_desc_table[key] = value;
586 }
587 },
588
589 format_task_description: function(type, id) {
590 let farray = Proxmox.Utils.task_desc_table[type];
591 let text;
592 if (!farray) {
593 text = type;
594 if (id) {
595 type += ' ' + id;
596 }
597 return text;
598 } else if (Ext.isFunction(farray)) {
599 return farray(type, id);
600 }
601 let prefix = farray[0];
602 text = farray[1];
603 if (prefix && id !== undefined) {
604 return prefix + ' ' + id + ' - ' + text;
605 }
606 return text;
607 },
608
609 format_size: function(size, useSI) {
610 let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
611 let order = 0;
612 const baseValue = useSI ? 1000 : 1024;
613 while (size >= baseValue && order < units.length) {
614 size = size / baseValue;
615 order++;
616 }
617
618 let unit = units[order], commaDigits = 2;
619 if (order === 0) {
620 commaDigits = 0;
621 } else if (!useSI) {
622 unit += 'i';
623 }
624 return `${size.toFixed(commaDigits)} ${unit}B`;
625 },
626
627 SizeUnits: {
628 'B': 1,
629
630 'KiB': 1024,
631 'MiB': 1024*1024,
632 'GiB': 1024*1024*1024,
633 'TiB': 1024*1024*1024*1024,
634 'PiB': 1024*1024*1024*1024*1024,
635
636 'KB': 1000,
637 'MB': 1000*1000,
638 'GB': 1000*1000*1000,
639 'TB': 1000*1000*1000*1000,
640 'PB': 1000*1000*1000*1000*1000,
641 },
642
643 parse_size_unit: function(val) {
644 //let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i);
645 let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i);
646 let size = parseFloat(m[1]);
647 let scale = m[2].toUpperCase();
648 let binary = m[3].toLowerCase();
649
650 let unit = `${scale}${binary}B`;
651 let factor = Proxmox.Utils.SizeUnits[unit];
652
653 return { size, factor, unit, binary }; // for convenience return all we got
654 },
655
656 size_unit_to_bytes: function(val) {
657 let { size, factor } = Proxmox.Utils.parse_size_unit(val);
658 return size * factor;
659 },
660
661 autoscale_size_unit: function(val) {
662 let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val);
663 return Proxmox.Utils.format_size(size * factor, binary !== "i");
664 },
665
666 size_unit_ratios: function(a, b) {
667 a = typeof a !== "undefined" ? a : 0;
668 b = typeof b !== "undefined" ? b : Infinity;
669 let aBytes = typeof a === "number" ? a : Proxmox.Utils.size_unit_to_bytes(a);
670 let bBytes = typeof b === "number" ? b : Proxmox.Utils.size_unit_to_bytes(b);
671 return aBytes / (bBytes || Infinity); // avoid division by zero
672 },
673
674 render_upid: function(value, metaData, record) {
675 let task = record.data;
676 let type = task.type || task.worker_type;
677 let id = task.id || task.worker_id;
678
679 return Proxmox.Utils.format_task_description(type, id);
680 },
681
682 render_uptime: function(value) {
683 let uptime = value;
684
685 if (uptime === undefined) {
686 return '';
687 }
688
689 if (uptime <= 0) {
690 return '-';
691 }
692
693 return Proxmox.Utils.format_duration_long(uptime);
694 },
695
696 systemd_unescape: function(string_value) {
697 const charcode_0 = '0'.charCodeAt(0);
698 const charcode_9 = '9'.charCodeAt(0);
699 const charcode_A = 'A'.charCodeAt(0);
700 const charcode_F = 'F'.charCodeAt(0);
701 const charcode_a = 'a'.charCodeAt(0);
702 const charcode_f = 'f'.charCodeAt(0);
703 const charcode_x = 'x'.charCodeAt(0);
704 const charcode_minus = '-'.charCodeAt(0);
705 const charcode_slash = '/'.charCodeAt(0);
706 const charcode_backslash = '\\'.charCodeAt(0);
707
708 let parse_hex_digit = function(d) {
709 if (d >= charcode_0 && d <= charcode_9) {
710 return d - charcode_0;
711 }
712 if (d >= charcode_A && d <= charcode_F) {
713 return d - charcode_A + 10;
714 }
715 if (d >= charcode_a && d <= charcode_f) {
716 return d - charcode_a + 10;
717 }
718 throw "got invalid hex digit";
719 };
720
721 let value = new TextEncoder().encode(string_value);
722 let result = new Uint8Array(value.length);
723
724 let i = 0;
725 let result_len = 0;
726
727 while (i < value.length) {
728 let c0 = value[i];
729 if (c0 === charcode_minus) {
730 result.set([charcode_slash], result_len);
731 result_len += 1;
732 i += 1;
733 continue;
734 }
735 if ((i + 4) < value.length) {
736 let c1 = value[i+1];
737 if (c0 === charcode_backslash && c1 === charcode_x) {
738 let h1 = parse_hex_digit(value[i+2]);
739 let h0 = parse_hex_digit(value[i+3]);
740 let ord = h1*16+h0;
741 result.set([ord], result_len);
742 result_len += 1;
743 i += 4;
744 continue;
745 }
746 }
747 result.set([c0], result_len);
748 result_len += 1;
749 i += 1;
750 }
751
752 return new TextDecoder().decode(result.slice(0, result.len));
753 },
754
755 parse_task_upid: function(upid) {
756 let task = {};
757
758 let res = upid.match(/^UPID:([^\s:]+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):(([0-9A-Fa-f]{8,16}):)?([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/);
759 if (!res) {
760 throw "unable to parse upid '" + upid + "'";
761 }
762 task.node = res[1];
763 task.pid = parseInt(res[2], 16);
764 task.pstart = parseInt(res[3], 16);
765 if (res[5] !== undefined) {
766 task.task_id = parseInt(res[5], 16);
767 }
768 task.starttime = parseInt(res[6], 16);
769 task.type = res[7];
770 task.id = Proxmox.Utils.systemd_unescape(res[8]);
771 task.user = res[9];
772
773 task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
774
775 return task;
776 },
777
778 parse_task_status: function(status) {
779 if (status === 'OK') {
780 return 'ok';
781 }
782
783 if (status === 'unknown') {
784 return 'unknown';
785 }
786
787 let match = status.match(/^WARNINGS: (.*)$/);
788 if (match) {
789 return 'warning';
790 }
791
792 return 'error';
793 },
794
795 format_task_status: function(status) {
796 let parsed = Proxmox.Utils.parse_task_status(status);
797 switch (parsed) {
798 case 'unknown': return Proxmox.Utils.unknownText;
799 case 'error': return Proxmox.Utils.errorText + ': ' + status;
800 case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
801 case 'ok': // fall-through
802 default: return status;
803 }
804 },
805
806 render_duration: function(value) {
807 if (value === undefined) {
808 return '-';
809 }
810 return Proxmox.Utils.format_duration_human(value);
811 },
812
813 render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
814 let servertime = new Date(value * 1000);
815 return Ext.Date.format(servertime, 'Y-m-d H:i:s');
816 },
817
818 render_zfs_health: function(value) {
819 if (typeof value === 'undefined') {
820 return "";
821 }
822 var iconCls = 'question-circle';
823 switch (value) {
824 case 'AVAIL':
825 case 'ONLINE':
826 iconCls = 'check-circle good';
827 break;
828 case 'REMOVED':
829 case 'DEGRADED':
830 iconCls = 'exclamation-circle warning';
831 break;
832 case 'UNAVAIL':
833 case 'FAULTED':
834 case 'OFFLINE':
835 iconCls = 'times-circle critical';
836 break;
837 default: //unknown
838 }
839
840 return '<i class="fa fa-' + iconCls + '"></i> ' + value;
841 },
842
843 get_help_info: function(section) {
844 let helpMap;
845 if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
846 helpMap = proxmoxOnlineHelpInfo; // eslint-disable-line no-undef
847 } else if (typeof pveOnlineHelpInfo !== 'undefined') {
848 // be backward compatible with older pve-doc-generators
849 helpMap = pveOnlineHelpInfo; // eslint-disable-line no-undef
850 } else {
851 throw "no global OnlineHelpInfo map declared";
852 }
853
854 if (helpMap[section]) {
855 return helpMap[section];
856 }
857 // try to normalize - and _ separators, to support asciidoc and sphinx
858 // references at the same time.
859 let section_minus_normalized = section.replace(/_/g, '-');
860 if (helpMap[section_minus_normalized]) {
861 return helpMap[section_minus_normalized];
862 }
863 let section_underscore_normalized = section.replace(/-/g, '_');
864 return helpMap[section_underscore_normalized];
865 },
866
867 get_help_link: function(section) {
868 let info = Proxmox.Utils.get_help_info(section);
869 if (!info) {
870 return undefined;
871 }
872 return window.location.origin + info.link;
873 },
874
875 openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
876 let url = Ext.Object.toQueryString({
877 console: vmtype, // kvm, lxc, upgrade or shell
878 xtermjs: 1,
879 vmid: vmid,
880 vmname: vmname,
881 node: nodename,
882 cmd: cmd,
883
884 });
885 let nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
886 if (nw) {
887 nw.focus();
888 }
889 },
890
891 render_optional_url: function(value) {
892 if (value && value.match(/^https?:\/\//) !== null) {
893 return '<a target="_blank" href="' + value + '">' + value + '</a>';
894 }
895 return value;
896 },
897
898 render_san: function(value) {
899 var names = [];
900 if (Ext.isArray(value)) {
901 value.forEach(function(val) {
902 if (!Ext.isNumber(val)) {
903 names.push(val);
904 }
905 });
906 return names.join('<br>');
907 }
908 return value;
909 },
910
911 render_usage: val => (val * 100).toFixed(2) + '%',
912
913 render_cpu_usage: function(val, max) {
914 return Ext.String.format(
915 `${gettext('{0}% of {1}')} ${gettext('CPU(s)')}`,
916 (val*100).toFixed(2),
917 max,
918 );
919 },
920
921 render_size_usage: function(val, max, useSI) {
922 if (max === 0) {
923 return gettext('N/A');
924 }
925 let fmt = v => Proxmox.Utils.format_size(v, useSI);
926 let ratio = (val * 100 / max).toFixed(2);
927 return ratio + '% (' + Ext.String.format(gettext('{0} of {1}'), fmt(val), fmt(max)) + ')';
928 },
929
930 render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
931 if (!(record.data.uptime && Ext.isNumeric(value))) {
932 return '';
933 }
934
935 let maxcpu = record.data.maxcpu || 1;
936 if (!Ext.isNumeric(maxcpu) || maxcpu < 1) {
937 return '';
938 }
939 let cpuText = maxcpu > 1 ? 'CPUs' : 'CPU';
940 let ratio = (value * 100).toFixed(1);
941 return `${ratio}% of ${maxcpu.toString()} ${cpuText}`;
942 },
943
944 render_size: function(value, metaData, record, rowIndex, colIndex, store) {
945 if (!Ext.isNumeric(value)) {
946 return '';
947 }
948 return Proxmox.Utils.format_size(value);
949 },
950
951 render_cpu_model: function(cpu) {
952 let socketText = cpu.sockets > 1 ? gettext('Sockets') : gettext('Socket');
953 return `${cpu.cpus} x ${cpu.model} (${cpu.sockets.toString()} ${socketText})`;
954 },
955
956 /* this is different for nodes */
957 render_node_cpu_usage: function(value, record) {
958 return Proxmox.Utils.render_cpu_usage(value, record.cpus);
959 },
960
961 render_node_size_usage: function(record) {
962 return Proxmox.Utils.render_size_usage(record.used, record.total);
963 },
964
965 loadTextFromFile: function(file, callback, maxBytes) {
966 let maxSize = maxBytes || 8192;
967 if (file.size > maxSize) {
968 Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
969 return;
970 }
971 let reader = new FileReader();
972 reader.onload = evt => callback(evt.target.result);
973 reader.readAsText(file);
974 },
975
976 parsePropertyString: function(value, defaultKey) {
977 var res = {},
978 error;
979
980 if (typeof value !== 'string' || value === '') {
981 return res;
982 }
983
984 Ext.Array.each(value.split(','), function(p) {
985 var kv = p.split('=', 2);
986 if (Ext.isDefined(kv[1])) {
987 res[kv[0]] = kv[1];
988 } else if (Ext.isDefined(defaultKey)) {
989 if (Ext.isDefined(res[defaultKey])) {
990 error = 'defaultKey may be only defined once in propertyString';
991 return false; // break
992 }
993 res[defaultKey] = kv[0];
994 } else {
995 error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
996 return false; // break
997 }
998 return true;
999 });
1000
1001 if (error !== undefined) {
1002 console.error(error);
1003 return undefined;
1004 }
1005
1006 return res;
1007 },
1008
1009 printPropertyString: function(data, defaultKey) {
1010 var stringparts = [],
1011 gotDefaultKeyVal = false,
1012 defaultKeyVal;
1013
1014 Ext.Object.each(data, function(key, value) {
1015 if (defaultKey !== undefined && key === defaultKey) {
1016 gotDefaultKeyVal = true;
1017 defaultKeyVal = value;
1018 } else if (Ext.isArray(value)) {
1019 stringparts.push(key + '=' + value.join(';'));
1020 } else if (value !== '') {
1021 stringparts.push(key + '=' + value);
1022 }
1023 });
1024
1025 stringparts = stringparts.sort();
1026 if (gotDefaultKeyVal) {
1027 stringparts.unshift(defaultKeyVal);
1028 }
1029
1030 return stringparts.join(',');
1031 },
1032
1033 acmedomain_count: 5,
1034
1035 parseACMEPluginData: function(data) {
1036 let res = {};
1037 let extradata = [];
1038 data.split('\n').forEach((line) => {
1039 // capture everything after the first = as value
1040 let [key, value] = line.split('=');
1041 if (value !== undefined) {
1042 res[key] = value;
1043 } else {
1044 extradata.push(line);
1045 }
1046 });
1047 return [res, extradata];
1048 },
1049
1050 delete_if_default: function(values, fieldname, default_val, create) {
1051 if (values[fieldname] === '' || values[fieldname] === default_val) {
1052 if (!create) {
1053 if (values.delete) {
1054 if (Ext.isArray(values.delete)) {
1055 values.delete.push(fieldname);
1056 } else {
1057 values.delete += ',' + fieldname;
1058 }
1059 } else {
1060 values.delete = fieldname;
1061 }
1062 }
1063
1064 delete values[fieldname];
1065 }
1066 },
1067
1068 printACME: function(value) {
1069 if (Ext.isArray(value.domains)) {
1070 value.domains = value.domains.join(';');
1071 }
1072 return Proxmox.Utils.printPropertyString(value);
1073 },
1074
1075 parseACME: function(value) {
1076 if (!value) {
1077 return {};
1078 }
1079
1080 var res = {};
1081 var error;
1082
1083 Ext.Array.each(value.split(','), function(p) {
1084 var kv = p.split('=', 2);
1085 if (Ext.isDefined(kv[1])) {
1086 res[kv[0]] = kv[1];
1087 } else {
1088 error = 'Failed to parse key-value pair: '+p;
1089 return false;
1090 }
1091 return true;
1092 });
1093
1094 if (error !== undefined) {
1095 console.error(error);
1096 return undefined;
1097 }
1098
1099 if (res.domains !== undefined) {
1100 res.domains = res.domains.split(/;/);
1101 }
1102
1103 return res;
1104 },
1105
1106 add_domain_to_acme: function(acme, domain) {
1107 if (acme.domains === undefined) {
1108 acme.domains = [domain];
1109 } else {
1110 acme.domains.push(domain);
1111 acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
1112 }
1113 return acme;
1114 },
1115
1116 remove_domain_from_acme: function(acme, domain) {
1117 if (acme.domains !== undefined) {
1118 acme.domains = acme.domains.filter(
1119 (value, index, self) => self.indexOf(value) === index && value !== domain,
1120 );
1121 }
1122 return acme;
1123 },
1124
1125 get_health_icon: function(state, circle) {
1126 if (circle === undefined) {
1127 circle = false;
1128 }
1129
1130 if (state === undefined) {
1131 state = 'uknown';
1132 }
1133
1134 var icon = 'faded fa-question';
1135 switch (state) {
1136 case 'good':
1137 icon = 'good fa-check';
1138 break;
1139 case 'upgrade':
1140 icon = 'warning fa-upload';
1141 break;
1142 case 'old':
1143 icon = 'warning fa-refresh';
1144 break;
1145 case 'warning':
1146 icon = 'warning fa-exclamation';
1147 break;
1148 case 'critical':
1149 icon = 'critical fa-times';
1150 break;
1151 default: break;
1152 }
1153
1154 if (circle) {
1155 icon += '-circle';
1156 }
1157
1158 return icon;
1159 },
1160
1161 formatNodeRepoStatus: function(status, product) {
1162 let fmt = (txt, cls) => `<i class="fa fa-fw fa-lg fa-${cls}"></i>${txt}`;
1163
1164 let getUpdates = Ext.String.format(gettext('{0} updates'), product);
1165 let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product);
1166
1167 if (status === 'ok') {
1168 return fmt(getUpdates, 'check-circle good') + ' ' +
1169 fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good');
1170 } else if (status === 'no-sub') {
1171 return fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good') + ' ' +
1172 fmt(gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning');
1173 } else if (status === 'non-production') {
1174 return fmt(getUpdates, 'check-circle good') + ' ' +
1175 fmt(gettext('Non production-ready repository enabled!'), 'exclamation-circle warning');
1176 } else if (status === 'no-repo') {
1177 return fmt(noRepo, 'exclamation-circle critical');
1178 }
1179
1180 return Proxmox.Utils.unknownText;
1181 },
1182
1183 render_u2f_error: function(error) {
1184 var ErrorNames = {
1185 '1': gettext('Other Error'),
1186 '2': gettext('Bad Request'),
1187 '3': gettext('Configuration Unsupported'),
1188 '4': gettext('Device Ineligible'),
1189 '5': gettext('Timeout'),
1190 };
1191 return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
1192 },
1193
1194 // Convert an ArrayBuffer to a base64url encoded string.
1195 // A `null` value will be preserved for convenience.
1196 bytes_to_base64url: function(bytes) {
1197 if (bytes === null) {
1198 return null;
1199 }
1200
1201 return btoa(Array
1202 .from(new Uint8Array(bytes))
1203 .map(val => String.fromCharCode(val))
1204 .join(''),
1205 )
1206 .replace(/\+/g, '-')
1207 .replace(/\//g, '_')
1208 .replace(/[=]/g, '');
1209 },
1210
1211 // Convert an a base64url string to an ArrayBuffer.
1212 // A `null` value will be preserved for convenience.
1213 base64url_to_bytes: function(b64u) {
1214 if (b64u === null) {
1215 return null;
1216 }
1217
1218 return new Uint8Array(
1219 atob(b64u
1220 .replace(/-/g, '+')
1221 .replace(/_/g, '/'),
1222 )
1223 .split('')
1224 .map(val => val.charCodeAt(0)),
1225 );
1226 },
1227
1228 stringToRGB: function(string) {
1229 let hash = 0;
1230 if (!string) {
1231 return hash;
1232 }
1233 string += 'prox'; // give short strings more variance
1234 for (let i = 0; i < string.length; i++) {
1235 hash = string.charCodeAt(i) + ((hash << 5) - hash);
1236 hash = hash & hash; // to int
1237 }
1238
1239 let alpha = 0.7; // make the color a bit brighter
1240 let bg = 255; // assume white background
1241
1242 return [
1243 (hash & 255) * alpha + bg * (1 - alpha),
1244 ((hash >> 8) & 255) * alpha + bg * (1 - alpha),
1245 ((hash >> 16) & 255) * alpha + bg * (1 - alpha),
1246 ];
1247 },
1248
1249 rgbToCss: function(rgb) {
1250 return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
1251 },
1252
1253 rgbToHex: function(rgb) {
1254 let r = Math.round(rgb[0]).toString(16);
1255 let g = Math.round(rgb[1]).toString(16);
1256 let b = Math.round(rgb[2]).toString(16);
1257 return `${r}${g}${b}`;
1258 },
1259
1260 hexToRGB: function(hex) {
1261 if (!hex) {
1262 return undefined;
1263 }
1264 if (hex.length === 7) {
1265 hex = hex.slice(1);
1266 }
1267 let r = parseInt(hex.slice(0, 2), 16);
1268 let g = parseInt(hex.slice(2, 4), 16);
1269 let b = parseInt(hex.slice(4, 6), 16);
1270 return [r, g, b];
1271 },
1272
1273 // optimized & simplified SAPC function
1274 // https://github.com/Myndex/SAPC-APCA
1275 getTextContrastClass: function(rgb) {
1276 const blkThrs = 0.022;
1277 const blkClmp = 1.414;
1278
1279 // linearize & gamma correction
1280 let r = (rgb[0] / 255) ** 2.4;
1281 let g = (rgb[1] / 255) ** 2.4;
1282 let b = (rgb[2] / 255) ** 2.4;
1283
1284 // relative luminance sRGB
1285 let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
1286
1287 // black clamp
1288 bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
1289
1290 // SAPC with white text
1291 let contrastLight = bg ** 0.65 - 1;
1292 // SAPC with black text
1293 let contrastDark = bg ** 0.56 - 0.046134502;
1294
1295 if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
1296 return 'light';
1297 } else {
1298 return 'dark';
1299 }
1300 },
1301
1302 getTagElement: function(string, color_overrides) {
1303 let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
1304 let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
1305 let cls;
1306 if (rgb.length > 3) {
1307 style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
1308 cls = "proxmox-tag-dark";
1309 } else {
1310 let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
1311 cls = `proxmox-tag-${txtCls}`;
1312 }
1313 return `<span class="${cls}" style="${style}">${string}</span>`;
1314 },
1315
1316 // Setting filename here when downloading from a remote url sometimes fails in chromium browsers
1317 // because of a bug when using attribute download in conjunction with a self signed certificate.
1318 // For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362
1319 downloadAsFile: function(source, fileName) {
1320 let hiddenElement = document.createElement('a');
1321 hiddenElement.href = source;
1322 hiddenElement.target = '_blank';
1323 if (fileName) {
1324 hiddenElement.download = fileName;
1325 }
1326 hiddenElement.click();
1327 },
1328},
1329
1330 singleton: true,
1331 constructor: function() {
1332 let me = this;
1333 Ext.apply(me, me.utilities);
1334
1335 let IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
1336 let IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
1337 let IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
1338 let IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
1339 let IPV4_CIDR_MASK = "([0-9]{1,2})";
1340 let IPV6_CIDR_MASK = "([0-9]{1,3})";
1341
1342
1343 me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
1344 me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")/" + IPV4_CIDR_MASK + "$");
1345
1346 /* eslint-disable no-useless-concat,no-multi-spaces */
1347 let IPV6_REGEXP = "(?:" +
1348 "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
1349 "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
1350 "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
1351 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
1352 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
1353 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
1354 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" +
1355 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" +
1356 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" +
1357 ")";
1358 /* eslint-enable no-useless-concat,no-multi-spaces */
1359
1360 me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
1361 me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")/" + IPV6_CIDR_MASK + "$");
1362 me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
1363
1364 me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
1365 me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "/" + IPV4_CIDR_MASK + ")$");
1366
1367 let 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])?))";
1368 me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
1369 me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$");
1370
1371 me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/;
1372
1373 me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(?::(\\d+))?$");
1374 me.HostPortBrackets_match = new RegExp("^\\[(" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](?::(\\d+))?$");
1375 me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
1376 me.Vlan_match = /^vlan(\d+)/;
1377 me.VlanInterface_match = /(\w+)\.(\d+)/;
1378 },
1379});
1380
1381Ext.define('Proxmox.Async', {
1382 singleton: true,
1383
1384 // Returns a Promise resolving to the result of an `API2Request` or rejecting to the error
1385 // response on failure
1386 api2: function(reqOpts) {
1387 return new Promise((resolve, reject) => {
1388 delete reqOpts.callback; // not allowed in this api
1389 reqOpts.success = response => resolve(response);
1390 reqOpts.failure = response => reject(response);
1391 Proxmox.Utils.API2Request(reqOpts);
1392 });
1393 },
1394
1395 // Delay for a number of milliseconds.
1396 sleep: function(millis) {
1397 return new Promise((resolve, _reject) => setTimeout(resolve, millis));
1398 },
1399});
1400
1401Ext.override(Ext.data.Store, {
1402 // If the store's proxy is changed while it is waiting for an AJAX
1403 // response, `onProxyLoad` will still be called for the outdated response.
1404 // To avoid displaying inconsistent information, only process responses
1405 // belonging to the current proxy. However, do not apply this workaround
1406 // to the mobile UI, as Sencha Touch has an incompatible internal API.
1407 onProxyLoad: function(operation) {
1408 let me = this;
1409 if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) {
1410 me.callParent(arguments);
1411 } else {
1412 console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`);
1413 }
1414 },
1415});