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