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