diff --git a/README.md b/README.md index c86c108..40e2686 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/global.css b/global.css index 72837d3..e1a9e70 100644 --- a/global.css +++ b/global.css @@ -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%; diff --git a/global.js b/global.js index 378a3aa..d895aec 100644 --- a/global.js +++ b/global.js @@ -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 }; diff --git a/include/linode_subnav.html b/include/linode_subnav.html index ab72d4f..8f41a98 100644 --- a/include/linode_subnav.html +++ b/include/linode_subnav.html @@ -6,7 +6,6 @@ Rescue Resize Clone - Graphs Backups Settings diff --git a/linodes/dashboard/dashboard.css b/linodes/dashboard/dashboard.css index 8655485..01af940 100644 --- a/linodes/dashboard/dashboard.css +++ b/linodes/dashboard/dashboard.css @@ -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; diff --git a/linodes/dashboard/dashboard.js b/linodes/dashboard/dashboard.js index 7655746..806ae56 100644 --- a/linodes/dashboard/dashboard.js +++ b/linodes/dashboard/dashboard.js @@ -15,7 +15,7 @@ * along with Linode Manager Classic. If not, see . */ -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); })(); diff --git a/linodes/dashboard/index.shtml b/linodes/dashboard/index.shtml index 073a3fd..7caf205 100644 --- a/linodes/dashboard/index.shtml +++ b/linodes/dashboard/index.shtml @@ -109,6 +109,145 @@ along with Linode Manager Classic. If not, see .

Graphs

+ +
+

CPU (%)

+ + + + + + + + + + + + + + + + + + +
MaxAvgLast
CPU %
+

Disk I/O (blocks/s)

+ + + + + + + + + + + + + + + + + + + + + + + + +
MaxAvgLast
I/O Rate
Swap Rate
+

Network - IPv4 (bits/s)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MaxAvgLast
Private Out
Private In
Public Out
+
Public In
Total TrafficIn: Out: Combined:
+

Network - IPv6 (bits/s)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MaxAvgLast
Private Out
Private In
Public Out
+
Public In
Total TrafficIn: Out: Combined:
+