X-Git-Url: https://git.proxmox.com/?p=proxmox-widget-toolkit.git;a=blobdiff_plain;f=src%2FUtils.js;h=ef72630951b64569ca7195faba8e2079e4ff7012;hp=c52bef2abc9ed5e6229f90c8aec158173fd5766a;hb=4fedb4e28d2da5cfbf244e72da4e4149a6a60554;hpb=66c5ceb8482c2510d30184526f1aa7ede89ed3e6 diff --git a/src/Utils.js b/src/Utils.js index c52bef2..ef72630 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -62,35 +62,35 @@ utilities: { stateText: gettext('State'), groupText: gettext('Group'), - language_map: { - ar: 'Arabic', - ca: 'Catalan', - zh_CN: 'Chinese (Simplified)', - zh_TW: 'Chinese (Traditional)', - da: 'Danish', - nl: 'Dutch', - en: 'English', - eu: 'Euskera (Basque)', - fr: 'French', - de: 'German', - he: 'Hebrew', - it: 'Italian', - ja: 'Japanese', - kr: 'Korean', - nb: 'Norwegian (Bokmal)', - nn: 'Norwegian (Nynorsk)', - fa: 'Persian (Farsi)', - pl: 'Polish', - pt_BR: 'Portuguese (Brazil)', - ru: 'Russian', - sl: 'Slovenian', - es: 'Spanish', - sv: 'Swedish', - tr: 'Turkish', + language_map: { //language map is sorted alphabetically by iso 639-1 + ar: `العربية - ${gettext("Arabic")}`, + ca: `Català - ${gettext("Catalan")}`, + da: `Dansk - ${gettext("Danish")}`, + de: `Deutsch - ${gettext("German")}`, + en: `English - ${gettext("English")}`, + es: `Español - ${gettext("Spanish")}`, + eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`, + fa: `فارسی - ${gettext("Persian (Farsi)")}`, + fr: `Français - ${gettext("French")}`, + he: `עברית - ${gettext("Hebrew")}`, + it: `Italiano - ${gettext("Italian")}`, + ja: `日本語 - ${gettext("Japanese")}`, + kr: `한국어 - ${gettext("Korean")}`, + nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`, + nl: `Nederlands - ${gettext("Dutch")}`, + nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`, + pl: `Polski - ${gettext("Polish")}`, + pt_BR: `Português Brasileiro - ${gettext("Portuguese (Brazil)")}`, + ru: `Русский - ${gettext("Russian")}`, + sl: `Slovenščina - ${gettext("Slovenian")}`, + sv: `Svenska - ${gettext("Swedish")}`, + tr: `Türkçe - ${gettext("Turkish")}`, + zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`, + zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`, }, render_language: function(value) { - if (!value) { + if (!value || value === '__default__') { return Proxmox.Utils.defaultText + ' (English)'; } let text = Proxmox.Utils.language_map[value]; @@ -100,6 +100,8 @@ utilities: { return value; }, + renderEnabledIcon: enabled => ``, + language_array: function() { let data = [['__default__', Proxmox.Utils.render_language('')]]; Ext.Object.each(Proxmox.Utils.language_map, function(key, value) { @@ -109,6 +111,31 @@ utilities: { return data; }, + theme_map: { + crisp: 'Light theme', + "proxmox-dark": 'Proxmox Dark', + }, + + render_theme: function(value) { + if (!value || value === '__default__') { + return Proxmox.Utils.defaultText + ' (auto)'; + } + let text = Proxmox.Utils.theme_map[value]; + if (text) { + return text; + } + return value; + }, + + theme_array: function() { + let data = [['__default__', Proxmox.Utils.render_theme('')]]; + Ext.Object.each(Proxmox.Utils.theme_map, function(key, value) { + data.push([key, Proxmox.Utils.render_theme(value)]); + }); + + return data; + }, + bond_mode_gettext_map: { '802.3ad': 'LACP (802.3ad)', 'lacp-balance-slb': 'LACP (balance-slb)', @@ -155,7 +182,7 @@ utilities: { // somewhat like a human would tell durations, omit zero values and do not // give seconds precision if we talk days already format_duration_human: function(ut) { - let seconds = 0, minutes = 0, hours = 0, days = 0; + let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0; if (ut <= 0.1) { return '<0.1s'; @@ -171,7 +198,11 @@ utilities: { hours = remaining % 24; remaining = Math.trunc(remaining / 24); if (remaining > 0) { - days = remaining; + days = remaining % 365; + remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years... + if (remaining > 0) { + years = remaining; + } } } } @@ -182,11 +213,14 @@ utilities: { return t > 0; }; + let addMinutes = !add(years, 'y'); let addSeconds = !add(days, 'd'); add(hours, 'h'); - add(minutes, 'm'); - if (addSeconds) { - add(seconds, 's'); + if (addMinutes) { + add(minutes, 'm'); + if (addSeconds) { + add(seconds, 's'); + } } return res.join(' '); }, @@ -299,7 +333,8 @@ utilities: { if (Proxmox.LoggedOut) { return; } - Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name); + // ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure" + Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true); window.localStorage.removeItem("ProxmoxUser"); }, @@ -388,16 +423,15 @@ utilities: { if (!result.success) { msg = gettext("Unknown error"); if (result.message) { - msg = result.message; + msg = Ext.htmlEncode(result.message); if (result.status) { - msg += ' (' + result.status + ')'; + msg += ` (${result.status})`; } } if (verbose && Ext.isObject(result.errors)) { msg += "
"; - Ext.Object.each(result.errors, function(prop, desc) { - msg += "
" + Ext.htmlEncode(prop) + ": " + - Ext.htmlEncode(desc); + Ext.Object.each(result.errors, (prop, desc) => { + msg += `
${Ext.htmlEncode(prop)}: ${Ext.htmlEncode(desc)}`; }); } } @@ -411,6 +445,10 @@ utilities: { waitMsg: gettext('Please wait...'), }, reqOpts); + // default to enable if user isn't handling the failure already explicitly + let autoErrorAlert = reqOpts.autoErrorAlert ?? + (typeof reqOpts.failure !== 'function' && typeof reqOpts.callback !== 'function'); + if (!newopts.url.match(/^\/api2/)) { newopts.url = '/api2/extjs' + newopts.url; } @@ -432,6 +470,9 @@ utilities: { response.htmlStatus = Proxmox.Utils.extractRequestError(result, true); Ext.callback(callbackFn, options.scope, [options, false, response]); Ext.callback(failureFn, options.scope, [response, options]); + if (autoErrorAlert) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } return; } Ext.callback(callbackFn, options.scope, [options, true, response]); @@ -561,7 +602,7 @@ utilities: { return; } - let items = container.query('>'); // direct childs + let items = container.query('>'); // direct children factor = Math.min(factor, items.length); container.oldFactor = factor; @@ -664,6 +705,53 @@ utilities: { return `${size.toFixed(commaDigits)} ${unit}B`; }, + SizeUnits: { + 'B': 1, + + 'KiB': 1024, + 'MiB': 1024*1024, + 'GiB': 1024*1024*1024, + 'TiB': 1024*1024*1024*1024, + 'PiB': 1024*1024*1024*1024*1024, + + 'KB': 1000, + 'MB': 1000*1000, + 'GB': 1000*1000*1000, + 'TB': 1000*1000*1000*1000, + 'PB': 1000*1000*1000*1000*1000, + }, + + parse_size_unit: function(val) { + //let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i); + let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i); + let size = parseFloat(m[1]); + let scale = m[2].toUpperCase(); + let binary = m[3].toLowerCase(); + + let unit = `${scale}${binary}B`; + let factor = Proxmox.Utils.SizeUnits[unit]; + + return { size, factor, unit, binary }; // for convenience return all we got + }, + + size_unit_to_bytes: function(val) { + let { size, factor } = Proxmox.Utils.parse_size_unit(val); + return size * factor; + }, + + autoscale_size_unit: function(val) { + let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val); + return Proxmox.Utils.format_size(size * factor, binary !== "i"); + }, + + size_unit_ratios: function(a, b) { + a = typeof a !== "undefined" ? a : 0; + b = typeof b !== "undefined" ? b : Infinity; + let aBytes = typeof a === "number" ? a : Proxmox.Utils.size_unit_to_bytes(a); + let bBytes = typeof b === "number" ? b : Proxmox.Utils.size_unit_to_bytes(b); + return aBytes / (bBytes || Infinity); // avoid division by zero + }, + render_upid: function(value, metaData, record) { let task = record.data; let type = task.type || task.worker_type; @@ -1172,6 +1260,152 @@ utilities: { return Proxmox.Utils.unknownText; }, + + render_u2f_error: function(error) { + var ErrorNames = { + '1': gettext('Other Error'), + '2': gettext('Bad Request'), + '3': gettext('Configuration Unsupported'), + '4': gettext('Device Ineligible'), + '5': gettext('Timeout'), + }; + return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText; + }, + + // Convert an ArrayBuffer to a base64url encoded string. + // A `null` value will be preserved for convenience. + bytes_to_base64url: function(bytes) { + if (bytes === null) { + return null; + } + + return btoa(Array + .from(new Uint8Array(bytes)) + .map(val => String.fromCharCode(val)) + .join(''), + ) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/[=]/g, ''); + }, + + // Convert an a base64url string to an ArrayBuffer. + // A `null` value will be preserved for convenience. + base64url_to_bytes: function(b64u) { + if (b64u === null) { + return null; + } + + return new Uint8Array( + atob(b64u + .replace(/-/g, '+') + .replace(/_/g, '/'), + ) + .split('') + .map(val => val.charCodeAt(0)), + ); + }, + + stringToRGB: function(string) { + let hash = 0; + if (!string) { + return hash; + } + string += 'prox'; // give short strings more variance + for (let i = 0; i < string.length; i++) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; // to int + } + + let alpha = 0.7; // make the color a bit brighter + let bg = 255; // assume white background + + return [ + (hash & 255) * alpha + bg * (1 - alpha), + ((hash >> 8) & 255) * alpha + bg * (1 - alpha), + ((hash >> 16) & 255) * alpha + bg * (1 - alpha), + ]; + }, + + rgbToCss: function(rgb) { + return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + }, + + rgbToHex: function(rgb) { + let r = Math.round(rgb[0]).toString(16); + let g = Math.round(rgb[1]).toString(16); + let b = Math.round(rgb[2]).toString(16); + return `${r}${g}${b}`; + }, + + hexToRGB: function(hex) { + if (!hex) { + return undefined; + } + if (hex.length === 7) { + hex = hex.slice(1); + } + let r = parseInt(hex.slice(0, 2), 16); + let g = parseInt(hex.slice(2, 4), 16); + let b = parseInt(hex.slice(4, 6), 16); + return [r, g, b]; + }, + + // optimized & simplified SAPC function + // https://github.com/Myndex/SAPC-APCA + getTextContrastClass: function(rgb) { + const blkThrs = 0.022; + const blkClmp = 1.414; + + // linearize & gamma correction + let r = (rgb[0] / 255) ** 2.4; + let g = (rgb[1] / 255) ** 2.4; + let b = (rgb[2] / 255) ** 2.4; + + // relative luminance sRGB + let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750; + + // black clamp + bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp; + + // SAPC with white text + let contrastLight = bg ** 0.65 - 1; + // SAPC with black text + let contrastDark = bg ** 0.56 - 0.046134502; + + if (Math.abs(contrastLight) >= Math.abs(contrastDark)) { + return 'light'; + } else { + return 'dark'; + } + }, + + getTagElement: function(string, color_overrides) { + let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string); + let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`; + let cls; + if (rgb.length > 3) { + style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`; + cls = "proxmox-tag-dark"; + } else { + let txtCls = Proxmox.Utils.getTextContrastClass(rgb); + cls = `proxmox-tag-${txtCls}`; + } + return `${string}`; + }, + + // Setting filename here when downloading from a remote url sometimes fails in chromium browsers + // because of a bug when using attribute download in conjunction with a self signed certificate. + // For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362 + downloadAsFile: function(source, fileName) { + let hiddenElement = document.createElement('a'); + hiddenElement.href = source; + hiddenElement.target = '_blank'; + if (fileName) { + hiddenElement.download = fileName; + } + hiddenElement.click(); + }, }, singleton: true, @@ -1215,6 +1449,8 @@ utilities: { me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$"); me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$"); + me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/; + me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(?::(\\d+))?$"); me.HostPortBrackets_match = new RegExp("^\\[(" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](?::(\\d+))?$"); me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$"); @@ -1227,7 +1463,7 @@ Ext.define('Proxmox.Async', { singleton: true, // Returns a Promise resolving to the result of an `API2Request` or rejecting to the error - // repsonse on failure + // response on failure api2: function(reqOpts) { return new Promise((resolve, reject) => { delete reqOpts.callback; // not allowed in this api @@ -1242,3 +1478,19 @@ Ext.define('Proxmox.Async', { return new Promise((resolve, _reject) => setTimeout(resolve, millis)); }, }); + +Ext.override(Ext.data.Store, { + // If the store's proxy is changed while it is waiting for an AJAX + // response, `onProxyLoad` will still be called for the outdated response. + // To avoid displaying inconsistent information, only process responses + // belonging to the current proxy. However, do not apply this workaround + // to the mobile UI, as Sencha Touch has an incompatible internal API. + onProxyLoad: function(operation) { + let me = this; + if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) { + me.callParent(arguments); + } else { + console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`); + } + }, +});