Merge pull request 'Implement Graphs' (#8) from graphs into master

Reviewed-on: https://git.bradleylaboon.com/lb.laboon/lmc/pulls/8
This commit is contained in:
L. Bradley LaBoon 2023-04-03 20:19:33 -04:00
commit ac999c9704
10 changed files with 569 additions and 181 deletions

View File

@ -1,4 +1,4 @@
Full disclosure: I am an employee of Linode, however this project is being developed independently in my own free time and is not associated in any way with Linode, LLC. Only publicly-available documentation and information is being used in the development of this project.
Full disclosure: I am an employee of Linode/Akamai, however this project is being developed independently in my own free time and is not associated in any way with Linode, LLC or Akamai Technologies, Inc. Only publicly-available documentation and information is being used in the development of this project.
# Linode Manager Classic
LMC is a modern recreation of Linode's original manager interface that many people know and love. It is implemented as a client-side browser app using just vanilla HTML5, CSS3, and JavaScript (no 3rd-party libraries or other external dependencies). It uses Linode's OAuth provider for authentication and interfaces with APIv4.
@ -18,8 +18,8 @@ This project is currently a work in progress. The following list provides a high
- [x] DNS
- [x] Account Details (excluding PayPal support)
- [x] User Profile Settings
- [x] Graphs
- [ ] PayPal payments
- [ ] Graphs
- [ ] StackScripts
- [ ] NodeBalancers
- [ ] Longview
@ -34,4 +34,4 @@ Before reporting an issue, please search the issue tracker to see if the issue h
The best way to contribute is by opening issues on the issue tracker. For simple fixes or adjustments I might accept a PR, but for the time being I would prefer to maintain this project as a solo effort. I may change my mind in the future.
## Self-hosting
In order to self-host this application, you must first create your own OAuth client using your existing Linode account (instructions for doing this in Linode's new manager can be found [here](https://github.com/linode/manager/blob/develop/CREATE_CLIENT.md)). When creating the OAuth client, use the URL where you will be hosting the app as the Callback URL. Then you can clone this repository into your webroot and copy the `clientID.js.example` file to `clientID.js` and fill in the empty string with your Client ID. You must ensure that Server Side Includes (SSI) is enabled in your web server. Optionally, you can configure your web server to use the `404.html` file as a custom error document.
In order to self-host this application, you must first create your own OAuth client using your existing Linode account. When creating the OAuth client, use the URL where you will be hosting the app as the Callback URL. Then you can clone this repository into your webroot and copy the [clientID.js.example](clientID.js.example) file to `clientID.js` and fill in the empty string with your Client ID. You must ensure that Server Side Includes (SSI) is enabled in your web server. Optionally, you can configure your web server to use the [404.html](404.html) file as a custom error document.

View File

@ -77,6 +77,24 @@ header {
font-weight: bold;
}
.lmc-graph canvas {
width: 100%;
}
.lmc-graph h4 {
text-align: center;
}
.lmc-graph table {
width: 100%;
}
.lmc-graph-color {
height: 15px;
display: inline-block;
width: 15px;
}
.lmc-table {
background-color: #CECECE;
width: 100%;

View File

@ -452,6 +452,63 @@ function displayUser(response)
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)
{
@ -793,4 +850,4 @@ function translateKernel(slug, element)
apiGet("/linode/kernels/" + slug, callback, null);
}
export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel };
export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, drawSeries, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel };

View File

@ -6,7 +6,6 @@
<a class="subnav-link" href="/linodes/rescue?lid=0">Rescue</a>
<a class="subnav-link" href="/linodes/resize?lid=0">Resize</a>
<a class="subnav-link" href="/linodes/clone?lid=0">Clone</a>
<a class="subnav-link" href="/linodes/graphs?lid=0">Graphs</a>
<a class="subnav-link" href="/linodes/backups?lid=0">Backups</a>
<a class="subnav-link" href="/linodes/settings?lid=0">Settings</a>
</nav>

View File

@ -25,10 +25,18 @@
margin-top: 5px;
}
canvas {
height: 250px;
}
#config-table tr:last-of-type {
border-bottom: 1px solid #E8E8E8;
}
#cpu-color {
background-color: #03C;
}
.disk-icon {
height: 24px;
width: 26px;
@ -42,12 +50,51 @@
display: none;
}
#graph-range {
font-size: 18px;
}
h3 {
border-bottom: 1px solid #E8E8E8;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 18px;
font-weight: lighter;
margin-bottom: 50px;
}
#io-rate-color {
background-color: #FFD04B;
}
#ipv4-in-color {
background-color: #03C;
}
#ipv4-out-color {
background-color: #32CD32;
}
#ipv4-privin-color {
background-color: #C09;
}
#ipv4-privout-color {
background-color: #FF9;
}
#ipv6-in-color {
background-color: #03C;
}
#ipv6-out-color {
background-color: #32CD32;
}
#ipv6-privin-color {
background-color: #C09;
}
#ipv6-privout-color {
background-color: #FF9;
}
.job-failed {
@ -202,6 +249,10 @@ h3 {
float: right;
}
#swap-rate-color {
background-color: #FA373E;
}
#upgrade {
background-color: #F0F0F0;
display: none;

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParams, setupHeader, timeString, translateKernel } from "/global.js";
import { settings, elements, apiDelete, apiGet, apiPost, drawSeries, eventTitles, parseParams, setupHeader, timeString, translateKernel } from "/global.js";
(function()
{
@ -26,6 +26,10 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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-";
@ -34,7 +38,44 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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";
@ -64,6 +105,9 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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";
@ -80,6 +124,7 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
data.linodeTransfer = {};
data.notifications = [];
data.plan = {};
data.stats = {};
data.volumes = [];
// Static references to UI elements
@ -87,9 +132,50 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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 = {};
@ -110,6 +196,9 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
ui.storageFree = {};
ui.storageTotal = {};
ui.storageUsed = {};
ui.swapRateAvg = {};
ui.swapRateLast = {};
ui.swapRateMax = {};
ui.transferMonthly = {};
ui.transferOverage = {};
ui.transferUsed = {};
@ -120,6 +209,7 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
var state = {};
state.diskRefresh = false;
state.eventsComplete = 0;
state.haveRanges = false;
state.linodeRefresh = false;
state.showExtraEvents = false;
@ -143,8 +233,8 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
apiPost("/linode/instances/" + data.params.lid + "/boot", request, callback);
};
// Convert a byte count into a "friendly" byte string (KB, MB, GB, etc)
var byteString = function(count)
// Convert an unqualified count into a string with an SI prefix (i.e. bytes to MB/GB/etc)
var countSI = function(count)
{
var prefix = "KMGTPEZY";
var unit = "";
@ -158,7 +248,7 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
}
}
return count.toFixed(2) + " " + unit + "B";
return count.toFixed(2) + " " + unit;
};
// Generate a config profile table row
@ -525,6 +615,33 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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;
};
@ -662,6 +779,159 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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()
{
@ -686,7 +956,7 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
// Display network transfer info
ui.transferMonthly.innerHTML = data.linodeTransfer.quota + " GB";
ui.transferUsed.innerHTML = byteString(data.linodeTransfer.used);
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 + "%";
@ -786,43 +1056,25 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
anchors[i].href = anchors[i].href.replace("lid=0", "lid=" + data.params.lid);
// Get element references
ui.backups = document.getElementById(elements.backups);
ui.boot = document.getElementById(elements.boot);
ui.configTable = document.getElementById(elements.configTable);
ui.diskTable = document.getElementById(elements.diskTable);
ui.diskUsage = document.getElementById(elements.diskUsage);
ui.eventTable = document.getElementById(elements.eventTable);
ui.jobProgress = document.getElementById(elements.jobProgress);
ui.jobProgressRow = document.getElementById(elements.jobProgressRow);
ui.lastBackup = document.getElementById(elements.lastBackup);
ui.lastBackupTime = document.getElementById(elements.lastBackupTime);
ui.linodeLabel = document.getElementById(elements.linodeLabel);
ui.linodeTag = document.getElementById(elements.linodeTag);
ui.linodeTagLink = document.getElementById(elements.linodeTagLink);
ui.loadingConfigs = document.getElementById(elements.loadingConfigs);
ui.loadingDisks = document.getElementById(elements.loadingDisks);
ui.loadingJobs = document.getElementById(elements.loadingJobs);
ui.loadingVolumes = document.getElementById(elements.loadingVolumes);
ui.moreJobs = document.getElementById(elements.moreJobs);
ui.netUsage = document.getElementById(elements.netUsage);
ui.notifications = document.getElementById(elements.notifications);
ui.reboot = document.getElementById(elements.reboot);
ui.serverStatus = document.getElementById(elements.serverStatus);
ui.shutDown = document.getElementById(elements.shutDown);
ui.storageFree = document.getElementById(elements.storageFree);
ui.storageTotal = document.getElementById(elements.storageTotal);
ui.storageUsed = document.getElementById(elements.storageUsed);
ui.transferMonthly = document.getElementById(elements.transferMonthly);
ui.transferOverage = document.getElementById(elements.transferOverage);
ui.transferUsed = document.getElementById(elements.transferUsed);
ui.upgrade = document.getElementById(elements.upgrade);
ui.volumeTable = document.getElementById(elements.volumeTable);
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);
@ -836,6 +1088,7 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
};
apiGet("/account/events", displayEvents, filter);
apiGet("/account/notifications", displayNotifications, null);
apiGet("/linode/instances/" + data.params.lid + "/stats", displayStats, null);
};
// Button handler for shutdown button
@ -908,6 +1161,16 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam
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);
})();

View File

@ -109,6 +109,145 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
</tbody>
</table>
<h3>Graphs</h3>
<select disabled id="graph-range">
<option selected value="">Last 24 Hours</option>
</select>
<div class="lmc-graph">
<h4>CPU (%)</h4>
<canvas id="cpu-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="cpu-color" class="lmc-graph-color"></div> CPU %</td>
<td id="cpu-max"></td>
<td id="cpu-avg"></td>
<td id="cpu-last"></td>
</tr>
</tbody>
</table>
<h4>Disk I/O (blocks/s)</h4>
<canvas id="io-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="io-rate-color" class="lmc-graph-color"></div> I/O Rate</td>
<td id="io-rate-max"></td>
<td id="io-rate-avg"></td>
<td id="io-rate-last"></td>
</tr>
<tr>
<td><div id="swap-rate-color" class="lmc-graph-color"></div> Swap Rate</td>
<td id="swap-rate-max"></td>
<td id="swap-rate-avg"></td>
<td id="swap-rate-last"></td>
</tr>
</tbody>
</table>
<h4>Network - IPv4 (bits/s)</h4>
<canvas id="ipv4-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="ipv4-privout-color" class="lmc-graph-color"></div> Private Out</td>
<td id="ipv4-privout-max"></td>
<td id="ipv4-privout-avg"></td>
<td id="ipv4-privout-last"></td>
</tr>
<tr>
<td><div id="ipv4-privin-color" class="lmc-graph-color"></div> Private In</td>
<td id="ipv4-privin-max"></td>
<td id="ipv4-privin-avg"></td>
<td id="ipv4-privin-last"></td>
</tr>
<tr>
<td><div id="ipv4-out-color" class="lmc-graph-color"></div> Public Out</td>
<td id="ipv4-out-max"></td>
<td id="ipv4-out-avg"</td>
<td id="ipv4-out-last"></td>
</tr>
<tr>
<td><div id="ipv4-in-color" class="lmc-graph-color"></div> Public In</td>
<td id="ipv4-in-max"></td>
<td id="ipv4-in-avg"></td>
<td id="ipv4-in-last"></td>
</tr>
<tr>
<td>Total Traffic</td>
<td>In: <span id="ipv4-total-in"></span></td>
<td>Out: <span id="ipv4-total-out"></span></td>
<td>Combined: <span id="ipv4-total"></span></td>
</tr>
</tbody>
</table>
<h4>Network - IPv6 (bits/s)</h4>
<canvas id="ipv6-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="ipv6-privout-color" class="lmc-graph-color"></div> Private Out</td>
<td id="ipv6-privout-max"></td>
<td id="ipv6-privout-avg"></td>
<td id="ipv6-privout-last"></td>
</tr>
<tr>
<td><div id="ipv6-privin-color" class="lmc-graph-color"></div> Private In</td>
<td id="ipv6-privin-max"></td>
<td id="ipv6-privin-avg"></td>
<td id="ipv6-privin-last"></td>
</tr>
<tr>
<td><div id="ipv6-out-color" class="lmc-graph-color"></div> Public Out</td>
<td id="ipv6-out-max"></td>
<td id="ipv6-out-avg"</td>
<td id="ipv6-out-last"></td>
</tr>
<tr>
<td><div id="ipv6-in-color" class="lmc-graph-color"></div> Public In</td>
<td id="ipv6-in-max"></td>
<td id="ipv6-in-avg"></td>
<td id="ipv6-in-last"></td>
</tr>
<tr>
<td>Total Traffic</td>
<td>In: <span id="ipv6-total-in"></span></td>
<td>Out: <span id="ipv6-total-out"></span></td>
<td>Combined: <span id="ipv6-total"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="linode-sidebar">
<div class="sidebar-box">

View File

@ -1,22 +0,0 @@
/*
* 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 url('/global.css');
#graphs {
padding: 0px 15px 15px;
}

View File

@ -1,82 +0,0 @@
/*
* 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, apiGet, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.linodeLabel = "linode-label";
elements.linodeTag = "linode-tag";
elements.linodeTagLink = "linode-tag-link";
// Data recieved from API calls
var data = {};
data.linode = {};
// Static references to UI elements
var ui = {};
ui.linodeLabel = {};
ui.linodeTag = {};
ui.linodeTagLink = {};
// Callback for linode details API call
var displayDetails = function(response)
{
data.linode = response;
// Set page title and header stuff
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";
}
};
// 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
ui.linodeLabel = document.getElementById(elements.linodeLabel);
ui.linodeTag = document.getElementById(elements.linodeTag);
ui.linodeTagLink = document.getElementById(elements.linodeTagLink);
// Get data from API
apiGet("/linode/instances/" + data.params.lid, displayDetails, null);
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -1,35 +0,0 @@
<!--
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/>.
-->
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>LMC - Graphs</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="graphs.css" />
<script src="graphs.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<!--#include virtual="/include/linode_subnav.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/linodes">Linodes</a> » <span id="linode-tag"><a id="linode-tag-link" href=""></a> » </span><a id="linode-label" href="/linodes/dashboard?lid=0"></a> » <span class="top-links-title">Graphs</span></div>
<div id="graphs">
</div>
</div>
</body>
</html>