]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/Utils.js
fix #4551: ui: translate byte unit in `format_size`
[proxmox-widget-toolkit.git] / src / Utils.js
CommitLineData
ecabd437
TL
1Ext.ns('Proxmox');
2Ext.ns('Proxmox.Setup');
3
4if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) {
5 throw "Proxmox library not initialized";
6}
7
ecabd437
TL
8// avoid errors when running without development tools
9if (!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
24Ext.Ajax.defaultHeaders = {
25 'Accept': 'application/json',
26};
27
28Ext.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 }
b6d1735a
TL
35 let storedAuth = Proxmox.Utils.getStoredAuth();
36 if (storedAuth.token) {
37 options.headers.Authorization = storedAuth.token;
63252ab6 38 }
ecabd437
TL
39});
40
41Ext.define('Proxmox.Utils', { // a singleton
42utilities: {
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'),
64d85d96 51 warningsText: gettext('Warnings'),
ecabd437
TL
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
58518b15 65 language_map: { //language map is sorted alphabetically by iso 639-1
c4392921
TL
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)")}`,
ecabd437
TL
90 },
91
92 render_language: function(value) {
e06715d2 93 if (!value || value === '__default__') {
ecabd437
TL
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
64f65c02
LW
103 renderEnabledIcon: enabled => `<i class="fa fa-${enabled ? 'check' : 'minus'}"></i>`,
104
ecabd437
TL
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
15fddc20 114 theme_map: {
a017567b 115 crisp: 'Light theme',
15fddc20
DT
116 "proxmox-dark": 'Proxmox Dark',
117 },
118
119 render_theme: function(value) {
120 if (!value || value === '__default__') {
a017567b 121 return Proxmox.Utils.defaultText + ' (auto)';
15fddc20
DT
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
ecabd437
TL
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) {
3ccdce40 185 let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0;
ecabd437 186
b2d7d422
DC
187 if (ut <= 0.1) {
188 return '<0.1s';
ecabd437
TL
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) {
3ccdce40
TL
201 days = remaining % 365;
202 remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years...
203 if (remaining > 0) {
204 years = remaining;
205 }
ecabd437
TL
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
3ccdce40 216 let addMinutes = !add(years, 'y');
ecabd437
TL
217 let addSeconds = !add(days, 'd');
218 add(hours, 'h');
3ccdce40
TL
219 if (addMinutes) {
220 add(minutes, 'm');
221 if (addSeconds) {
222 add(seconds, 's');
223 }
ecabd437
TL
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
e722f108
TL
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) {
cd20320b 290 let username = Proxmox.Utils.parse_userid(userid)[0] || "";
e722f108
TL
291 return Ext.htmlEncode(username);
292 },
293
294 render_realm: function(userid) {
cd20320b 295 let username = Proxmox.Utils.parse_userid(userid)[1] || "";
e722f108
TL
296 return Ext.htmlEncode(username);
297 },
298
63252ab6
TM
299 getStoredAuth: function() {
300 let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser'));
301 return storedAuth || {};
302 },
303
ecabd437 304 setAuthData: function(data) {
ecabd437
TL
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
63252ab6
TM
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 }
ecabd437
TL
317 },
318
319 authOK: function() {
320 if (Proxmox.LoggedOut) {
321 return undefined;
322 }
b6d1735a 323 let storedAuth = Proxmox.Utils.getStoredAuth();
ecabd437 324 let cookie = Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name);
63252ab6
TM
325 if ((Proxmox.UserName !== '' && cookie && !cookie.startsWith("PVE:tfa!")) || storedAuth.token) {
326 return cookie || storedAuth.token;
ecabd437
TL
327 } else {
328 return false;
329 }
330 },
331
332 authClear: function() {
333 if (Proxmox.LoggedOut) {
334 return;
335 }
96fb7eaa 336 // ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
c1a35841 337 Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true);
63252ab6 338 window.localStorage.removeItem("ProxmoxUser");
ecabd437
TL
339 },
340
e8c60b32
TL
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
ecabd437
TL
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
661faeed 389 monStoreErrors: function(component, store, clearMaskBeforeLoad, errorCallback) {
ecabd437
TL
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);
661faeed
SR
414 if (!errorCallback || !errorCallback(error, msg)) {
415 Proxmox.Utils.setErrorMask(component, msg);
416 }
ecabd437
TL
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) {
cf93d1da 426 msg = Ext.htmlEncode(result.message);
ecabd437 427 if (result.status) {
d53046d6 428 msg += ` (${result.status})`;
ecabd437
TL
429 }
430 }
431 if (verbose && Ext.isObject(result.errors)) {
432 msg += "<br>";
d53046d6
TL
433 Ext.Object.each(result.errors, (prop, desc) => {
434 msg += `<br><b>${Ext.htmlEncode(prop)}</b>: ${Ext.htmlEncode(desc)}`;
ecabd437
TL
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
319d450b
TL
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
ecabd437
TL
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]);
319d450b
TL
473 if (autoErrorAlert) {
474 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
475 }
ecabd437
TL
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
106fe29e
TL
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
ecabd437 535 checked_command: function(orig_cmd) {
a718654e
TL
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
57ba4cdc 546 .data.status.toLowerCase() !== 'active') {
a718654e
TL
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 },
ecabd437 563 },
a718654e 564 );
ecabd437
TL
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
fb4bb95e 588 updateColumnWidth: function(container, thresholdWidth) {
ecabd437
TL
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 {
fb4bb95e
TL
597 thresholdWidth = (thresholdWidth || 1400) + 1;
598 factor = Math.ceil(container.getSize().width / thresholdWidth);
ecabd437
TL
599 }
600
601 if (container.oldFactor === factor) {
602 return;
603 }
604
017a6376 605 let items = container.query('>'); // direct children
ecabd437
TL
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
8b7e349a
TL
619 // NOTE: depreacated, use updateColumnWidth
620 updateColumns: container => Proxmox.Utils.updateColumnWidth(container),
621
ecabd437
TL
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
fc0a18e6 650 // NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
ecabd437 651 task_desc_table: {
3571d713 652 aptupdate: ['', gettext('Update package database')],
ecabd437 653 diskinit: ['Disk', gettext('Initialize Disk with GPT')],
ecabd437 654 spiceshell: ['', gettext('Shell') + ' (Spice)'],
3571d713
DC
655 srvreload: ['SRV', gettext('Reload')],
656 srvrestart: ['SRV', gettext('Restart')],
ecabd437
TL
657 srvstart: ['SRV', gettext('Start')],
658 srvstop: ['SRV', gettext('Stop')],
3571d713
DC
659 termproxy: ['', gettext('Console') + ' (xterm.js)'],
660 vncshell: ['', gettext('Shell')],
ecabd437
TL
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
b46ad78c 690 format_size: function(size, useSI) {
01e64778
NU
691 let unitsSI = [gettext('B'), gettext('KB'), gettext('MB'), gettext('GB'),
692 gettext('TB'), gettext('PB'), gettext('EB'), gettext('ZB'), gettext('YB')];
693 let unitsIEC = [gettext('B'), gettext('KiB'), gettext('MiB'), gettext('GiB'),
694 gettext('TiB'), gettext('PiB'), gettext('EiB'), gettext('ZiB'), gettext('YiB')];
b46ad78c 695 let order = 0;
01e64778 696 let commaDigits = 2;
b46ad78c 697 const baseValue = useSI ? 1000 : 1024;
01e64778 698 while (size >= baseValue && order < unitsSI.length) {
b46ad78c
TL
699 size = size / baseValue;
700 order++;
ecabd437
TL
701 }
702
01e64778 703 let unit = useSI ? unitsSI[order] : unitsIEC[order];
b46ad78c
TL
704 if (order === 0) {
705 commaDigits = 0;
b46ad78c 706 }
01e64778 707 return `${size.toFixed(commaDigits)} ${unit}`;
ecabd437
TL
708 },
709
1fad0e88
TL
710 SizeUnits: {
711 'B': 1,
712
713 'KiB': 1024,
714 'MiB': 1024*1024,
715 'GiB': 1024*1024*1024,
716 'TiB': 1024*1024*1024*1024,
717 'PiB': 1024*1024*1024*1024*1024,
718
719 'KB': 1000,
720 'MB': 1000*1000,
721 'GB': 1000*1000*1000,
722 'TB': 1000*1000*1000*1000,
723 'PB': 1000*1000*1000*1000*1000,
724 },
725
851ebc36
TL
726 parse_size_unit: function(val) {
727 //let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i);
728 let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i);
729 let size = parseFloat(m[1]);
730 let scale = m[2].toUpperCase();
731 let binary = m[3].toLowerCase();
732
733 let unit = `${scale}${binary}B`;
734 let factor = Proxmox.Utils.SizeUnits[unit];
735
736 return { size, factor, unit, binary }; // for convenience return all we got
737 },
738
739 size_unit_to_bytes: function(val) {
740 let { size, factor } = Proxmox.Utils.parse_size_unit(val);
741 return size * factor;
742 },
743
744 autoscale_size_unit: function(val) {
745 let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val);
746 return Proxmox.Utils.format_size(size * factor, binary !== "i");
747 },
748
749 size_unit_ratios: function(a, b) {
750 a = typeof a !== "undefined" ? a : 0;
751 b = typeof b !== "undefined" ? b : Infinity;
752 let aBytes = typeof a === "number" ? a : Proxmox.Utils.size_unit_to_bytes(a);
753 let bBytes = typeof b === "number" ? b : Proxmox.Utils.size_unit_to_bytes(b);
754 return aBytes / (bBytes || Infinity); // avoid division by zero
755 },
756
ecabd437
TL
757 render_upid: function(value, metaData, record) {
758 let task = record.data;
759 let type = task.type || task.worker_type;
760 let id = task.id || task.worker_id;
761
762 return Proxmox.Utils.format_task_description(type, id);
763 },
764
765 render_uptime: function(value) {
766 let uptime = value;
767
768 if (uptime === undefined) {
769 return '';
770 }
771
772 if (uptime <= 0) {
773 return '-';
774 }
775
776 return Proxmox.Utils.format_duration_long(uptime);
777 },
778
6221e40c 779 systemd_unescape: function(string_value) {
6221e40c
DM
780 const charcode_0 = '0'.charCodeAt(0);
781 const charcode_9 = '9'.charCodeAt(0);
782 const charcode_A = 'A'.charCodeAt(0);
783 const charcode_F = 'F'.charCodeAt(0);
784 const charcode_a = 'a'.charCodeAt(0);
785 const charcode_f = 'f'.charCodeAt(0);
786 const charcode_x = 'x'.charCodeAt(0);
787 const charcode_minus = '-'.charCodeAt(0);
788 const charcode_slash = '/'.charCodeAt(0);
789 const charcode_backslash = '\\'.charCodeAt(0);
790
791 let parse_hex_digit = function(d) {
792 if (d >= charcode_0 && d <= charcode_9) {
793 return d - charcode_0;
794 }
795 if (d >= charcode_A && d <= charcode_F) {
796 return d - charcode_A + 10;
797 }
798 if (d >= charcode_a && d <= charcode_f) {
799 return d - charcode_a + 10;
800 }
801 throw "got invalid hex digit";
802 };
803
804 let value = new TextEncoder().encode(string_value);
805 let result = new Uint8Array(value.length);
806
807 let i = 0;
808 let result_len = 0;
809
810 while (i < value.length) {
811 let c0 = value[i];
812 if (c0 === charcode_minus) {
813 result.set([charcode_slash], result_len);
814 result_len += 1;
815 i += 1;
816 continue;
817 }
818 if ((i + 4) < value.length) {
819 let c1 = value[i+1];
820 if (c0 === charcode_backslash && c1 === charcode_x) {
821 let h1 = parse_hex_digit(value[i+2]);
822 let h0 = parse_hex_digit(value[i+3]);
823 let ord = h1*16+h0;
824 result.set([ord], result_len);
825 result_len += 1;
826 i += 4;
827 continue;
828 }
829 }
830 result.set([c0], result_len);
831 result_len += 1;
832 i += 1;
833 }
834
835 return new TextDecoder().decode(result.slice(0, result.len));
836 },
837
ecabd437
TL
838 parse_task_upid: function(upid) {
839 let task = {};
840
841 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]+):$/);
842 if (!res) {
843 throw "unable to parse upid '" + upid + "'";
844 }
845 task.node = res[1];
846 task.pid = parseInt(res[2], 16);
847 task.pstart = parseInt(res[3], 16);
848 if (res[5] !== undefined) {
849 task.task_id = parseInt(res[5], 16);
850 }
851 task.starttime = parseInt(res[6], 16);
852 task.type = res[7];
6221e40c 853 task.id = Proxmox.Utils.systemd_unescape(res[8]);
ecabd437
TL
854 task.user = res[9];
855
856 task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
857
858 return task;
859 },
860
b1d446d0
DC
861 parse_task_status: function(status) {
862 if (status === 'OK') {
863 return 'ok';
864 }
865
866 if (status === 'unknown') {
867 return 'unknown';
868 }
869
870 let match = status.match(/^WARNINGS: (.*)$/);
871 if (match) {
872 return 'warning';
873 }
874
875 return 'error';
876 },
877
9865b73e
FE
878 format_task_status: function(status) {
879 let parsed = Proxmox.Utils.parse_task_status(status);
4294143f
FE
880 switch (parsed) {
881 case 'unknown': return Proxmox.Utils.unknownText;
9865b73e 882 case 'error': return Proxmox.Utils.errorText + ': ' + status;
9b4b243a 883 case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
4294143f 884 case 'ok': // fall-through
9865b73e 885 default: return status;
4294143f
FE
886 }
887 },
888
ecabd437
TL
889 render_duration: function(value) {
890 if (value === undefined) {
891 return '-';
892 }
893 return Proxmox.Utils.format_duration_human(value);
894 },
895
896 render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
897 let servertime = new Date(value * 1000);
898 return Ext.Date.format(servertime, 'Y-m-d H:i:s');
899 },
900
f29e6a66
DC
901 render_zfs_health: function(value) {
902 if (typeof value === 'undefined') {
903 return "";
904 }
905 var iconCls = 'question-circle';
906 switch (value) {
907 case 'AVAIL':
908 case 'ONLINE':
909 iconCls = 'check-circle good';
910 break;
911 case 'REMOVED':
912 case 'DEGRADED':
913 iconCls = 'exclamation-circle warning';
914 break;
915 case 'UNAVAIL':
916 case 'FAULTED':
917 case 'OFFLINE':
918 iconCls = 'times-circle critical';
919 break;
920 default: //unknown
921 }
922
923 return '<i class="fa fa-' + iconCls + '"></i> ' + value;
924 },
925
ecabd437
TL
926 get_help_info: function(section) {
927 let helpMap;
928 if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
929 helpMap = proxmoxOnlineHelpInfo; // eslint-disable-line no-undef
930 } else if (typeof pveOnlineHelpInfo !== 'undefined') {
931 // be backward compatible with older pve-doc-generators
932 helpMap = pveOnlineHelpInfo; // eslint-disable-line no-undef
933 } else {
934 throw "no global OnlineHelpInfo map declared";
935 }
936
7f56fd0c
TL
937 if (helpMap[section]) {
938 return helpMap[section];
939 }
940 // try to normalize - and _ separators, to support asciidoc and sphinx
941 // references at the same time.
63ec56e5 942 let section_minus_normalized = section.replace(/_/g, '-');
7f56fd0c
TL
943 if (helpMap[section_minus_normalized]) {
944 return helpMap[section_minus_normalized];
945 }
63ec56e5 946 let section_underscore_normalized = section.replace(/-/g, '_');
7f56fd0c 947 return helpMap[section_underscore_normalized];
ecabd437
TL
948 },
949
950 get_help_link: function(section) {
951 let info = Proxmox.Utils.get_help_info(section);
952 if (!info) {
953 return undefined;
954 }
955 return window.location.origin + info.link;
956 },
957
958 openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
959 let url = Ext.Object.toQueryString({
960 console: vmtype, // kvm, lxc, upgrade or shell
961 xtermjs: 1,
962 vmid: vmid,
963 vmname: vmname,
964 node: nodename,
965 cmd: cmd,
966
967 });
968 let nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
969 if (nw) {
970 nw.focus();
971 }
972 },
973
25680ef5
WB
974 render_optional_url: function(value) {
975 if (value && value.match(/^https?:\/\//) !== null) {
976 return '<a target="_blank" href="' + value + '">' + value + '</a>';
977 }
978 return value;
979 },
980
981 render_san: function(value) {
982 var names = [];
983 if (Ext.isArray(value)) {
984 value.forEach(function(val) {
985 if (!Ext.isNumber(val)) {
986 names.push(val);
987 }
988 });
989 return names.join('<br>');
990 }
991 return value;
992 },
993
ce8289fc 994 render_usage: val => (val * 100).toFixed(2) + '%',
ef3f1cfc
DC
995
996 render_cpu_usage: function(val, max) {
9e663a6a
TL
997 return Ext.String.format(
998 `${gettext('{0}% of {1}')} ${gettext('CPU(s)')}`,
999 (val*100).toFixed(2),
1000 max,
1001 );
ef3f1cfc
DC
1002 },
1003
b46ad78c 1004 render_size_usage: function(val, max, useSI) {
ef3f1cfc
DC
1005 if (max === 0) {
1006 return gettext('N/A');
1007 }
b46ad78c 1008 let fmt = v => Proxmox.Utils.format_size(v, useSI);
ce8289fc
TL
1009 let ratio = (val * 100 / max).toFixed(2);
1010 return ratio + '% (' + Ext.String.format(gettext('{0} of {1}'), fmt(val), fmt(max)) + ')';
ef3f1cfc
DC
1011 },
1012
1013 render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
1014 if (!(record.data.uptime && Ext.isNumeric(value))) {
1015 return '';
1016 }
1017
9e663a6a 1018 let maxcpu = record.data.maxcpu || 1;
1334cdca 1019 if (!Ext.isNumeric(maxcpu) || maxcpu < 1) {
ef3f1cfc
DC
1020 return '';
1021 }
9e663a6a
TL
1022 let cpuText = maxcpu > 1 ? 'CPUs' : 'CPU';
1023 let ratio = (value * 100).toFixed(1);
1024 return `${ratio}% of ${maxcpu.toString()} ${cpuText}`;
ef3f1cfc
DC
1025 },
1026
1027 render_size: function(value, metaData, record, rowIndex, colIndex, store) {
1028 if (!Ext.isNumeric(value)) {
1029 return '';
1030 }
ef3f1cfc
DC
1031 return Proxmox.Utils.format_size(value);
1032 },
1033
9e663a6a
TL
1034 render_cpu_model: function(cpu) {
1035 let socketText = cpu.sockets > 1 ? gettext('Sockets') : gettext('Socket');
1036 return `${cpu.cpus} x ${cpu.model} (${cpu.sockets.toString()} ${socketText})`;
ef3f1cfc
DC
1037 },
1038
1039 /* this is different for nodes */
1040 render_node_cpu_usage: function(value, record) {
1041 return Proxmox.Utils.render_cpu_usage(value, record.cpus);
1042 },
1043
1044 render_node_size_usage: function(record) {
1045 return Proxmox.Utils.render_size_usage(record.used, record.total);
1046 },
1047
25680ef5
WB
1048 loadTextFromFile: function(file, callback, maxBytes) {
1049 let maxSize = maxBytes || 8192;
1050 if (file.size > maxSize) {
1051 Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
1052 return;
1053 }
1054 let reader = new FileReader();
1055 reader.onload = evt => callback(evt.target.result);
1056 reader.readAsText(file);
1057 },
1058
1059 parsePropertyString: function(value, defaultKey) {
1060 var res = {},
1061 error;
1062
1063 if (typeof value !== 'string' || value === '') {
1064 return res;
1065 }
1066
1067 Ext.Array.each(value.split(','), function(p) {
1068 var kv = p.split('=', 2);
1069 if (Ext.isDefined(kv[1])) {
1070 res[kv[0]] = kv[1];
1071 } else if (Ext.isDefined(defaultKey)) {
1072 if (Ext.isDefined(res[defaultKey])) {
1073 error = 'defaultKey may be only defined once in propertyString';
1074 return false; // break
1075 }
1076 res[defaultKey] = kv[0];
1077 } else {
1078 error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
1079 return false; // break
1080 }
1081 return true;
1082 });
1083
1084 if (error !== undefined) {
1085 console.error(error);
1086 return undefined;
1087 }
1088
1089 return res;
1090 },
1091
1092 printPropertyString: function(data, defaultKey) {
1093 var stringparts = [],
1094 gotDefaultKeyVal = false,
1095 defaultKeyVal;
1096
1097 Ext.Object.each(data, function(key, value) {
1098 if (defaultKey !== undefined && key === defaultKey) {
1099 gotDefaultKeyVal = true;
1100 defaultKeyVal = value;
1101 } else if (Ext.isArray(value)) {
1102 stringparts.push(key + '=' + value.join(';'));
1103 } else if (value !== '') {
1104 stringparts.push(key + '=' + value);
1105 }
1106 });
1107
1108 stringparts = stringparts.sort();
1109 if (gotDefaultKeyVal) {
1110 stringparts.unshift(defaultKeyVal);
1111 }
1112
1113 return stringparts.join(',');
1114 },
1115
1116 acmedomain_count: 5,
1117
1118 parseACMEPluginData: function(data) {
1119 let res = {};
1120 let extradata = [];
1121 data.split('\n').forEach((line) => {
1122 // capture everything after the first = as value
1123 let [key, value] = line.split('=');
1124 if (value !== undefined) {
1125 res[key] = value;
1126 } else {
1127 extradata.push(line);
1128 }
1129 });
1130 return [res, extradata];
1131 },
1132
1133 delete_if_default: function(values, fieldname, default_val, create) {
1134 if (values[fieldname] === '' || values[fieldname] === default_val) {
1135 if (!create) {
1136 if (values.delete) {
1137 if (Ext.isArray(values.delete)) {
1138 values.delete.push(fieldname);
1139 } else {
1140 values.delete += ',' + fieldname;
1141 }
1142 } else {
1143 values.delete = fieldname;
1144 }
1145 }
1146
1147 delete values[fieldname];
1148 }
1149 },
1150
1151 printACME: function(value) {
1152 if (Ext.isArray(value.domains)) {
1153 value.domains = value.domains.join(';');
1154 }
1155 return Proxmox.Utils.printPropertyString(value);
1156 },
1157
1158 parseACME: function(value) {
1159 if (!value) {
1160 return {};
1161 }
1162
1163 var res = {};
1164 var error;
1165
1166 Ext.Array.each(value.split(','), function(p) {
1167 var kv = p.split('=', 2);
1168 if (Ext.isDefined(kv[1])) {
1169 res[kv[0]] = kv[1];
1170 } else {
1171 error = 'Failed to parse key-value pair: '+p;
1172 return false;
1173 }
1174 return true;
1175 });
1176
1177 if (error !== undefined) {
1178 console.error(error);
1179 return undefined;
1180 }
1181
1182 if (res.domains !== undefined) {
1183 res.domains = res.domains.split(/;/);
1184 }
1185
1186 return res;
1187 },
1188
1189 add_domain_to_acme: function(acme, domain) {
1190 if (acme.domains === undefined) {
1191 acme.domains = [domain];
1192 } else {
1193 acme.domains.push(domain);
1194 acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
1195 }
1196 return acme;
1197 },
1198
1199 remove_domain_from_acme: function(acme, domain) {
1200 if (acme.domains !== undefined) {
1201 acme.domains = acme.domains.filter(
1202 (value, index, self) => self.indexOf(value) === index && value !== domain,
1203 );
1204 }
1205 return acme;
1206 },
d365540e 1207
fbbe68fd
DC
1208 get_health_icon: function(state, circle) {
1209 if (circle === undefined) {
1210 circle = false;
1211 }
1212
1213 if (state === undefined) {
1214 state = 'uknown';
1215 }
1216
1217 var icon = 'faded fa-question';
1218 switch (state) {
1219 case 'good':
1220 icon = 'good fa-check';
1221 break;
1222 case 'upgrade':
1223 icon = 'warning fa-upload';
1224 break;
1225 case 'old':
1226 icon = 'warning fa-refresh';
1227 break;
1228 case 'warning':
1229 icon = 'warning fa-exclamation';
1230 break;
1231 case 'critical':
1232 icon = 'critical fa-times';
1233 break;
1234 default: break;
1235 }
1236
1237 if (circle) {
1238 icon += '-circle';
1239 }
1240
1241 return icon;
1242 },
40296471 1243
7c65b8bf
FE
1244 formatNodeRepoStatus: function(status, product) {
1245 let fmt = (txt, cls) => `<i class="fa fa-fw fa-lg fa-${cls}"></i>${txt}`;
1246
1247 let getUpdates = Ext.String.format(gettext('{0} updates'), product);
1248 let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product);
1249
1250 if (status === 'ok') {
1251 return fmt(getUpdates, 'check-circle good') + ' ' +
1252 fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good');
1253 } else if (status === 'no-sub') {
1254 return fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good') + ' ' +
1255 fmt(gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning');
1256 } else if (status === 'non-production') {
1257 return fmt(getUpdates, 'check-circle good') + ' ' +
1258 fmt(gettext('Non production-ready repository enabled!'), 'exclamation-circle warning');
1259 } else if (status === 'no-repo') {
1260 return fmt(noRepo, 'exclamation-circle critical');
1261 }
1262
1263 return Proxmox.Utils.unknownText;
1264 },
c7f2b15a
WB
1265
1266 render_u2f_error: function(error) {
1267 var ErrorNames = {
1268 '1': gettext('Other Error'),
1269 '2': gettext('Bad Request'),
1270 '3': gettext('Configuration Unsupported'),
1271 '4': gettext('Device Ineligible'),
1272 '5': gettext('Timeout'),
1273 };
1274 return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
1275 },
1276
1277 // Convert an ArrayBuffer to a base64url encoded string.
1278 // A `null` value will be preserved for convenience.
1279 bytes_to_base64url: function(bytes) {
1280 if (bytes === null) {
1281 return null;
1282 }
1283
1284 return btoa(Array
1285 .from(new Uint8Array(bytes))
1286 .map(val => String.fromCharCode(val))
1287 .join(''),
1288 )
1289 .replace(/\+/g, '-')
1290 .replace(/\//g, '_')
1291 .replace(/[=]/g, '');
1292 },
1293
1294 // Convert an a base64url string to an ArrayBuffer.
1295 // A `null` value will be preserved for convenience.
1296 base64url_to_bytes: function(b64u) {
1297 if (b64u === null) {
1298 return null;
1299 }
1300
1301 return new Uint8Array(
1302 atob(b64u
1303 .replace(/-/g, '+')
1304 .replace(/_/g, '/'),
1305 )
1306 .split('')
1307 .map(val => val.charCodeAt(0)),
1308 );
1309 },
23968741
DC
1310
1311 stringToRGB: function(string) {
1312 let hash = 0;
1313 if (!string) {
1314 return hash;
1315 }
1316 string += 'prox'; // give short strings more variance
1317 for (let i = 0; i < string.length; i++) {
1318 hash = string.charCodeAt(i) + ((hash << 5) - hash);
1319 hash = hash & hash; // to int
1320 }
1321
1322 let alpha = 0.7; // make the color a bit brighter
1323 let bg = 255; // assume white background
1324
1325 return [
1326 (hash & 255) * alpha + bg * (1 - alpha),
1327 ((hash >> 8) & 255) * alpha + bg * (1 - alpha),
1328 ((hash >> 16) & 255) * alpha + bg * (1 - alpha),
1329 ];
1330 },
1331
1332 rgbToCss: function(rgb) {
1333 return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
1334 },
1335
1336 rgbToHex: function(rgb) {
1337 let r = Math.round(rgb[0]).toString(16);
1338 let g = Math.round(rgb[1]).toString(16);
1339 let b = Math.round(rgb[2]).toString(16);
1340 return `${r}${g}${b}`;
1341 },
1342
1343 hexToRGB: function(hex) {
1344 if (!hex) {
1345 return undefined;
1346 }
1347 if (hex.length === 7) {
1348 hex = hex.slice(1);
1349 }
1350 let r = parseInt(hex.slice(0, 2), 16);
1351 let g = parseInt(hex.slice(2, 4), 16);
1352 let b = parseInt(hex.slice(4, 6), 16);
1353 return [r, g, b];
1354 },
1355
1356 // optimized & simplified SAPC function
1357 // https://github.com/Myndex/SAPC-APCA
1358 getTextContrastClass: function(rgb) {
1359 const blkThrs = 0.022;
1360 const blkClmp = 1.414;
1361
1362 // linearize & gamma correction
1363 let r = (rgb[0] / 255) ** 2.4;
1364 let g = (rgb[1] / 255) ** 2.4;
1365 let b = (rgb[2] / 255) ** 2.4;
1366
1367 // relative luminance sRGB
1368 let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
1369
1370 // black clamp
1371 bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
1372
1373 // SAPC with white text
1374 let contrastLight = bg ** 0.65 - 1;
1375 // SAPC with black text
1376 let contrastDark = bg ** 0.56 - 0.046134502;
1377
1378 if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
1379 return 'light';
1380 } else {
1381 return 'dark';
1382 }
1383 },
1384
1385 getTagElement: function(string, color_overrides) {
1386 let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
1387 let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
1388 let cls;
1389 if (rgb.length > 3) {
1390 style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
1391 cls = "proxmox-tag-dark";
1392 } else {
1393 let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
1394 cls = `proxmox-tag-${txtCls}`;
1395 }
1396 return `<span class="${cls}" style="${style}">${string}</span>`;
1397 },
8189ce63
DT
1398
1399 // Setting filename here when downloading from a remote url sometimes fails in chromium browsers
1400 // because of a bug when using attribute download in conjunction with a self signed certificate.
1401 // For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362
1402 downloadAsFile: function(source, fileName) {
1403 let hiddenElement = document.createElement('a');
1404 hiddenElement.href = source;
1405 hiddenElement.target = '_blank';
1406 if (fileName) {
1407 hiddenElement.download = fileName;
1408 }
1409 hiddenElement.click();
1410 },
ecabd437
TL
1411},
1412
1413 singleton: true,
1414 constructor: function() {
1415 let me = this;
1416 Ext.apply(me, me.utilities);
1417
1418 let IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
1419 let IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
1420 let IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
1421 let IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
1422 let IPV4_CIDR_MASK = "([0-9]{1,2})";
1423 let IPV6_CIDR_MASK = "([0-9]{1,3})";
1424
1425
1426 me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
1427 me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")/" + IPV4_CIDR_MASK + "$");
1428
1429 /* eslint-disable no-useless-concat,no-multi-spaces */
1430 let IPV6_REGEXP = "(?:" +
1431 "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
1432 "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
1433 "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
1434 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
1435 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
1436 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
1437 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" +
1438 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" +
1439 "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" +
1440 ")";
1441 /* eslint-enable no-useless-concat,no-multi-spaces */
1442
1443 me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
1444 me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")/" + IPV6_CIDR_MASK + "$");
1445 me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
1446
1447 me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
1448 me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "/" + IPV4_CIDR_MASK + ")$");
1449
a1d40d37 1450 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])?))";
ecabd437 1451 me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
66c5ceb8 1452 me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$");
ecabd437 1453
54fc2533 1454 me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/;
9109c392 1455
a1d40d37
DC
1456 me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(?::(\\d+))?$");
1457 me.HostPortBrackets_match = new RegExp("^\\[(" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](?::(\\d+))?$");
1458 me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
7bff067a
DJ
1459 me.Vlan_match = /^vlan(\d+)/;
1460 me.VlanInterface_match = /(\w+)\.(\d+)/;
ecabd437
TL
1461 },
1462});
c2c12542
TL
1463
1464Ext.define('Proxmox.Async', {
1465 singleton: true,
1466
106fe29e 1467 // Returns a Promise resolving to the result of an `API2Request` or rejecting to the error
017a6376 1468 // response on failure
c2c12542
TL
1469 api2: function(reqOpts) {
1470 return new Promise((resolve, reject) => {
1471 delete reqOpts.callback; // not allowed in this api
1472 reqOpts.success = response => resolve(response);
106fe29e 1473 reqOpts.failure = response => reject(response);
c2c12542
TL
1474 Proxmox.Utils.API2Request(reqOpts);
1475 });
1476 },
1477
1478 // Delay for a number of milliseconds.
1479 sleep: function(millis) {
1480 return new Promise((resolve, _reject) => setTimeout(resolve, millis));
1481 },
1482});
51083ee5
FW
1483
1484Ext.override(Ext.data.Store, {
1485 // If the store's proxy is changed while it is waiting for an AJAX
1486 // response, `onProxyLoad` will still be called for the outdated response.
1487 // To avoid displaying inconsistent information, only process responses
4fedb4e2
FW
1488 // belonging to the current proxy. However, do not apply this workaround
1489 // to the mobile UI, as Sencha Touch has an incompatible internal API.
51083ee5
FW
1490 onProxyLoad: function(operation) {
1491 let me = this;
4fedb4e2 1492 if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) {
51083ee5
FW
1493 me.callParent(arguments);
1494 } else {
1495 console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`);
1496 }
1497 },
1498});