X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FUtils.js;h=52cc626f1438b6bf1023e5b109cc5b5ecebe07c1;hb=c1a3584103284a328fe263264de28de987dab51c;hp=7b78eeb12a5ecdc97f7367820148328264a17433;hpb=63ec56e5bc8cd814267e493de73179d62246b8bd;p=proxmox-widget-toolkit.git diff --git a/src/Utils.js b/src/Utils.js index 7b78eeb..52cc626 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -48,6 +48,7 @@ utilities: { noneText: gettext('none'), NoneText: gettext('None'), errorText: gettext('Error'), + warningsText: gettext('Warnings'), unknownText: gettext('Unknown'), defaultText: gettext('Default'), daysText: gettext('days'), @@ -89,7 +90,7 @@ utilities: { }, render_language: function(value) { - if (!value) { + if (!value || value === '__default__') { return Proxmox.Utils.defaultText + ' (English)'; } let text = Proxmox.Utils.language_map[value]; @@ -154,7 +155,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'; @@ -170,7 +171,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; + } } } } @@ -181,11 +186,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(' '); }, @@ -237,6 +245,30 @@ utilities: { return min < width ? width : min; }, + // returns username + realm + parse_userid: function(userid) { + if (!Ext.isString(userid)) { + return [undefined, undefined]; + } + + let match = userid.match(/^(.+)@([^@]+)$/); + if (match !== null) { + return [match[1], match[2]]; + } + + return [undefined, undefined]; + }, + + render_username: function(userid) { + let username = Proxmox.Utils.parse_userid(userid)[0] || ""; + return Ext.htmlEncode(username); + }, + + render_realm: function(userid) { + let username = Proxmox.Utils.parse_userid(userid)[1] || ""; + return Ext.htmlEncode(username); + }, + getStoredAuth: function() { let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser')); return storedAuth || {}; @@ -274,10 +306,20 @@ utilities: { if (Proxmox.LoggedOut) { return; } - Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name); + Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true); window.localStorage.removeItem("ProxmoxUser"); }, + // The End-User gets redirected back here after login on the OpenID auth. portal, and in the + // redirection URL the state and auth.code are passed as URL GET params, this helper parses those + getOpenIDRedirectionAuthorization: function() { + const auth = Ext.Object.fromQueryString(window.location.search); + if (auth.state !== undefined && auth.code !== undefined) { + return auth; + } + return undefined; + }, + // comp.setLoading() is buggy in ExtJS 4.0.7, so we // use el.mask() instead setErrorMask: function(comp, msg) { @@ -316,7 +358,7 @@ utilities: { return msg.join('
'); }, - monStoreErrors: function(component, store, clearMaskBeforeLoad) { + monStoreErrors: function(component, store, clearMaskBeforeLoad, errorCallback) { if (clearMaskBeforeLoad) { component.mon(store, 'beforeload', function(s, operation, eOpts) { Proxmox.Utils.setErrorMask(component, false); @@ -341,7 +383,9 @@ utilities: { let error = request._operation.getError(); let msg = Proxmox.Utils.getResponseErrorMessage(error); - Proxmox.Utils.setErrorMask(component, msg); + if (!errorCallback || !errorCallback(error, msg)) { + Proxmox.Utils.setErrorMask(component, msg); + } }); }, @@ -443,6 +487,17 @@ utilities: { Ext.Ajax.request(newopts); }, + // can be useful for catching displaying errors from the API, e.g.: + // Proxmox.Async.api2({ + // ... + // }).catch(Proxmox.Utils.alertResponseFailure); + alertResponseFailure: (response) => { + Ext.Msg.alert( + gettext('Error'), + response.htmlStatus || response.result.message, + ); + }, + checked_command: function(orig_cmd) { Proxmox.Utils.API2Request( { @@ -496,7 +551,7 @@ utilities: { }); }, - updateColumnWidth: function(container) { + updateColumnWidth: function(container, thresholdWidth) { let mode = Ext.state.Manager.get('summarycolumns') || 'auto'; let factor; if (mode !== 'auto') { @@ -505,14 +560,15 @@ utilities: { factor = 1; } } else { - factor = container.getSize().width < 1600 ? 1 : 2; + thresholdWidth = (thresholdWidth || 1400) + 1; + factor = Math.ceil(container.getSize().width / thresholdWidth); } if (container.oldFactor === factor) { return; } - let items = container.query('>'); // direct childs + let items = container.query('>'); // direct children factor = Math.min(factor, items.length); container.oldFactor = factor; @@ -526,6 +582,9 @@ utilities: { container.updateLayout(); }, + // NOTE: depreacated, use updateColumnWidth + updateColumns: container => Proxmox.Utils.updateColumnWidth(container), + dialog_title: function(subject, create, isAdd) { if (create) { if (isAdd) { @@ -554,6 +613,7 @@ utilities: { Proxmox.Utils.unknownText; }, + // NOTE: only add general, product agnostic, ones here! Else use override helper in product repos task_desc_table: { aptupdate: ['', gettext('Update package database')], diskinit: ['Disk', gettext('Initialize Disk with GPT')], @@ -593,14 +653,69 @@ utilities: { return text; }, - format_size: function(size) { - let units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; - let num = 0; - while (size >= 1024 && num++ <= units.length) { - size = size / 1024; + format_size: function(size, useSI) { + let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + let order = 0; + const baseValue = useSI ? 1000 : 1024; + while (size >= baseValue && order < units.length) { + size = size / baseValue; + order++; + } + + let unit = units[order], commaDigits = 2; + if (order === 0) { + commaDigits = 0; + } else if (!useSI) { + unit += 'i'; } + 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"); + }, - return size.toFixed(num > 0?2:0) + " " + units[num] + "B"; + 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) { @@ -724,6 +839,17 @@ utilities: { return 'error'; }, + format_task_status: function(status) { + let parsed = Proxmox.Utils.parse_task_status(status); + switch (parsed) { + case 'unknown': return Proxmox.Utils.unknownText; + case 'error': return Proxmox.Utils.errorText + ': ' + status; + case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText); + case 'ok': // fall-through + default: return status; + } + }, + render_duration: function(value) { if (value === undefined) { return '-'; @@ -809,6 +935,342 @@ utilities: { } }, + render_optional_url: function(value) { + if (value && value.match(/^https?:\/\//) !== null) { + return '' + value + ''; + } + return value; + }, + + render_san: function(value) { + var names = []; + if (Ext.isArray(value)) { + value.forEach(function(val) { + if (!Ext.isNumber(val)) { + names.push(val); + } + }); + return names.join('
'); + } + return value; + }, + + render_usage: val => (val * 100).toFixed(2) + '%', + + render_cpu_usage: function(val, max) { + return Ext.String.format( + `${gettext('{0}% of {1}')} ${gettext('CPU(s)')}`, + (val*100).toFixed(2), + max, + ); + }, + + render_size_usage: function(val, max, useSI) { + if (max === 0) { + return gettext('N/A'); + } + let fmt = v => Proxmox.Utils.format_size(v, useSI); + let ratio = (val * 100 / max).toFixed(2); + return ratio + '% (' + Ext.String.format(gettext('{0} of {1}'), fmt(val), fmt(max)) + ')'; + }, + + render_cpu: function(value, metaData, record, rowIndex, colIndex, store) { + if (!(record.data.uptime && Ext.isNumeric(value))) { + return ''; + } + + let maxcpu = record.data.maxcpu || 1; + if (!Ext.isNumeric(maxcpu) || maxcpu < 1) { + return ''; + } + let cpuText = maxcpu > 1 ? 'CPUs' : 'CPU'; + let ratio = (value * 100).toFixed(1); + return `${ratio}% of ${maxcpu.toString()} ${cpuText}`; + }, + + render_size: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value)) { + return ''; + } + return Proxmox.Utils.format_size(value); + }, + + render_cpu_model: function(cpu) { + let socketText = cpu.sockets > 1 ? gettext('Sockets') : gettext('Socket'); + return `${cpu.cpus} x ${cpu.model} (${cpu.sockets.toString()} ${socketText})`; + }, + + /* this is different for nodes */ + render_node_cpu_usage: function(value, record) { + return Proxmox.Utils.render_cpu_usage(value, record.cpus); + }, + + render_node_size_usage: function(record) { + return Proxmox.Utils.render_size_usage(record.used, record.total); + }, + + loadTextFromFile: function(file, callback, maxBytes) { + let maxSize = maxBytes || 8192; + if (file.size > maxSize) { + Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size); + return; + } + let reader = new FileReader(); + reader.onload = evt => callback(evt.target.result); + reader.readAsText(file); + }, + + parsePropertyString: function(value, defaultKey) { + var res = {}, + error; + + if (typeof value !== 'string' || value === '') { + return res; + } + + Ext.Array.each(value.split(','), function(p) { + var kv = p.split('=', 2); + if (Ext.isDefined(kv[1])) { + res[kv[0]] = kv[1]; + } else if (Ext.isDefined(defaultKey)) { + if (Ext.isDefined(res[defaultKey])) { + error = 'defaultKey may be only defined once in propertyString'; + return false; // break + } + res[defaultKey] = kv[0]; + } else { + error = 'invalid propertyString, not a key=value pair and no defaultKey defined'; + return false; // break + } + return true; + }); + + if (error !== undefined) { + console.error(error); + return undefined; + } + + return res; + }, + + printPropertyString: function(data, defaultKey) { + var stringparts = [], + gotDefaultKeyVal = false, + defaultKeyVal; + + Ext.Object.each(data, function(key, value) { + if (defaultKey !== undefined && key === defaultKey) { + gotDefaultKeyVal = true; + defaultKeyVal = value; + } else if (Ext.isArray(value)) { + stringparts.push(key + '=' + value.join(';')); + } else if (value !== '') { + stringparts.push(key + '=' + value); + } + }); + + stringparts = stringparts.sort(); + if (gotDefaultKeyVal) { + stringparts.unshift(defaultKeyVal); + } + + return stringparts.join(','); + }, + + acmedomain_count: 5, + + parseACMEPluginData: function(data) { + let res = {}; + let extradata = []; + data.split('\n').forEach((line) => { + // capture everything after the first = as value + let [key, value] = line.split('='); + if (value !== undefined) { + res[key] = value; + } else { + extradata.push(line); + } + }); + return [res, extradata]; + }, + + delete_if_default: function(values, fieldname, default_val, create) { + if (values[fieldname] === '' || values[fieldname] === default_val) { + if (!create) { + if (values.delete) { + if (Ext.isArray(values.delete)) { + values.delete.push(fieldname); + } else { + values.delete += ',' + fieldname; + } + } else { + values.delete = fieldname; + } + } + + delete values[fieldname]; + } + }, + + printACME: function(value) { + if (Ext.isArray(value.domains)) { + value.domains = value.domains.join(';'); + } + return Proxmox.Utils.printPropertyString(value); + }, + + parseACME: function(value) { + if (!value) { + return {}; + } + + var res = {}; + var error; + + Ext.Array.each(value.split(','), function(p) { + var kv = p.split('=', 2); + if (Ext.isDefined(kv[1])) { + res[kv[0]] = kv[1]; + } else { + error = 'Failed to parse key-value pair: '+p; + return false; + } + return true; + }); + + if (error !== undefined) { + console.error(error); + return undefined; + } + + if (res.domains !== undefined) { + res.domains = res.domains.split(/;/); + } + + return res; + }, + + add_domain_to_acme: function(acme, domain) { + if (acme.domains === undefined) { + acme.domains = [domain]; + } else { + acme.domains.push(domain); + acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index); + } + return acme; + }, + + remove_domain_from_acme: function(acme, domain) { + if (acme.domains !== undefined) { + acme.domains = acme.domains.filter( + (value, index, self) => self.indexOf(value) === index && value !== domain, + ); + } + return acme; + }, + + get_health_icon: function(state, circle) { + if (circle === undefined) { + circle = false; + } + + if (state === undefined) { + state = 'uknown'; + } + + var icon = 'faded fa-question'; + switch (state) { + case 'good': + icon = 'good fa-check'; + break; + case 'upgrade': + icon = 'warning fa-upload'; + break; + case 'old': + icon = 'warning fa-refresh'; + break; + case 'warning': + icon = 'warning fa-exclamation'; + break; + case 'critical': + icon = 'critical fa-times'; + break; + default: break; + } + + if (circle) { + icon += '-circle'; + } + + return icon; + }, + + formatNodeRepoStatus: function(status, product) { + let fmt = (txt, cls) => `${txt}`; + + let getUpdates = Ext.String.format(gettext('{0} updates'), product); + let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product); + + if (status === 'ok') { + return fmt(getUpdates, 'check-circle good') + ' ' + + fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good'); + } else if (status === 'no-sub') { + return fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good') + ' ' + + fmt(gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning'); + } else if (status === 'non-production') { + return fmt(getUpdates, 'check-circle good') + ' ' + + fmt(gettext('Non production-ready repository enabled!'), 'exclamation-circle warning'); + } else if (status === 'no-repo') { + return fmt(noRepo, 'exclamation-circle critical'); + } + + 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)), + ); + }, }, singleton: true, @@ -850,11 +1312,32 @@ utilities: { 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])?))"; me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$"); + me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$"); 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+))?$"); - me.Vlan_match = /^vlan(\\d+)/; - me.VlanInterface_match = /(\\w+)\\.(\\d+)/; + me.Vlan_match = /^vlan(\d+)/; + me.VlanInterface_match = /(\w+)\.(\d+)/; + }, +}); + +Ext.define('Proxmox.Async', { + singleton: true, + + // Returns a Promise resolving to the result of an `API2Request` or rejecting to the error + // response on failure + api2: function(reqOpts) { + return new Promise((resolve, reject) => { + delete reqOpts.callback; // not allowed in this api + reqOpts.success = response => resolve(response); + reqOpts.failure = response => reject(response); + Proxmox.Utils.API2Request(reqOpts); + }); + }, + + // Delay for a number of milliseconds. + sleep: function(millis) { + return new Promise((resolve, _reject) => setTimeout(resolve, millis)); }, });