lmc/linodes/dashboard/dashboard.js

1159 lines
39 KiB
JavaScript

/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiDelete, apiGet, apiPost, countSI, drawSeries, eventTitles, parseParams, setupHeader, timeString, translateKernel } from "/global.js";
(function()
{
// Element names specific to this page
elements.backups = "backups";
elements.backupsEnabled = "backups-enabled";
elements.boot = "boot";
elements.configRadioName = "config-radio";
elements.configRemovePrefix = "config-remove-";
elements.configTable = "config-table";
elements.cpuAvg = "cpu-avg";
elements.cpuGraph = "cpu-graph";
elements.cpuLast = "cpu-last";
elements.cpuMax = "cpu-max";
elements.diskIcon = "disk-icon";
elements.diskIconImg = "/img/disk.gif";
elements.diskRemovePrefix = "disk-remove-";
elements.diskTable = "disk-table";
elements.diskUsage = "disk-usage";
elements.eventRowPrefix = "event-row-";
elements.eventTable = "event-table";
elements.extraEvent = "extra-event";
elements.graphRange = "graph-range";
elements.info = "info";
elements.ioGraph = "io-graph";
elements.ioRateAvg = "io-rate-avg";
elements.ioRateLast = "io-rate-last";
elements.ioRateMax = "io-rate-max";
elements.ipv4Graph = "ipv4-graph";
elements.ipv4InAvg = "ipv4-in-avg";
elements.ipv4InLast = "ipv4-in-last";
elements.ipv4InMax = "ipv4-in-max";
elements.ipv4OutAvg = "ipv4-out-avg";
elements.ipv4OutLast = "ipv4-out-last";
elements.ipv4OutMax = "ipv4-out-max";
elements.ipv4PrivInAvg = "ipv4-privin-avg";
elements.ipv4PrivInLast = "ipv4-privin-last";
elements.ipv4PrivInMax = "ipv4-privin-max";
elements.ipv4PrivOutAvg = "ipv4-privout-avg";
elements.ipv4PrivOutLast = "ipv4-privout-last";
elements.ipv4PrivOutMax = "ipv4-privout-max";
elements.ipv4Total = "ipv4-total";
elements.ipv4TotalIn = "ipv4-total-in";
elements.ipv4TotalOut = "ipv4-total-out";
elements.ipv6Graph = "ipv6-graph";
elements.ipv6InAvg = "ipv6-in-avg";
elements.ipv6InLast = "ipv6-in-last";
elements.ipv6InMax = "ipv6-in-max";
elements.ipv6OutAvg = "ipv6-out-avg";
elements.ipv6OutLast = "ipv6-out-last";
elements.ipv6OutMax = "ipv6-out-max";
elements.ipv6PrivInAvg = "ipv6-privin-avg";
elements.ipv6PrivInLast = "ipv6-privin-last";
elements.ipv6PrivInMax = "ipv6-privin-max";
elements.ipv6PrivOutAvg = "ipv6-privout-avg";
elements.ipv6PrivOutLast = "ipv6-privout-last";
elements.ipv6PrivOutMax = "ipv6-privout-max";
elements.ipv6Total = "ipv6-total";
elements.ipv6TotalIn = "ipv6-total-in";
elements.ipv6TotalOut = "ipv6-total-out";
elements.jobFailed = "job-failed";
elements.jobInfo = "job-info";
elements.jobNotice = "job-notice";
elements.jobProgress = "job-progress";
elements.jobProgressRow = "job-progress-row";
elements.jobRunning = "job-running";
elements.jobSuccess = "job-success";
elements.jobType = "job-type";
elements.lastBackup = "last-backup";
elements.lastBackupTime = "last-backup-time";
elements.linodeLabel = "linode-label";
elements.linodeTag = "linode-tag";
elements.linodeTagLink = "linode-tag-link";
elements.lmcRow = "lmc-tr3";
elements.loadingConfigs = "loading-configs";
elements.loadingDisks = "loading-disks";
elements.loadingJobs = "loading-jobs";
elements.loadingVolumes = "loading-volumes";
elements.moreJobs = "moar-jobs";
elements.netUsage = "net-usage";
elements.notification = "notification";
elements.notifications = "notifications";
elements.reboot = "reboot";
elements.serverStatus = "server-status";
elements.shutDown = "shut-down";
elements.spinnerImg = "/img/spinner-trans.gif";
elements.storageFree = "storage-free";
elements.storageTotal = "storage-total";
elements.storageUsed = "storage-used";
elements.swapRateAvg = "swap-rate-avg";
elements.swapRateLast = "swap-rate-last";
elements.swapRateMax = "swap-rate-max";
elements.transferMonthly = "transfer-monthly";
elements.transferOverage = "transfer-overage";
elements.transferUsed = "transfer-used";
elements.upgrade = "upgrade";
elements.volumeTable = "volume-table";
// Data recieved from API calls
var data = {};
data.params = {};
data.configs = [];
data.disks = [];
data.events = [];
data.linode = {};
data.linodeTransfer = {};
data.notifications = [];
data.plan = {};
data.stats = {};
data.volumes = [];
// Static references to UI elements
var ui = {};
ui.backups = {};
ui.boot = {};
ui.configTable = {};
ui.cpuAvg = {};
ui.cpuGraph = {};
ui.cpuLast = {};
ui.cpuMax = {};
ui.diskTable = {};
ui.diskUsage = {};
ui.eventTable = {};
ui.graphRange = {};
ui.ioGraph = {};
ui.ioRateAvg = {};
ui.ioRateLast = {};
ui.ioRateMax = {};
ui.ipv4Graph = {};
ui.ipv4InAvg = {};
ui.ipv4InLast = {};
ui.ipv4InMax = {};
ui.ipv4OutAvg = {};
ui.ipv4OutLast = {};
ui.ipv4OutMax = {};
ui.ipv4PrivInAvg = {};
ui.ipv4PrivInLast = {};
ui.ipv4PrivInMax = {};
ui.ipv4PrivOutAvg = {};
ui.ipv4PrivOutLast = {};
ui.ipv4PrivOutMax = {};
ui.ipv4Total = {};
ui.ipv4TotalIn = {};
ui.ipv4TotalOut = {};
ui.ipv6Graph = {};
ui.ipv6InAvg = {};
ui.ipv6InLast = {};
ui.ipv6InMax = {};
ui.ipv6OutAvg = {};
ui.ipv6OutLast = {};
ui.ipv6OutMax = {};
ui.ipv6PrivInAvg = {};
ui.ipv6PrivInLast = {};
ui.ipv6PrivInMax = {};
ui.ipv6PrivOutAvg = {};
ui.ipv6PrivOutLast = {};
ui.ipv6PrivOutMax = {};
ui.ipv6Total = {};
ui.ipv6TotalIn = {};
ui.ipv6TotalOut = {};
ui.jobProgress = {};
ui.jobProgressRow = {};
ui.lastBackup = {};
ui.lastBackupTime = {};
ui.linodeLabel = {};
ui.linodeTag = {};
ui.linodeTagLink = {};
ui.loadingConfigs = {};
ui.loadingDisks = {};
ui.loadingJobs = {};
ui.loadingVolumes = {};
ui.moreJobs = {};
ui.netUsage = {};
ui.notifications = {};
ui.reboot = {};
ui.serverStatus = {};
ui.shutDown = {};
ui.storageFree = {};
ui.storageTotal = {};
ui.storageUsed = {};
ui.swapRateAvg = {};
ui.swapRateLast = {};
ui.swapRateMax = {};
ui.transferMonthly = {};
ui.transferOverage = {};
ui.transferUsed = {};
ui.upgrade = {};
ui.volumeTable = {};
// Temporary state
var state = {};
state.diskRefresh = false;
state.eventsComplete = 0;
state.haveRanges = false;
state.linodeRefresh = false;
state.showExtraEvents = false;
// Button handler for boot button
var bootLinode = function(event)
{
if (!confirm("Boot this Linode?"))
return;
var config = getSelectedConfig();
var request = {};
if (config)
request.config_id = config;
// Reload the page on successful API return
var callback = function()
{
location.reload();
};
apiPost("/linode/instances/" + data.params.lid + "/boot", request, callback);
};
// Generate a config profile table row
var createConfigRow = function(config)
{
var row = document.createElement("tr");
row.className = elements.lmcRow;
// Radio button selector
var radioCell = document.createElement("td");
var radioInput = document.createElement("input");
radioInput.name = elements.configRadioName;
radioInput.className = elements.configRadioName;
radioInput.id = elements.configRadioName + "-" + config.id;
radioInput.type = "radio";
radioCell.appendChild(radioInput);
row.appendChild(radioCell);
// Config profile name and kernel
var nameCell = document.createElement("td");
var nameLink = document.createElement("a");
nameLink.href = "/linodes/config?cid=" + config.id + "&lid=" + data.params.lid;
if (config.label.length > 41)
nameLink.innerHTML = config.label.substring(0, 41) + "...";
else
nameLink.innerHTML = config.label;
nameCell.appendChild(nameLink);
var prefix = document.createElement("span");
prefix.className = elements.info;
prefix.innerHTML = " (";
nameCell.appendChild(prefix);
var kernel = document.createElement("span");
kernel.className = elements.info;
translateKernel(config.kernel, kernel);
nameCell.appendChild(kernel);
var suffix = document.createElement("span");
suffix.className = elements.info;
suffix.innerHTML = ")";
nameCell.appendChild(suffix);
row.appendChild(nameCell);
// Option links
var options = document.createElement("td");
var editLink = document.createElement("a");
editLink.href = "/linodes/config?cid=" + config.id + "&lid=" + data.params.lid;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.href = "#";
removeLink.id = elements.configRemovePrefix + config.id;
removeLink.innerHTML = "Remove";
removeLink.addEventListener("click", deleteConfig);
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Generate a disk table row
var createDiskRow = function(disk)
{
var row = document.createElement("tr");
row.className = elements.lmcRow;
// Disk icon
var iconCell = document.createElement("td");
var diskIcon = document.createElement("img");
diskIcon.className = elements.diskIcon;
diskIcon.src = elements.diskIconImg;
iconCell.appendChild(diskIcon);
row.appendChild(iconCell);
// Disk name and size
var nameCell = document.createElement("td");
var nameLink = document.createElement("a");
nameLink.href = "/linodes/disk?did=" + disk.id + "&lid=" + data.params.lid;
nameLink.innerHTML = disk.label;
var size = document.createElement("span");
size.className = elements.info;
size.innerHTML = " (" + disk.size + " MB, " + disk.filesystem + ")";
nameCell.appendChild(nameLink);
nameCell.appendChild(size);
row.appendChild(nameCell);
// Options links
var options = document.createElement("td");
var editLink = document.createElement("a");
editLink.href = "/linodes/disk?did=" + disk.id + "&lid=" + data.params.lid;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.href = "#";
removeLink.id = elements.diskRemovePrefix + disk.id;
removeLink.innerHTML = "Remove";
removeLink.addEventListener("click", deleteDisk);
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Generate an event table row
var createEventRow = function(event, extra)
{
var row = document.createElement("tr");
row.id = elements.eventRowPrefix + event.id;
row.className = elements.lmcRow;
if (extra)
row.className += " " + elements.extraEvent;
// Status cell
var statusCell = document.createElement("td");
if (event.status.match(/scheduled|started/)) {
statusCell.className = elements.jobRunning;
var spinner = document.createElement("img");
spinner.src = elements.spinnerImg;
statusCell.appendChild(spinner);
} else if (event.status == "failed") {
var failed = document.createElement("span");
failed.className = elements.jobFailed;
failed.innerHTML = "Failed";
statusCell.appendChild(failed);
} else if (event.status == "notification") {
var notice = document.createElement("span");
notice.className = elements.jobNotice;
notice.innerHTML = "Notice";
statusCell.appendChild(notice);
} else if (event.status == "finished") {
var success = document.createElement("span");
success.className = elements.jobSuccess;
success.innerHTML = "Success";
statusCell.appendChild(success);
}
row.appendChild(statusCell);
// Details cell
var detailsCell = document.createElement("td");
var jobType = document.createElement("span");
jobType.className = elements.jobType;
if (eventTitles[event.action])
jobType.innerHTML = eventTitles[event.action];
else
jobType.innerHTML = event.action;
var jobDetails = document.createElement("span");
if (event.secondary_entity)
jobDetails.innerHTML = " - " + event.secondary_entity.label;
var br = document.createElement("br");
var jobInfo = document.createElement("span");
jobInfo.className = elements.jobInfo;
var eventDate = new Date(event.created + "Z");
var curDate = new Date();
jobInfo.innerHTML = "Entered: " + timeString(curDate - eventDate, true);
if (event.duration)
jobInfo.innerHTML += " - Took: " + timeString(event.duration * 1000, false);
else if (event.status != "notification")
jobInfo.innerHTML += " - Waiting...";
detailsCell.appendChild(jobType);
detailsCell.appendChild(jobDetails);
detailsCell.appendChild(br);
detailsCell.appendChild(jobInfo);
row.appendChild(detailsCell);
// Progress cell
var progressCell = document.createElement("td");
if (event.percent_complete && event.percent_complete != 100)
progressCell.innerHTML = event.percent_complete + "%";
if (event.time_remaining) {
if (progressCell.innerHTML.length)
progressCell.innerHTML += ", ";
var times = event.time_remaining.split(":");
var ms = 0;
ms += parseInt(times[0]) * 3600000;
ms += parseInt(times[1]) * 60000;
ms += parseInt(times[2]) * 1000;
progressCell.innerHTML += timeString(ms, false) + " to go";
}
if (event.rate) {
if (progressCell.innerHTML.length)
progressCell.innerHTML += ", ";
progressCell.innerHTML += event.rate;
}
row.appendChild(progressCell);
return row;
};
// Generate a volume table row
var createVolumeRow = function(volume)
{
var row = document.createElement("tr");
row.className = elements.lmcRow;
// Disk icon
var iconCell = document.createElement("td");
var diskIcon = document.createElement("img");
diskIcon.className = elements.diskIcon;
diskIcon.src = elements.diskIconImg;
iconCell.appendChild(diskIcon);
row.appendChild(iconCell);
// Volume name and size
var nameCell = document.createElement("td");
var nameLink = document.createElement("a");
nameLink.href = "/volumes/settings?vid=" + volume.id;
nameLink.innerHTML = volume.label;
var size = document.createElement("span");
size.className = elements.info;
size.innerHTML = " (" + volume.size + " GiB)";
nameCell.appendChild(nameLink);
nameCell.appendChild(size);
row.appendChild(nameCell);
// Options links
var options = document.createElement("td");
var editLink = document.createElement("a");
editLink.href = "/volumes/settings?vid=" + volume.id;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var cloneLink = document.createElement("a");
cloneLink.href = "/volumes/clone?vid=" + volume.id;
cloneLink.innerHTML = "Clone";
var detachLink = document.createElement("a");
detachLink.href = "/volumes/detach?vid=" + volume.id;
detachLink.innerHTML = "Detach";
var removeLink = document.createElement("a");
removeLink.href = "/volumes/remove?vid=" + volume.id;
removeLink.innerHTML = "Remove";
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(cloneLink);
options.appendChild(separator.cloneNode(true));
options.appendChild(detachLink);
options.appendChild(separator.cloneNode(true));
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Handler for config delete
var deleteConfig = function(event)
{
if (!confirm("Delete this Configuration Profile?"))
return;
var cid = event.currentTarget.id.substring(elements.configRemovePrefix.length);
apiDelete("/linode/instances/" + data.params.lid + "/configs/" + cid, function()
{
location.reload();
});
};
// Handler for disk delete
var deleteDisk = function(event)
{
var did = parseInt(event.currentTarget.id.substring(elements.diskRemovePrefix.length));
var disk = null;
for (var i = 0; i < data.disks.length; i++) {
if (data.disks[i].id == did) {
disk = data.disks[i];
break;
}
}
if (!disk)
return;
if (!confirm("Delete " + disk.label + "?"))
return;
apiDelete("/linode/instances/" + data.params.lid + "/disks/" + did, function()
{
location.reload();
});
};
// Callback for config profile API call
var displayConfigs = function(response)
{
// Add configs to array
data.configs = data.configs.concat(response.data);
// Request the next page if there are more
if (response.page != response.pages) {
apiGet("/linode/instances/" + data.params.lid + "/configs?page=" + (response.page + 1), displayConfigs, null);
return;
}
// Remove loading row
ui.loadingConfigs.remove();
// Insert config profile rows into table
for (var i = 0; i < data.configs.length; i++)
ui.configTable.appendChild(createConfigRow(data.configs[i]));
};
// Callback for linode details API call
var displayDetails = function(response)
{
data.linode = response;
// Set page title and header stuff
if (document.title.indexOf("//") == -1)
document.title += " // " + data.linode.label;
ui.linodeLabel.innerHTML = data.linode.label;
if (data.linode.tags.length == 1) {
ui.linodeTagLink.href = "/linodes?tag=" + data.linode.tags[0];
ui.linodeTagLink.innerHTML = "(" + data.linode.tags[0] + ")";
ui.linodeTag.style.display = "inline";
} else {
ui.linodeTag.style.display = "none";
}
// Set running status
ui.serverStatus.innerHTML = data.linode.status.charAt(0).toUpperCase() + data.linode.status.slice(1).replace(/_/g, " ");
if (data.linode.status == "running") {
ui.shutDown.disabled = false;
ui.shutDown.style.display = "block";
} else {
ui.shutDown.style.display = "none";
}
if (data.linode.status.match(/offline|provisioning|migrating|rebuilding|restoring|shutting_down|resizing/)) {
ui.reboot.style.display = "none";
ui.boot.disabled = false;
ui.boot.style.display = "inline";
} else if (data.linode.status != "cloning") {
ui.boot.style.display = "none";
ui.reboot.disabled = false;
ui.reboot.style.display = "inline";
}
// Update storage info
displayStorage();
// Set backup status
if (data.linode.backups.enabled) {
ui.backups.innerHTML = "Enabled!";
ui.backups.className += " " + elements.backupsEnabled;
if (data.linode.backups.last_successful) {
var backupDate = new Date(data.linode.backups.last_successful + "Z");
var now = new Date();
ui.lastBackupTime.innerHTML = timeString(now - backupDate, true);
ui.lastBackup.style.display = "block";
}
} else {
ui.backups.innerHTML = "";
var text = document.createElement("span");
text.innerHTML = "No - ";
var backupLink = document.createElement("a");
backupLink.href = "/linodes/backups_enable?lid=" + data.params.lid;
backupLink.innerHTML = "Enable";
backups.appendChild(text);
backups.appendChild(backupLink);
}
// Get plan info
if (data.linode.type && !data.plan.id)
apiGet("/linode/types/" + data.linode.type, displayPlan, null);
// Populate graph range picker
if (!state.haveRanges) {
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var created = new Date(data.linode.created + "Z");
var now = new Date();
var last30 = document.createElement("option");
last30.value = "/" + now.getFullYear() + "/" + (now.getMonth() + 1).toString().padStart(2, "0");
last30.innerHTML = "Last 30 Days";
ui.graphRange.appendChild(last30);
while (!(now.getFullYear() == created.getFullYear() && now.getMonth() == created.getMonth())) {
if (now.getMonth() == 0) {
now.setMonth(11);
now.setFullYear(now.getFullYear() - 1);
} else {
now.setMonth(now.getMonth() - 1);
}
var yearMonth = document.createElement("option");
yearMonth.value = "/" + now.getFullYear() + "/" + (now.getMonth() + 1).toString().padStart(2, "0");
yearMonth.innerHTML = months[now.getMonth()] + " " + now.getFullYear();
ui.graphRange.appendChild(yearMonth);
}
state.haveRanges = true;
}
state.linodeRefresh = false;
};
// Callback for disks API call
var displayDisks = function(response)
{
// Add disks to array
data.disks = data.disks.concat(response.data);
// Request the next page if there are more
if (response.page != response.pages) {
apiGet("/linode/instances/" + data.params.lid + "/disks?page=" + (response.page + 1), displayDisks, null);
return;
}
// Remove loading row
ui.loadingDisks.remove();
// Update storage info
displayStorage();
// Insert disk rows into table
ui.diskTable.innerHTML = "";
for (var i = 0; i < data.disks.length; i++)
ui.diskTable.appendChild(createDiskRow(data.disks[i]));
state.diskRefresh = false;
};
// Callback for events API call
var displayEvents = function(response)
{
// Only collect a max of 8 finished jobs
for (var i = 0; i < response.data.length && state.eventsComplete < 8; i++) {
if (response.data[i].status.match(/failed|finished|notification/))
state.eventsComplete++;
data.events.push(response.data[i]);
}
// Request the next page if there are more
if (state.eventsComplete < 8 && response.page != response.pages) {
var filter = {
"entity.type": "linode",
"entity.id": parseInt(data.params.lid)
};
apiGet("/account/events?page=" + (response.page + 1), displayEvents, filter);
return;
}
// Remove loading row
ui.loadingJobs.remove();
// Insert events into table and compute total progress
var progress = 0;
var totalProgress = 0;
for (var i = 0; i < data.events.length; i++) {
var extra = (data.events[i].status.match(/failed|finished|notification/) && i >= 4);
ui.eventTable.appendChild(createEventRow(data.events[i], extra));
if (!data.events[i].status.match(/failed|finished|notification/)) {
totalProgress += 100;
progress += data.events[i].percent_complete;
// Set refresh timer for event
window.setTimeout(getEvent, settings.refreshRate, data.events[i].id);
}
}
// If there are ongoing events, show the progress bar
if (totalProgress) {
ui.jobProgress.max = totalProgress;
ui.jobProgress.value = progress;
ui.jobProgress.innerHTML = (progress / totalProgress * 100).toFixed(0) + "%";
ui.jobProgress.title = ui.jobProgress.innerHTML;
ui.jobProgressRow.style.display = "table-row";
}
};
// Callback for notifications API call
var displayNotifications = function(response)
{
// Add notifications to array
data.notifications = data.notifications.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/account/notifications?page=" + (response.page + 1), displayNotifications, null);
return;
}
// Display notifications
for (var i = 0; i < data.notifications.length; i++) {
if (!data.notifications[i].entity || data.notifications[i].entity.type != "linode" || data.notifications[i].entity.id != data.params.lid)
continue;
if (data.notifications[i].type == "maintenance") {
var now = new Date();
var maintStart = new Date(data.notifications[i].when + "Z");
data.notifications[i].label = "Maintenance Scheduled";
data.notifications[i].message = "This Linode's physical host will be undergoing maintenance on " + maintStart.toLocaleString() + " (in " + timeString(maintStart - now, false) + ").";
data.notifications[i].message += " During this time, your Linode will be shut down and remain offline, then returned to its last state (running or powered off).";
data.notifications[i].message += " For more information, please see your open support tickets.";
}
if (data.notifications[i].type == "migration_scheduled") {
var now = new Date();
var migrateStart = new Date(data.notifications[i].when + "Z");
data.notifications[i].message += " If no action is taken, the migration will start automatically on " + migrateStart.toLocaleString() + " (in " + timeString(migrateStart - now, false) + ").";
}
var notification = document.createElement("div");
notification.className = elements.notification;
var header = document.createElement("h1");
header.innerHTML = data.notifications[i].label;
notification.appendChild(header);
if (data.notifications[i].type == "migration_pending" || data.notifications[i].type == "migration_scheduled") {
var migrateButton = document.createElement("button");
migrateButton.type = "button";
migrateButton.innerHTML = "Migrate " + data.notifications[i].entity.label + " NOW";
migrateButton.addEventListener("click", initiateMigration);
notification.appendChild(migrateButton);
}
var body = document.createElement("p");
body.innerHTML = data.notifications[i].message;
notification.appendChild(body);
ui.notifications.appendChild(notification);
}
};
// Callback for linode plan API call
var displayPlan = function(response)
{
data.plan = response;
// Display the upgrade banner if one is available
if (data.plan.successor)
ui.upgrade.style.display = "block";
};
// Show stats graphs
var displayStats = function(response)
{
// Insert dummy points in case of blank data
if (!response.data.cpu.length)
response.data.cpu = [[0,0]];
if (!response.data.io.io.length)
response.data.io.io = [[0,0]];
if (!response.data.io.swap.length)
response.data.io.swap = [[0,0]];
if (!response.data.netv4.private_out.length)
response.data.netv4.private_out = [[0,0]];
if (!response.data.netv4.private_in.length)
response.data.netv4.private_in = [[0,0]];
if (!response.data.netv4.out.length)
response.data.netv4.out = [[0,0]];
if (!response.data.netv4.in.length)
response.data.netv4.in = [[0,0]];
if (!response.data.netv6.private_out.length)
response.data.netv6.private_out = [[0,0]];
if (!response.data.netv6.private_in.length)
response.data.netv6.private_in = [[0,0]];
if (!response.data.netv6.out.length)
response.data.netv6.out = [[0,0]];
if (!response.data.netv6.in.length)
response.data.netv6.in = [[0,0]];
data.stats.cpu = [{
"color": "#03C",
"fill": false,
"points": response.data.cpu
}];
data.stats.io = [
{
"color": "#FFD04B",
"fill": true,
"points": response.data.io.io
},
{
"color": "#FA373E",
"fill": false,
"points": response.data.io.swap
}
];
data.stats.netv4 = [
{
"color": "#FF9",
"fill": true,
"points": response.data.netv4.private_out
},
{
"color": "#C09",
"fill": false,
"points": response.data.netv4.private_in
},
{
"color": "#32CD32",
"fill": true,
"points": response.data.netv4.out
},
{
"color": "#03C",
"fill": false,
"points": response.data.netv4.in
}
];
data.stats.netv6 = [
{
"color": "#FF9",
"fill": true,
"points": response.data.netv6.private_out
},
{
"color": "#C09",
"fill": false,
"points": response.data.netv6.private_in
},
{
"color": "#32CD32",
"fill": true,
"points": response.data.netv6.out
},
{
"color": "#03C",
"fill": false,
"points": response.data.netv6.in
}
];
// Draw graphs
drawSeries(data.stats.cpu, ui.cpuGraph);
drawSeries(data.stats.io, ui.ioGraph);
drawSeries(data.stats.netv4, ui.ipv4Graph);
drawSeries(data.stats.netv6, ui.ipv6Graph);
// Compute traffic totals
var ipv4Out = data.stats.netv4[0].avg * (data.stats.netv4[0].points[data.stats.netv4[0].points.length-1][0] - data.stats.netv4[0].points[0][0]) / 1000;
ipv4Out += data.stats.netv4[2].avg * (data.stats.netv4[2].points[data.stats.netv4[2].points.length-1][0] - data.stats.netv4[2].points[0][0]) / 1000;
ipv4Out /= 8;
var ipv4In = data.stats.netv4[1].avg * (data.stats.netv4[1].points[data.stats.netv4[1].points.length-1][0] - data.stats.netv4[1].points[0][0]) / 1000;
ipv4In += data.stats.netv4[3].avg * (data.stats.netv4[3].points[data.stats.netv4[3].points.length-1][0] - data.stats.netv4[3].points[0][0]) / 1000;
ipv4In /= 8;
var ipv6Out = data.stats.netv6[0].avg * (data.stats.netv6[0].points[data.stats.netv6[0].points.length-1][0] - data.stats.netv6[0].points[0][0]) / 1000;
ipv6Out += data.stats.netv6[2].avg * (data.stats.netv6[2].points[data.stats.netv6[2].points.length-1][0] - data.stats.netv6[2].points[0][0]) / 1000;
ipv6Out /= 8;
var ipv6In = data.stats.netv6[1].avg * (data.stats.netv6[1].points[data.stats.netv6[1].points.length-1][0] - data.stats.netv6[1].points[0][0]) / 1000;
ipv6In += data.stats.netv6[3].avg * (data.stats.netv6[3].points[data.stats.netv6[3].points.length-1][0] - data.stats.netv6[3].points[0][0]) / 1000;
ipv6In /= 8;
// Update tables
ui.cpuMax.innerHTML = data.stats.cpu[0].max + "%";
ui.cpuAvg.innerHTML = data.stats.cpu[0].avg.toFixed(2) + "%";
ui.cpuLast.innerHTML = data.stats.cpu[0].points[data.stats.cpu[0].points.length - 1][1] + "%";
ui.ioRateMax.innerHTML = data.stats.io[0].max;
ui.ioRateAvg.innerHTML = data.stats.io[0].avg.toFixed(2);
ui.ioRateLast.innerHTML = data.stats.io[0].points[data.stats.io[0].points.length - 1][1];
ui.swapRateMax.innerHTML = data.stats.io[1].max;
ui.swapRateAvg.innerHTML = data.stats.io[1].avg.toFixed(2);
ui.swapRateLast.innerHTML = data.stats.io[1].points[data.stats.io[1].points.length - 1][1];
ui.ipv4PrivOutMax.innerHTML = countSI(data.stats.netv4[0].max) + "b/s";
ui.ipv4PrivOutAvg.innerHTML = countSI(data.stats.netv4[0].avg) + "b/s";
ui.ipv4PrivOutLast.innerHTML = countSI(data.stats.netv4[0].points[data.stats.netv4[0].points.length - 1][1]) + "b/s";
ui.ipv4PrivInMax.innerHTML = countSI(data.stats.netv4[1].max) + "b/s";
ui.ipv4PrivInAvg.innerHTML = countSI(data.stats.netv4[1].avg) + "b/s";
ui.ipv4PrivInLast.innerHTML = countSI(data.stats.netv4[1].points[data.stats.netv4[1].points.length - 1][1]) + "b/s";
ui.ipv4OutMax.innerHTML = countSI(data.stats.netv4[2].max) + "b/s";
ui.ipv4OutAvg.innerHTML = countSI(data.stats.netv4[2].avg) + "b/s";
ui.ipv4OutLast.innerHTML = countSI(data.stats.netv4[2].points[data.stats.netv4[2].points.length - 1][1]) + "b/s";
ui.ipv4InMax.innerHTML = countSI(data.stats.netv4[3].max) + "b/s";
ui.ipv4InAvg.innerHTML = countSI(data.stats.netv4[3].avg) + "b/s";
ui.ipv4InLast.innerHTML = countSI(data.stats.netv4[3].points[data.stats.netv4[3].points.length - 1][1]) + "b/s";
ui.ipv4TotalIn.innerHTML = countSI(ipv4In) + "B";
ui.ipv4TotalOut.innerHTML = countSI(ipv4Out) + "B";
ui.ipv4Total.innerHTML = countSI(ipv4In + ipv4Out) + "B";
ui.ipv6PrivOutMax.innerHTML = countSI(data.stats.netv6[0].max) + "b/s";
ui.ipv6PrivOutAvg.innerHTML = countSI(data.stats.netv6[0].avg) + "b/s";
ui.ipv6PrivOutLast.innerHTML = countSI(data.stats.netv6[0].points[data.stats.netv6[0].points.length - 1][1]) + "b/s";
ui.ipv6PrivInMax.innerHTML = countSI(data.stats.netv6[1].max) + "b/s";
ui.ipv6PrivInAvg.innerHTML = countSI(data.stats.netv6[1].avg) + "b/s";
ui.ipv6PrivInLast.innerHTML = countSI(data.stats.netv6[1].points[data.stats.netv6[1].points.length - 1][1]) + "b/s";
ui.ipv6OutMax.innerHTML = countSI(data.stats.netv6[2].max) + "b/s";
ui.ipv6OutAvg.innerHTML = countSI(data.stats.netv6[2].avg) + "b/s";
ui.ipv6OutLast.innerHTML = countSI(data.stats.netv6[2].points[data.stats.netv6[2].points.length - 1][1]) + "b/s";
ui.ipv6InMax.innerHTML = countSI(data.stats.netv6[3].max) + "b/s";
ui.ipv6InAvg.innerHTML = countSI(data.stats.netv6[3].avg) + "b/s";
ui.ipv6InLast.innerHTML = countSI(data.stats.netv6[3].points[data.stats.netv6[3].points.length - 1][1]) + "b/s";
ui.ipv6TotalIn.innerHTML = countSI(ipv6In) + "B";
ui.ipv6TotalOut.innerHTML = countSI(ipv6Out) + "B";
ui.ipv6Total.innerHTML = countSI(ipv6In + ipv6Out) + "B";
ui.graphRange.disabled = false;
};
// Show storage totals
var displayStorage = function()
{
if (!data.linode.specs)
return;
ui.storageTotal.innerHTML = data.linode.specs.disk + " MB";
var storageUsed = 0;
for (var i = 0; i < data.disks.length; i++)
storageUsed += data.disks[i].size;
ui.storageUsed.innerHTML = storageUsed + " MB";
ui.storageFree.innerHTML = (data.linode.specs.disk - storageUsed) + " MB";
ui.diskUsage.value = (storageUsed / data.linode.specs.disk * 100).toFixed(0);
ui.diskUsage.innerHTML = ui.diskUsage.value + "%";
ui.diskUsage.title = ui.diskUsage.innerHTML;
};
// Callback for network transfer API call
var displayTransfer = function(response)
{
data.linodeTransfer = response;
// Display network transfer info
ui.transferMonthly.innerHTML = data.linodeTransfer.quota + " GB";
ui.transferUsed.innerHTML = countSI(data.linodeTransfer.used) + "B";
ui.transferOverage.innerHTML = data.linodeTransfer.billable + " GB";
ui.netUsage.value = ((data.linodeTransfer.used / 1024 / 1024 / 1024) / data.linodeTransfer.quota * 100).toFixed(0);
ui.netUsage.innerHTML = ui.netUsage.value + "%";
ui.netUsage.title = ui.netUsage.innerHTML;
};
// Callback for volumes API call
var displayVolumes = function(response)
{
// Add volumes to array
data.volumes = data.volumes.concat(response.data);
// Request the next page if there are more
if (response.page != response.pages) {
apiGet("/linode/instances/" + data.params.lid + "/volumes?page=" + (response.page + 1), displayVolumes, null);
return;
}
// Remove loading row
ui.loadingVolumes.remove();
// Insert volume rows into table
for (var i = 0; i < data.volumes.length; i++)
ui.volumeTable.appendChild(createVolumeRow(data.volumes[i]));
};
// Retrieve the given event from the API
var getEvent = function(eventID)
{
apiGet("/account/events/" + eventID, updateEvent, null);
};
// Get selected config profile
var getSelectedConfig = function()
{
var configs = document.getElementsByClassName(elements.configRadioName);
for (var i = 0; i < configs.length; i++) {
if (configs[i].checked)
return parseInt(configs[i].id.replace(elements.configRadioName + "-", ""));
}
return 0;
};
// Button handler to initiate migration
var initiateMigration = function(event)
{
if (event.currentTarget.disabled)
return;
var req = {
"upgrade": false
};
apiPost("/linode/instances/" + data.params.lid + "/migrate", req, function(response)
{
location.reload();
});
};
// Button handler for reboot button
var rebootLinode = function(event)
{
if (!confirm("Are you sure you want to issue a Reboot?"))
return;
var config = getSelectedConfig();
var request = {};
if (config)
request.config_id = config;
// Reload the page on successful API return
var callback = function()
{
location.reload();
};
apiPost("/linode/instances/" + data.params.lid + "/reboot", request, callback);
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need a Linode ID, so die if we don't have it
if (!data.params.lid) {
alert("No Linode ID supplied!");
return;
}
setupHeader();
// Update links on page to include proper Linode ID
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
anchors[i].href = anchors[i].href.replace("lid=0", "lid=" + data.params.lid);
// Get element references
for (var i in ui)
ui[i] = document.getElementById(elements[i]);
// Attach button handlers
ui.boot.addEventListener("click", bootLinode);
ui.moreJobs.addEventListener("click", toggleEvents);
ui.reboot.addEventListener("click", rebootLinode);
ui.shutDown.addEventListener("click", shutDownLinode);
ui.graphRange.addEventListener("input", updateGraphs);
// Set graph resolutions
ui.cpuGraph.height = ui.cpuGraph.clientHeight;
ui.cpuGraph.width = ui.cpuGraph.clientWidth;
ui.ioGraph.height = ui.ioGraph.clientHeight;
ui.ioGraph.width = ui.ioGraph.clientWidth;
ui.ipv4Graph.height = ui.ipv4Graph.clientHeight;
ui.ipv4Graph.width = ui.ipv4Graph.clientWidth;
ui.ipv6Graph.height = ui.ipv6Graph.clientHeight;
ui.ipv6Graph.width = ui.ipv6Graph.clientWidth;
// Get data from the API
apiGet("/linode/instances/" + data.params.lid, displayDetails, null);
apiGet("/linode/instances/" + data.params.lid + "/configs", displayConfigs, null);
apiGet("/linode/instances/" + data.params.lid + "/disks", displayDisks, null);
apiGet("/linode/instances/" + data.params.lid + "/volumes", displayVolumes, null);
apiGet("/linode/instances/" + data.params.lid + "/transfer", displayTransfer, null);
var filter = {
"entity.type": "linode",
"entity.id": parseInt(data.params.lid)
};
apiGet("/account/events", displayEvents, filter);
apiGet("/account/notifications", displayNotifications, null);
apiGet("/linode/instances/" + data.params.lid + "/stats", displayStats, null);
};
// Button handler for shutdown button
var shutDownLinode = function(event)
{
if (!confirm("Are you sure you want to issue a Shutdown?"))
return;
// Reload the page on successful API return
var callback = function()
{
location.reload();
};
apiPost("/linode/instances/" + data.params.lid + "/shutdown", {}, callback);
};
// Toggle visibility of "extra" events
var toggleEvents = function(event)
{
// Flip
state.showExtraEvents = !state.showExtraEvents;
var display = "none";
if (state.showExtraEvents)
display = "table-row";
// Get extra events and show/hide them
var extras = document.getElementsByClassName(elements.extraEvent);
for (var i = 0; i < extras.length; i++)
extras[i].style.display = display;
};
// Update an existing event
var updateEvent = function(event)
{
// Update the local copy of the event
var progress = 0;
for (var i = 0; i < data.events.length; i++) {
if (data.events[i].id == event.id) {
progress = event.percent_complete - data.events[i].percent_complete;
// Refresh details if event status changed
if (event.status != data.events[i].status && !state.linodeRefresh && !state.diskRefresh) {
state.linodeRefresh = true;
state.diskRefresh = true;
data.linode = {};
data.disks = [];
apiGet("/linode/instances/" + data.params.lid, displayDetails, null);
apiGet("/linode/instances/" + data.params.lid + "/disks", displayDisks, null);
}
data.events[i] = event;
continue;
}
}
// Generate a new event row and replace the existing one
var eventRow = document.getElementById(elements.eventRowPrefix + event.id);
var extra = (eventRow.className.indexOf(elements.extraEvent) != -1);
eventRow.replaceWith(createEventRow(event, extra));
// Update progress bar
ui.jobProgress.value += progress;
ui.jobProgress.innerHTML = (ui.jobProgress.value / ui.jobProgress.max * 100).toFixed(0) + "%";
ui.jobProgress.title = ui.jobProgress.innerHTML;
if (ui.jobProgress.value == ui.jobProgress.max)
ui.jobProgressRow.style.display = "none";
// Refresh again if not complete
if (!event.status.match(/failed|finished|notification/))
window.setTimeout(getEvent, settings.refreshRate, event.id);
};
// Re-populate graphs with selected range
var updateGraphs = function(event)
{
if (event.currentTarget.disabled)
return;
apiGet("/linode/instances/" + data.params.lid + "/stats" + ui.graphRange.value, displayStats, null);
ui.graphRange.disabled = true;
};
// Attach onload handler
window.addEventListener("load", setup);
})();