/* * This file is part of Linode Manager Classic. * * Linode Manager Classic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Linode Manager Classic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Linode Manager Classic. If not, see . */ // Global default settings var settings = { "apiURL": "https://api.linode.com/v4", "loginURL": "https://login.linode.com", "oauthURL": "https://login.linode.com/oauth", "preferredDistro": "linode/debian10", "preferredKernel": "linode/grub2", "refreshRate": 2000 }; // Strings containing the names of various elements and CSS selectors var elements = { "navlink": "navlink", "navlinkActive": "navlink-active", "profilePic": "profile-pic", "subnavLink": "subnav-link", "subnavLinkActive": "subnav-link-active", "username": "username" }; // Regions (Linode doesn't provide "friendly" names via the API) var regionNames = { "us-central": "Dallas, TX, USA", "us-west": "Fremont, CA, USA", "us-southeast": "Atlanta, GA, USA", "us-east": "Newark, NJ, USA", "us-east-1b": "Newark 2, NJ, USA", "eu-west": "London, England, UK", "ap-south": "Singapore, SG", "eu-central": "Frankfurt, DE", "ap-northeast": "Tokyo, JP", "ap-northeast-1a": "Tokyo 2, JP", "ca-central": "Toronto, ON, CA", "ap-west": "Mumbai, IN", "ap-southeast": "Sydney, NSW, AU", "philadelphia": "Philadelphia, PA, USA", "absecon": "Absecon, NJ, USA", "us-iad": "Washington DC, USA", "us-ord": "Chicago, IL, USA", "fr-par": "Paris, FR" }; // Human-readable event titles var eventTitles = { "account_update": "Update Account", "account_settings_update": "Update Account Settings", "backups_enable": "Backups Enabled", "backups_cancel": "Backups Cancelled", "backups_restore": "Restore From Backup", "community_question_reply": "Community Question Reply", "community_like": "Community Like", "credit_card_updated": "Update Credit Card", "disk_create": "Create Disk", "disk_delete": "Delete Disk", "disk_update": "Update Disk", "disk_duplicate": "Clone Disk", "disk_imagize": "Create Image From Disk", "disk_resize": "Resize Disk", "dns_record_create": "Create DNS Record", "dns_record_delete": "Delete DNS Record", "dns_record_update": "Update DNS Record", "dns_zone_create": "Create DNS Zone", "dns_zone_delete": "Delete DNS Zone", "dns_zone_import": "Import DNS Zone", "dns_zone_update": "Update DNS Zone", "firewall_create": "Create Firewall", "firewall_delete": "Delete Firewall", "firewall_disable": "Disable Firewall", "firewall_enable": "Enable Firewall", "firewall_update": "Update Firewall", "firewall_device_add": "Add Firewall Device", "firewall_device_remove": "Remove Firewall Device", "host_reboot": "Host Initiated Restart", "image_delete": "Delete Image", "image_update": "Update Image", "ipaddress_update": "Update IP Address", "lassie_reboot": "Lassie Initiated Boot", "lish_boot": "LISH Initiated Boot", "linode_addip": "Add IP Address", "linode_boot": "System Boot", "linode_clone": "Clone Linode", "linode_create": "Linode Initial Configuration", "linode_delete": "Inactivate Linode", "linode_update": "Update Linode", "linode_deleteip": "Remove IP Address", "linode_migrate": "Migrate Linode", "linode_migrate_datacenter": "Migrate Linode", "linode_migrate_datacenter_create": "Migrate Linode", "linode_mutate": "Upgrade Linode", "linode_mutate_create": "Upgrade Linode", "linode_reboot": "System Reboot", "linode_rebuild": "Rebuild Linode", "linode_resize": "Resize Linode", "linode_resize_create": "Resize Linode", "linode_shutdown": "System Shutdown", "linode_snapshot": "Linode Snapshot", "linode_config_create": "Create Configuration Profile", "linode_config_delete": "Delete Configuration Profile", "linode_config_update": "Update Configuration Profile", "lke_node_create": "Create LKE Node", "longviewclient_create": "Create Longview Client", "longviewclient_delete": "Delete Longview Client", "longviewclient_update": "Update Longview Client", "managed_disabled": "Managed Service Disabled", "managed_enabled": "Managed Service Enabled", "managed_service_create": "Create Managed Service", "managed_service_delete": "Delete Managed Service", "nodebalancer_create": "NodeBalancer Initial Configuration", "nodebalancer_delete": "Inactivate NodeBalancer", "nodebalancer_update": "Update NodeBalancer", "nodebalancer_config_create": "Create NodeBalancer Configuration", "nodebalancer_config_delete": "Delete NodeBalancer Configuration", "nodebalancer_config_update": "Update NodeBalancer Configuration", "nodebalancer_node_create": "Create NodeBalancer Node", "nodebalancer_node_delete": "Delete NodeBalancer Node", "nodebalancer_node_update": "Update NodeBalancer Node", "oauth_client_create": "Create OAuth Client", "oauth_client_delete": "Delete OAuth Client", "oauth_client_secret_reset": "Reset OAuth Client Secret", "oauth_client_update": "Update OAuth Client", "password_reset": "Change Root Password", "payment_submitted": "Payment Submitted", "profile_update": "Update Profile", "stackscript_create": "Create StackScript", "stackscript_delete": "Delete StackScript", "stackscript_update": "Update StackScript", "stackscript_publicize": "Publish StackScript", "stackscript_revise": "Revise StackScript", "tag_create": "Create Tag", "tag_delete": "Delete Tag", "tfa_disabled": "2FA Disabled", "tfs_enabled": "2FA Enabled", "ticket_attachment_upload": "Uploaded Ticket Attachment", "ticket_create": "Create Ticket", "ticket_update": "Update Ticket", "token_create": "Create API Token", "token_delete": "Delete API Token", "token_update": "Update API Token", "user_create": "Create User", "user_delete": "Delete User", "user_update": "Update User", "user_ssh_key_add": "Add SSH Key", "user_ssh_key_delete": "Delete SSH Key", "user_ssh_key_update": "Update SSH Key", "vlan_attach": "Attach VLAN", "vlan_detach": "Detach VLAN", "volume_attach": "Attach Volume", "volume_clone": "Clone Volume", "volume_create": "Create Volume", "volume_delete": "Delete Volume", "volume_update": "Update Volume", "volume_detach": "Detach Volume", "volume_resize": "Resize Volume" }; // A list of OAuth scopes var oauthScopes = { "account": "Account", "databases": "Databases", "domains": "Domains", "events": "Events", "firewall": "Firewalls", "images": "Images", "ips": "IPs", "linodes": "Linodes", "lke": "Kubernetes", "longview": "Longview", "nodebalancers": "NodeBalancers", "object_storage": "Object Storage", "stackscripts": "StackScripts", "volumes": "Volumes" }; // Make an HTTP DELETE request to the Linode API function apiDelete(endpoint, callback) { // Redirect to login if there is no API key or it's expired if (localStorage.apiKey && localStorage.apiExpire) { var now = new Date(); var expires = new Date(parseInt(localStorage.apiExpire)); if (expires <= now) { alert("Your session has expired."); localStorage.removeItem("apiKey"); localStorage.removeItem("apiExpire"); location.href = "/?skip=1&redirectTo=" + encodeURIComponent(location.href); return; } } else { location.href = "/"; return; } var xmlhttp = new XMLHttpRequest(); xmlhttp.open("DELETE", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; var response = JSON.parse(xmlhttp.responseText); if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("DELETE " + settings.apiURL + endpoint); if (response.errors) { for (var i = 0; i < response.errors.length; i++) { if (response.errors[i].field) alert(response.errors[i].field + " " + response.errors[i].reason); else alert(response.errors[i].reason); } } // Redirect to login if invalid API key if (xmlhttp.status == 401) { localStorage.removeItem("apiKey"); location.href = "/"; } return; } response['_endpoint'] = endpoint; callback(response); }; xmlhttp.send(); } // Make an HTTP GET request to the Linode API function apiGet(endpoint, callback, filters) { // Redirect to login if there is no API key or it's expired if (localStorage.apiKey && localStorage.apiExpire) { var now = new Date(); var expires = new Date(parseInt(localStorage.apiExpire)); if (expires <= now) { alert("Your session has expired."); localStorage.removeItem("apiKey"); localStorage.removeItem("apiExpire"); location.href = "/?skip=1&redirectTo=" + encodeURIComponent(location.href); return; } } else { location.href = "/"; return; } var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); if (filters) xmlhttp.setRequestHeader("X-Filter", JSON.stringify(filters)); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; var response = JSON.parse(xmlhttp.responseText); if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("GET " + settings.apiURL + endpoint); if (response.errors) { for (var i = 0; i < response.errors.length; i++) { if (response.errors[i].field) alert(response.errors[i].field + " " + response.errors[i].reason); else alert(response.errors[i].reason); } } // Redirect to login if invalid API key if (xmlhttp.status == 401) { localStorage.removeItem("apiKey"); location.href = "/"; } // Redirect to linodes page if unauthorized if (xmlhttp.status == 403) location.href = "/linodes"; return; } response['_endpoint'] = endpoint; callback(response); }; xmlhttp.send(); } // Make an HTTP POST request to the Linode API function apiPost(endpoint, data, callback) { // Redirect to login if there is no API key or it's expired if (localStorage.apiKey && localStorage.apiExpire) { var now = new Date(); var expires = new Date(parseInt(localStorage.apiExpire)); if (expires <= now) { alert("Your session has expired."); localStorage.removeItem("apiKey"); localStorage.removeItem("apiExpire"); location.href = "/?skip=1&redirectTo=" + encodeURIComponent(location.href); return; } } else { location.href = "/"; return; } var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); if (data instanceof File) xmlhttp.setRequestHeader("Content-Type", data.type); else xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; var response = JSON.parse(xmlhttp.responseText); if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("POST " + settings.apiURL + endpoint); if (response.errors) { for (var i = 0; i < response.errors.length; i++) { if (response.errors[i].field) alert(response.errors[i].field + " " + response.errors[i].reason); else alert(response.errors[i].reason); } } // Redirect to login if invalid token if (xmlhttp.status == 401) { localStorage.removeItem("apiKey"); location.href = "/"; } return; } response['_endpoint'] = endpoint; callback(response); }; if (data instanceof File) xmlhttp.send(data); else xmlhttp.send(JSON.stringify(data)); } // Make an HTTP PUT request to the Linode API function apiPut(endpoint, data, callback) { // Redirect to login if there is no API key or it's expired if (localStorage.apiKey && localStorage.apiExpire) { var now = new Date(); var expires = new Date(parseInt(localStorage.apiExpire)); if (expires <= now) { alert("Your session has expired."); localStorage.removeItem("apiKey"); localStorage.removeItem("apiExpire"); location.href = "/?skip=1&redirectTo=" + encodeURIComponent(location.href); return; } } else { location.href = "/"; return; } var xmlhttp = new XMLHttpRequest(); xmlhttp.open("PUT", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); if (data instanceof File) xmlhttp.setRequestHeader("Content-Type", data.type); else xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; var response = JSON.parse(xmlhttp.responseText); if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("PUT " + settings.apiURL + endpoint); if (response.errors) { for (var i = 0; i < response.errors.length; i++) { if (response.errors[i].field) alert(response.errors[i].field + " " + response.errors[i].reason); else alert(response.errors[i].reason); } } // Redirect to login if invalid token if (xmlhttp.status == 401) { localStorage.removeItem("apiKey"); location.href = "/"; } return; } response['_endpoint'] = endpoint; callback(response); }; if (data instanceof File) xmlhttp.send(data); else xmlhttp.send(JSON.stringify(data)); } // Callback for user info API call function displayUser(response) { if (!response) { console.log("Error getting profile data"); return; } // Display username var usernameTag = document.getElementById(elements.username); usernameTag.innerHTML = response.username; // Display profile pic var profilePic = document.getElementById(elements.profilePic); profilePic.src = "https://www.gravatar.com/avatar/" + md5(response.email, false); profilePic.style = "display: initial;"; } // Draw timeseries data with the given canvas in the given color and fill // series is an array of objects, with each object containing the color/fill settings and an array of data points function drawSeries(series, canvas) { // Compute scale and totals var xMin = series[0].points[0][0]; var xMax = series[0].points[series[0].points.length - 1][0]; var yMax = 0; for (var i = 0; i < series.length; i++) { xMin = Math.min(xMin, series[i].points[0][0]); xMax = Math.max(xMax, series[i].points[series[i].points.length - 1][0]); series[i].max = 0, series[i].avg = 0; for (var j = 0; j < series[i].points.length; j++) { series[i].max = Math.max(series[i].max, series[i].points[j][1]); series[i].avg += series[i].points[j][1]; } series[i].avg /= series[i].points.length; yMax = Math.max(yMax, series[i].max); } xMax -= xMin; // Setup drawing context var ctx = canvas.getContext("2d"); ctx.lineWidth = 1; // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); for (var i = 0; i < series.length; i++) { ctx.fillStyle = series[i].color; ctx.strokeStyle = series[i].color; // Draw data ctx.beginPath(); ctx.moveTo((series[i].points[0][0] - xMin) / xMax * canvas.width, canvas.height - (series[i].points[0][1] / yMax * canvas.height)); for (var j = 1; j < series[i].points.length; j++) ctx.lineTo((series[i].points[j][0] - xMin) / xMax * canvas.width, canvas.height - (series[i].points[j][1] / yMax * canvas.height)); if (series[i].fill) { ctx.lineTo((series[i].points[series[i].points.length-1][0] - xMin) / xMax * canvas.width, canvas.height); ctx.lineTo((series[i].points[0][0] - xMin) / xMax * canvas.width, canvas.height); ctx.closePath(); ctx.fill(); } else { ctx.stroke(); } } // Draw axis lines ctx.strokeStyle = "black"; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, canvas.height); ctx.lineTo(canvas.width, canvas.height); ctx.stroke(); } // Return an MD5 hash of the given string function md5(str, binary) { // Convert string to bytes var data; if (binary) { var strBytes = []; for (var i = 0; i < str.length; i++) strBytes[i] = str.charCodeAt(i); data = new Uint8Array(strBytes); } else { data = new TextEncoder("utf-8").encode(str); } // Constants var s = new Uint32Array([ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 ]); var K = new Uint32Array([ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 ]); // Initialize hash var hash = new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]); // Apply padding var padLength = data.length + 1; while (padLength % 64 != 56) padLength++; padLength += 8; var padData = new Uint8Array(padLength); for (var i = 0; i < data.length; i++) padData[i] = data[i]; for (var i = data.length; i < padLength; i++) padData[i] = 0x0; // Append 1 bit padData[data.length] = 0x80; // Append original length var bits = data.length * 8; var upperLen = parseInt(bits.toString(16).slice(0, -8), 16); if (isNaN(upperLen)) upperLen = 0; padData[padLength-8] = bits; padData[padLength-7] = bits >>> 8; padData[padLength-6] = bits >>> 16; padData[padLength-5] = bits >>> 24; padData[padLength-4] = upperLen; padData[padLength-3] = upperLen >>> 8; padData[padLength-2] = upperLen >>> 16; padData[padLength-1] = upperLen >>> 24; // Process the data in 64-byte chunks for (var i = 0; i < padLength; i += 64) { var M = new Uint32Array(16); for (var j = 0, offset = i; j < 16; j++, offset += 4) M[j] = padData[offset] + (padData[offset+1] << 8) + (padData[offset+2] << 16) + (padData[offset+3] << 24); // Main loop var chunk = new Uint32Array(hash); for (var j = 0; j < 64; j++) { var locals = new Uint32Array(2); if (j <= 15) { locals[0] = (chunk[1] & chunk[2]) | ((~chunk[1]) & chunk[3]); locals[1] = j; } else if (j <= 31) { locals[0] = (chunk[3] & chunk[1]) | ((~chunk[3]) & chunk[2]); locals[1] = (5 * j + 1) % 16; } else if (j <= 47) { locals[0] = chunk[1] ^ chunk[2] ^ chunk[3]; locals[1] = (3 * j + 5) % 16; } else { locals[0] = chunk[2] ^ (chunk[1] | (~chunk[3])); locals[1] = (7 * j) % 16; } locals[0] += chunk[0] + K[j] + M[locals[1]]; chunk[0] = chunk[3]; chunk[3] = chunk[2]; chunk[2] = chunk[1]; chunk[1] += rotl(locals[0], s[j]); } // Add this chunk's hash to the main hash hash[0] += chunk[0]; hash[1] += chunk[1]; hash[2] += chunk[2]; hash[3] += chunk[3]; } // Convert to string var md5Str = ""; for (var i = 0; i < 4; i++) { var bytes = new Uint8Array([hash[i], hash[i] >> 8, hash[i] >> 16, hash[i] >> 24]); for (var j = 0; j < 4; j++) { if (bytes[j] < 0x10) md5Str += "0"; md5Str += bytes[j].toString(16); if (binary && (i != 3 || j != 3)) md5Str += ":"; } } return md5Str; } // Returns the ETA for a migration given the disk size (MB) and destination function migrateETA(size, local) { var str = ""; // Calculate minutes var minutes = 0; if (local) minutes = (0.75 * size / 1024).toFixed(0); else minutes = (10 * size / 1024).toFixed(0); // Divide into hours if (minutes >= 60) { var hours = Math.floor(minutes / 60); minutes = minutes % 60; if (hours == 1) hours = hours + " hour and "; else hours = hours + " hours and "; str += hours; } if (minutes == 1) minutes = minutes + " minute"; else minutes = minutes + " minutes"; str += minutes; return str; } // Make an OAuth HTTP POST request function oauthPost(endpoint, data, callback) { var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", settings.oauthURL + endpoint, true); xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; var response = JSON.parse(xmlhttp.responseText); if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("POST " + settings.oauthURL + endpoint); if (response.errors) { for (var i = 0; i < response.errors.length; i++) { if (response.errors[i].field) alert(response.errors[i].field + " " + response.errors[i].reason); else alert(response.errors[i].reason); } } return; } response['_endpoint'] = endpoint; callback(response); }; xmlhttp.send(data.toString()); } // Make an object storage HTTP PUT request function objPut(url, data, progress, callback) { var xmlhttp = new XMLHttpRequest(); xmlhttp.upload.addEventListener("progress", progress); xmlhttp.open("PUT", url, true); xmlhttp.setRequestHeader("Content-Type", "application/octet-stream"); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState != 4) return; if (xmlhttp.status >= 400) { console.log("Error " + xmlhttp.status); console.log("PUT " + url); alert("An error occurred during file upload!"); return; } callback(); }; xmlhttp.send(data); } // Parse URL parameters function parseParams() { var parsed = {}; if (location.search.length > 1) var params = new URLSearchParams(location.search.slice(1)); else var params = new URLSearchParams(location.hash.slice(1)); for (var pair of params.entries()) parsed[pair[0]] = pair[1]; return parsed; } // Bitwise left-rotate function rotl(val, sft) { return (val << sft) | (val >>> (32 - sft)); } // Setup the page header function setupHeader() { // Highlight the current page in the navbar var navlinks = document.getElementsByClassName(elements.navlink); for (var i = 0; i < navlinks.length; i++) { if ((location.origin + location.pathname).startsWith(navlinks[i].href)) navlinks[i].className = navlinks[i].className.replace(elements.navlink, elements.navlinkActive); } // Highlight the current page in the subnav var subnavLinks = document.getElementsByClassName(elements.subnavLink); for (var i = subnavLinks.length - 1; i >= 0; i--) { if ((location.origin + location.pathname).startsWith(subnavLinks[i].href.split("?")[0])) { subnavLinks[i].className += " " + elements.subnavLinkActive; break; } } // Get user info apiGet("/profile", displayUser, null); } // Convert a millisecond count into a "friendly" time string function timeString(ms, includeSuffix) { // Future or past? var suffix = ""; if (includeSuffix) { if (ms < 0) { suffix = " from now"; ms = -ms; } else { suffix = " ago"; } } // Break into years, days, hours, minutes, seconds, and ms var units = [0, 0, 0, 0, 0, ms.toFixed(0)]; var unitNames = ["year", "day", "hour", "minute", "second", "millisecond"]; while (units[5] >= 31536000000) { units[5] -= 31536000000; units[0]++; } while (units[5] >= 86400000) { units[5] -= 86400000; units[1]++; } while (units[5] >= 3600000) { units[5] -= 3600000; units[2]++; } while (units[5] >= 60000) { units[5] -= 60000; units[3]++; } while (units[5] >= 1000) { units[5] -= 1000; units[4]++; } // Grab the first two non-zero units var first = ""; var second = ""; for (var i = 0; i < units.length; i++) { if (!units[i]) continue; if (!first.length) { first = units[i] + " " + unitNames[i]; if (units[i] > 1) first += "s"; continue; } if (!second.length) { second = " " + units[i] + " " + unitNames[i]; if (units[i] > 1) second += "s"; break; } } if (!first.length) return "0 milliseconds" + suffix; else return first + second + suffix; } // Translates a kernel slug into a label and sets the contents of an element function translateKernel(slug, element) { var callback = function(response) { element.innerHTML = response.label; }; apiGet("/linode/kernels/" + slug, callback, null); } export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, drawSeries, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel };