Compare commits

..

14 Commits

55 changed files with 3142 additions and 309 deletions

View File

@ -28,7 +28,7 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<h2>We couldn't find that page :(</h2>
<h3 class="info">
It's possible this page hasn't been implemented yet.<br />
You can check the development status <a target="_blank" href="https://git.bradleylaboon.com/lb.laboon/lmc">here</a>.
You can check the development status <a target="_blank" href="https://laboon.dev/brad/lmc">here</a>.
</h3>
</fieldset>
</body>

View File

@ -16,22 +16,22 @@ This project is currently a work in progress. The following list provides a high
- [x] Block Storage (Volumes)
- [x] Images
- [x] DNS
- [x] Account Details (excluding PayPal support)
- [x] Account Details
- [x] User Profile Settings
- [x] Graphs
- [ ] PayPal payments
- [x] NodeBalancers
- [ ] Adding PayPal/GPay payment methods
- [ ] StackScripts
- [ ] NodeBalancers
- [ ] Longview
- [ ] Support Tickets
Eventually I plan to also implement features which were never available in the original manager, including Object Storage, One-Click Apps, and LKE.
## Reporting Issues
Before reporting an issue, please search the issue tracker to see if the issue has already reported. The canonical source of this repository is located at https://git.bradleylaboon.com/lb.laboon/lmc. Any forks on any other websites (including GitHub) are unofficial. The issue tracker located at that address should be used for all bug reports. In some cases, the root cause of an issue might be due to features which are currently missing from Linode's APIv4 (for example, APIv4 does not expose which physical host a given Linode instance is running on, so it is not possible to display this information like the original manager did). In these cases I may close the issue and redirect you upstream to Linode.
Before reporting an issue, please search the issue tracker to see if the issue has already reported. The canonical source of this repository is located at https://laboon.dev/brad/lmc. Any forks on any other websites (including GitHub) are unofficial. The issue tracker located at that address should be used for all bug reports. In some cases, the root cause of an issue might be due to features which are currently missing from Linode's APIv4 (for example, APIv4 does not expose which physical host a given Linode instance is running on, so it is not possible to display this information like the original manager did). In these cases I may close the issue and redirect you upstream to Linode.
## Contributing
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. 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.
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 for .shtml documents. Optionally, you can configure your web server to use the [404.html](404.html) file as a custom error document.

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeString } from "/global.js";
import { settings, elements, apiDelete, apiGet, apiPost, parseParams, setupHeader, timeString } from "/global.js";
(function()
{
@ -27,10 +27,9 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
elements.balancePositive = "balance-positive";
elements.balanceStatus = "balance-status";
elements.billingActivity = "billing-activity";
elements.ccNumber = "cc-number";
elements.ccDuration = "cc-duration";
elements.ccExpire = "cc-expire";
elements.current = "current";
elements.defaultPrefix = "default-payment-";
elements.deletePrefix = "delete-payment-";
elements.email = "email";
elements.gdpr = "gdpr";
elements.gdprDate = "gdpr-date";
@ -41,6 +40,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
elements.managed = "managed";
elements.managedButton = "managed-button";
elements.pay = "pay";
elements.paymentMethods = "payment-methods";
elements.promotions = "promotions";
elements.promotionsTable = "promotions-table";
elements.uninvoiced = "uninvoiced";
@ -50,7 +50,8 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
var data = {};
data.account = {};
data.invoices = [];
data.linodes = [];
data.numLinodes = 0;
data.paymentMethods = [];
data.payments = [];
// Static references to UI elements
@ -60,9 +61,6 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
ui.balance = {};
ui.balanceStatus = {};
ui.billingActivity = {};
ui.ccNumber = {};
ui.ccDuration = {};
ui.ccExpire = {};
ui.current = {};
ui.email = {};
ui.gdpr = [];
@ -70,6 +68,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
ui.managed = {};
ui.managedButton = {};
ui.pay = {};
ui.paymentMethods = {};
ui.promotions = [];
ui.promotionsTable = {};
ui.uninvoiced = {};
@ -80,6 +79,78 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
state.haveInvoices = false;
state.havePayments = false;
// Creates a row for the payment methods table
var createMethodRow = function(method)
{
var typeNames = {
"credit_card": "Credit Card",
"google_pay": "Google Pay",
"paypal": "PayPal"
};
var row = document.createElement("tr");
row.className = elements.lmcRowStandard;
var type = document.createElement("td");
if (typeNames[method.type])
type.innerHTML = typeNames[method.type];
else
type.innerHTML = method.type;
row.appendChild(type);
var details = document.createElement("td");
if (method.type == "credit_card" || method.type == "google_pay") {
var ccInfo = document.createElement("span");
ccInfo.innerHTML = method.data.card_type + " xxxxxxxxxxxx" + method.data.last_four + " Exp: " + method.data.expiry;
var br = document.createElement("br");
details.appendChild(ccInfo);
details.appendChild(br);
var monthYear = method.data.expiry.split("/");
var expireDate = new Date(parseInt(monthYear[1]), parseInt(monthYear[0]), 0);
var now = new Date();
if (expireDate - now > 0) {
var duration = document.createElement("span");
duration.innerHTML = "Expires " + timeString(now - expireDate, true);
details.appendChild(duration);
} else {
var expired = document.createElement("strong");
expired.innerHTML = "Expired!";
details.appendChild(expired);
}
} else if (method.type == "paypal") {
var email = document.createElement("span");
email.innerHTML = method.data.email;
details.appendChild(email);
}
row.appendChild(details);
var options = document.createElement("td");
if (method.is_default) {
var defaultMsg = document.createElement("strong");
defaultMsg.innerHTML = "This is the default payment method";
options.appendChild(defaultMsg);
} else {
var defaultLink = document.createElement("a");
defaultLink.id = elements.defaultPrefix + method.id;
defaultLink.href = "#";
defaultLink.innerHTML = "Make default";
defaultLink.addEventListener("click", defaultMethod);
var separator = document.createElement("span");
separator.innerHTML = " | ";
var deleteLink = document.createElement("a");
deleteLink.id = elements.deletePrefix + method.id;
deleteLink.href = "#";
deleteLink.innerHTML = "Delete";
deleteLink.addEventListener("click", deleteMethod);
options.appendChild(defaultLink);
options.appendChild(separator);
options.appendChild(deleteLink);
}
row.appendChild(options);
return row;
};
// Creates a row for the promotion table
var createPromoRow = function(promo)
{
@ -165,6 +236,32 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
return row;
};
// Handler for making a payment method default
var defaultMethod = function(event)
{
var id = event.currentTarget.id.slice(elements.defaultPrefix.length);
if (!confirm("Make this the default payment method?"))
return;
apiPost("/account/payment-methods/" + id + "/make-default", {}, function(response)
{
location.reload();
});
};
// Handler for deleting a payment method
var deleteMethod = function(event)
{
var id = event.currentTarget.id.slice(elements.deletePrefix.length);
if (!confirm("Delete this payment method?"))
return;
apiDelete("/account/payment-methods/" + id, function(response)
{
location.reload();
});
};
// Callback for account details API call
var displayAccount = function(response)
{
@ -199,30 +296,6 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
// Email
ui.email.innerHTML = data.account.email;
// CC info
ui.ccNumber.innerHTML = data.account.credit_card.last_four;
var expired = document.createElement("span");
var strong = document.createElement("strong");
strong.innerHTML = "Expired!";
var dash = document.createElement("span");
dash.innerHTML = " - ";
var updateLink = document.createElement("a");
updateLink.href = "/account/creditcard";
updateLink.innerHTML = "update credit card";
expired.appendChild(strong);
expired.appendChild(dash);
expired.appendChild(updateLink);
if (data.account.credit_card.expiry) {
ui.ccExpire.innerHTML = data.account.credit_card.expiry;
var monthYear = data.account.credit_card.expiry.split("/");
var expireDate = new Date(parseInt(monthYear[1]), parseInt(monthYear[0]), 0);
var now = new Date();
if (expireDate - now > 0)
ui.ccDuration.innerHTML = "Expires " + timeString(now - expireDate, true);
else
ui.ccDuration.appendChild(expired);
}
// Account balance
if (data.account.balance == 0) {
ui.balance.className = elements.balancePositive;
@ -275,15 +348,23 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
// Callback for linodes API call
var displayLinodes = function(response)
{
data.linodes = data.linodes.concat(response.data);
data.numLinodes = response.results;
ui.managedButton.disabled = false;
};
// Callback for payment methods API call
var displayMethods = function(response)
{
data.paymentMethods = data.paymentMethods.concat(response.data);
// Request the next page if there are more
if (response.page != response.pages) {
apiGet("/linode/instances?page=" + (response.page + 1), displayLinodes, null);
apiGet("/account/payment-methods?page=" + (response.page + 1), displayMethods, null);
return;
}
ui.managedButton.disabled = false;
for (var i = 0; i < data.paymentMethods.length; i++)
ui.paymentMethods.appendChild(createMethodRow(data.paymentMethods[i]));
};
// Callback for payments API call
@ -333,7 +414,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
if (event.currentTarget.disabled)
return;
if (!confirm("Linode Managed costs an additional $100/mo per Linode. This will increase your projected monthly bill by $" + (data.linodes.length * 100) + ". Are you sure?"))
if (!confirm("Linode Managed costs an additional $100/mo per Linode. This will increase your projected monthly bill by $" + (data.numLinodes * 100) + ". Are you sure?"))
return;
apiPost("/account/settings/managed-enable", {}, function(response)
@ -356,9 +437,6 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
ui.balance = document.getElementById(elements.balance);
ui.balanceStatus = document.getElementById(elements.balanceStatus);
ui.billingActivity = document.getElementById(elements.billingActivity);
ui.ccNumber = document.getElementById(elements.ccNumber);
ui.ccDuration = document.getElementById(elements.ccDuration);
ui.ccExpire = document.getElementById(elements.ccExpire);
ui.current = document.getElementById(elements.current);
ui.email = document.getElementById(elements.email);
ui.gdpr = document.getElementsByClassName(elements.gdpr);
@ -366,6 +444,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
ui.managed = document.getElementById(elements.managed);
ui.managedButton = document.getElementById(elements.managedButton);
ui.pay = document.getElementById(elements.pay);
ui.paymentMethods = document.getElementById(elements.paymentMethods);
ui.promotions = document.getElementsByClassName(elements.promotions);
ui.promotionsTable = document.getElementById(elements.promotionsTable);
ui.uninvoiced = document.getElementById(elements.uninvoiced);
@ -379,6 +458,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeStri
apiGet("/account/settings", displaySettings, null);
apiGet("/account/invoices", displayInvoices, null);
apiGet("/account/payments", displayPayments, null);
apiGet("/account/payment-methods", displayMethods, null);
apiGet("/linode/instances", displayLinodes, null);
};

View File

@ -117,7 +117,7 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
state.havePlans = true;
if (state.haveLinode)
if (state.haveLinodes)
displayCost();
};

View File

@ -15,15 +15,14 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/global.js";
import { settings, elements, apiPost, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.ccCurrent = "cc-current";
elements.ccNew = "cc-new";
elements.cvv = "cvv";
elements.expiryCurrent = "expiry-current";
elements.default = "default";
elements.expiryMonth = "expiry-month";
elements.expiryYear = "expiry-year";
elements.updateButton = "update-button";
@ -33,24 +32,13 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
// Static references to UI elements
var ui = {};
ui.ccCurrent = {};
ui.ccNew = {};
ui.cvv = {};
ui.expiryCurrent = {};
ui.default = {};
ui.expiryMonth = {};
ui.expiryYear = {};
ui.updateButton = {};
// Callback for account details API call
var displayCC = function(response)
{
if (!response.credit_card)
return;
ui.ccCurrent.innerHTML = response.credit_card.last_four;
ui.expiryCurrent.innerHTML = response.credit_card.expiry;
};
// Click handler for update button
var handleUpdate = function(event)
{
@ -58,13 +46,17 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
return;
var req = {
"card_number": ui.ccNew.value,
"cvv": ui.cvv.value,
"expiry_month": parseInt(ui.expiryMonth.value),
"expiry_year": parseInt(ui.expiryYear.value)
"data": {
"card_number": ui.ccNew.value.replaceAll("-", "").replaceAll(" ", ""),
"cvv": ui.cvv.value,
"expiry_month": parseInt(ui.expiryMonth.value),
"expiry_year": parseInt(ui.expiryYear.value)
},
"is_default": ui.default.checked,
"type": "credit_card"
};
apiPost("/account/credit-card", req, function(response)
apiPost("/account/payment-methods", req, function(response)
{
location.href = "/account";
});
@ -79,10 +71,9 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
setupHeader();
// Get element references
ui.ccCurrent = document.getElementById(elements.ccCurrent);
ui.ccNew = document.getElementById(elements.ccNew);
ui.cvv = document.getElementById(elements.cvv);
ui.expiryCurrent = document.getElementById(elements.expiryCurrent);
ui.default = document.getElementById(elements.default);
ui.expiryMonth = document.getElementById(elements.expiryMonth);
ui.expiryYear = document.getElementById(elements.expiryYear);
ui.updateButton = document.getElementById(elements.updateButton);
@ -99,9 +90,6 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
// Register event handlers
ui.updateButton.addEventListener("click", handleUpdate);
// Get data from API
apiGet("/account", displayCC, null);
};
// Attach onload handler

View File

@ -18,7 +18,7 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>LMC - Account // Update Credit Card</title>
<title>LMC - Account // Add Credit Card</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="creditcard.css" />
<script src="creditcard.js" type="module"></script>
@ -27,35 +27,17 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<!--#include virtual="/include/header.html"-->
<!--#include virtual="/include/account_subnav.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/account">Account</a> » <span class="top-links-title">Update Credit Card</span></div>
<div id="top-links"><a href="/account">Account</a> » <span class="top-links-title">Add Credit Card</span></div>
<div id="creditcard">
<table class="lmc-table">
<thead>
<tr>
<td colspan="3">Update Credit Card</td>
</tr>
<tr>
<td colspan="3">Current Card</td>
<td colspan="3">Add Credit Card</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td>Current Card</td>
<td>xxxxxxxxxxxx<span id="cc-current"></span> Exp: <span id="expiry-current"></span></td>
<td></td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Update Card</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>New Card Number</td>
<td>Card Number</td>
<td><input id="cc-new" type="text" /></td>
<td class="info">Linode accepts Visa, MasterCard, American Express, and Discover</td>
</tr>
@ -85,9 +67,14 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
</td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>Make Default</td>
<td><input id="default" type="checkbox" /></td>
<td class="info">This card will become the new default payment method</td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td colspan="2"><button id="update-button" type="button">Update Credit Card</button>
<td colspan="2"><button id="update-button" type="button">Add Credit Card</button>
</tr>
</tbody>
</table>

View File

@ -52,18 +52,10 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Credit Card</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Credit Card</td>
<td colspan="2">
xxxxxxxxxxxx<span id="cc-number"></span> Exp: <span id="cc-expire"></span><br />
<span id="cc-duration"></span>
</td>
<td colspan="3">Payment Methods</td>
</tr>
</tbody>
<tbody id="payment-methods"></tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>

View File

@ -54,7 +54,7 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<td id="subtotal"></td>
</tr>
<tr>
<td>Tax:</td>
<td>Tax Subtotal:</td>
<td id="tax"></td>
</tr>
<tr>

View File

@ -91,6 +91,16 @@ import { settings, elements, apiGet, parseParams, setupHeader } from "/global.js
var displayInvoice = function(response)
{
ui.subtotal.innerHTML = "$" + response.subtotal.toFixed(2);
for (var i = 0; i < response.tax_summary.length; i++) {
var taxRow = document.createElement("tr");
var taxText = document.createElement("td");
taxText.innerHTML = response.tax_summary[i].name + ":";
var tax = document.createElement("td");
tax.innerHTML = "$" + response.tax_summary[i].tax.toFixed(2);
taxRow.appendChild(taxText);
taxRow.appendChild(tax);
ui.tax.parentNode.parentNode.insertBefore(taxRow, ui.tax.parentNode);
}
ui.tax.innerHTML = "$" + response.tax.toFixed(2);
ui.total.innerHTML = "$" + response.total.toFixed(2);
};

View File

@ -32,62 +32,25 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<table class="lmc-table">
<thead>
<tr>
<td colspan="3">Make a Payment</td>
<td colspan="2">Make a Payment</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td>Current Balance</td>
<td id="balance" class="balance-positive"></td>
<td></td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Make a Payment - via Credit Card</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Current Card</td>
<td>xxxxxxxxxxxx<span id="cc-number"></span> Exp: <span id="cc-exp"></span></td>
<td class="info"><a href="/account/creditcard">(update credit card)</a></td>
</tr>
<tr class="lmc-tr3">
<td>Amount to Charge</td>
<td><input id="cc-amount" type="number" min="0" max="50000" step="0.01" value="0.00" /> <span class="info">(USD)</span></td>
<td></td>
<td><input id="cc-amount" type="number" min="5" max="2000" step="0.01" value="5.00" /> <span class="info">(USD)</span></td>
</tr>
<tr class="lmc-tr3">
<td>CVV</td>
<td><input id="cvv" type="text" size="8" /> <span class="info">(optional)</span></td>
<td></td>
<td>Payment Method</td>
<td><select id="method"></select></td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td colspan="2"><button disabled id="cc-charge" type="button">Charge Credit Card</button></td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Make a Payment - via PayPal</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Amount to Pay</td>
<td><input id="paypal-amount" type="number" min="0" max="10000" step="0.01" value="0.00" /> <span class="info">(USD)</span></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td colspan="2"><button disabled id="paypal-charge" type="button">Continue...</button></td>
<td><button disabled id="cc-charge" type="button">Make Payment</button></td>
</tr>
</tbody>
</table>

View File

@ -24,25 +24,18 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
elements.balanceNegative = "balance-negative";
elements.ccAmount = "cc-amount";
elements.ccCharge = "cc-charge";
elements.ccExp = "cc-exp";
elements.ccNumber = "cc-number";
elements.cvv = "cvv";
elements.paypalAmount = "paypal-amount";
elements.paypalCharge = "paypal-charge";
elements.method = "method";
// Data received from API calls
var data = {};
data.methods = [];
// Static references to UI elements
var ui = {};
ui.balance = {};
ui.ccAmount = {};
ui.ccCharge = {};
ui.ccExp = {};
ui.ccNumber = {};
ui.cvv = {};
ui.paypalAmount = {};
ui.paypalCharge = {};
ui.method = {};
// Callback for account details API call
var displayAccount = function(response)
@ -53,17 +46,55 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
} else if (response.balance > 0) {
ui.balance.innerHTML += response.balance.toFixed(2) + " outstanding";
ui.balance.className = elements.balanceNegative;
if (response.balance < 5) {
ui.ccAmount.min = response.balance.toFixed(2);
ui.ccAmount.value = response.balance.toFixed(2);
} else if (response.balance > 2000) {
ui.ccAmount.max = Math.min(50000, response.balance.toFixed(2));
}
} else {
ui.balance.innerHTML += response.balance.toFixed(2);
}
if (response.credit_card) {
ui.ccNumber.innerHTML = response.credit_card.last_four;
ui.ccExp.innerHTML = response.credit_card.expiry;
ui.ccCharge.disabled = false;
};
// Callback for payment methods API call
var displayMethods = function(response)
{
data.methods = data.methods.concat(response.data);
// Request the next page if there are more
if (response.page != response.pages) {
apiGet("/account/payment-methods?page=" + (response.page + 1), displayMethods, null);
return;
}
var typeNames = {
"credit_card": "Credit Card",
"google_pay": "Google Pay",
"paypal": "PayPal"
};
for (var i = 0; i < data.methods.length; i++) {
var option = document.createElement("option");
option.value = data.methods[i].id;
if (typeNames[data.methods[i].type])
option.innerHTML = typeNames[data.methods[i].type];
else
option.innerHTML = data.methods[i].type;
if (data.methods[i].type == "credit_card" || data.methods[i].type == "google_pay")
option.innerHTML += ": " + data.methods[i].data.card_type + " ****" + data.methods[i].data.last_four;
else if (data.methods[i].type == "paypal")
option.innerHTML += ": " + data.methods[i].data.email;
ui.method.appendChild(option);
if (data.methods[i].is_default) {
option.innerHTML += " (default)";
ui.method.value = data.methods[i].id;
}
}
ui.ccCharge.disabled = false;
//ui.paypalCharge.disabled = false;
};
// Click handler for CC charge button
@ -76,10 +107,9 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
return;
var req = {
"payment_method_id": parseInt(ui.method.value),
"usd": ui.ccAmount.value
};
if (ui.cvv.value.length)
req.cvv = ui.cvv.value;
apiPost("/account/payments", req, function(response)
{
@ -87,13 +117,6 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
});
};
// Click handler for PayPal button
var handlePayPal = function(event)
{
if (event.currentTarget.disabled)
return;
};
// Initial setup
var setup = function()
{
@ -106,18 +129,14 @@ import { settings, elements, apiGet, apiPost, parseParams, setupHeader } from "/
ui.balance = document.getElementById(elements.balance);
ui.ccAmount = document.getElementById(elements.ccAmount);
ui.ccCharge = document.getElementById(elements.ccCharge);
ui.ccExp = document.getElementById(elements.ccExp);
ui.ccNumber = document.getElementById(elements.ccNumber);
ui.cvv = document.getElementById(elements.cvv);
ui.paypalAmount = document.getElementById(elements.paypalAmount);
ui.paypalCharge = document.getElementById(elements.paypalCharge);
ui.method = document.getElementById(elements.method);
// Register event handlers
ui.ccCharge.addEventListener("click", handleCharge);
ui.paypalCharge.addEventListener("click", handlePayPal);
// Get data from API
apiGet("/account", displayAccount, null);
apiGet("/account/payment-methods", displayMethods, null);
};
// Attach onload handler

View File

@ -35,36 +35,38 @@ var elements = {
"username": "username"
};
// Regions (Linode doesn't provide "friendly" names via the API)
// Region names for legacy DCs no longer in the API
var regionNames = {
"us-central": "Dallas, TX, USA",
"us-west": "Fremont, CA, USA",
"us-southeast": "Atlanta, GA, USA",
"us-east": "Newark, NJ, USA",
"us-east-1b": "Newark 2, NJ, USA",
"eu-west": "London, England, UK",
"ap-south": "Singapore, SG",
"eu-central": "Frankfurt, DE",
"ap-northeast": "Tokyo, JP",
"ap-northeast-1a": "Tokyo 2, JP",
"ca-central": "Toronto, ON, CA",
"ap-west": "Mumbai, IN",
"ap-southeast": "Sydney, NSW, AU",
"philadelphia": "Philadelphia, PA, USA",
"absecon": "Absecon, NJ, USA",
"us-iad": "Washington DC, USA",
"us-ord": "Chicago, IL, USA",
"fr-par": "Paris, FR",
"in-maa": "Chennai, IN",
"us-sea": "Seattle, WA, USA",
"br-gru": "San Paulo, BR",
"nl-ams": "Amsterdam, NL",
"se-sto": "Stockholm, SE",
"jp-osa": "Osaka, JP",
"it-mil": "Milan, IT",
"id-cgk": "Jakarta, ID",
"us-lax": "Los Angeles, CA, USA",
"us-mia": "Miami, FL, USA"
"us-east-1b": "Newark 2, NJ",
"philadelphia": "Philadelphia, PA",
"absecon": "Absecon, NJ"
};
// Group contries into regions for easier selection
var countryContinents = {
"us": "na",
"gb": "eu",
"jp": "ap",
"sg": "ap",
"de": "eu",
"in": "ap",
"ca": "na",
"au": "ap",
"fr": "eu",
"br": "sa",
"nl": "eu",
"se": "eu",
"es": "eu",
"it": "eu",
"id": "ap",
"nz": "ap",
"pl": "eu",
"za": "af",
"my": "ap",
"hk": "ap",
"co": "sa",
"mx": "na",
"cl": "sa"
};
// Human-readable event titles
@ -195,7 +197,8 @@ var oauthScopes = {
"nodebalancers": "NodeBalancers",
"object_storage": "Object Storage",
"stackscripts": "StackScripts",
"volumes": "Volumes"
"volumes": "Volumes",
"vpc": "VPCs"
};
// Make an HTTP DELETE request to the Linode API
@ -446,6 +449,24 @@ function apiPut(endpoint, data, callback)
xmlhttp.send(JSON.stringify(data));
}
// Convert an unqualified count into a string with an SI prefix (i.e. bytes to MB/GB/etc)
function countSI(count)
{
var prefix = "KMGTPEZY";
var unit = "";
for (var i = 0; i < prefix.length; i++) {
if (count >= 1024) {
count /= 1024;
unit = prefix.charAt(i);
} else {
break;
}
}
return count.toFixed(2) + " " + unit;
}
// Callback for user info API call
function displayUser(response)
{
@ -862,4 +883,4 @@ function translateKernel(slug, element)
apiGet("/linode/kernels/" + slug, callback, null);
}
export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, drawSeries, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel };
export { settings, elements, regionNames, countryContinents, apiDelete, apiGet, apiPost, apiPut, countSI, drawSeries, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel };

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, apiPost, parseParams, setupHeader, timeString } from "/global.js";
import { settings, elements, apiGet, apiPost, parseParams, setupHeader, timeString } from "/global.js";
(function()
{

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, apiDelete, parseParams, setupHeader, timeString } from "/global.js";
import { settings, elements, apiGet, apiDelete, parseParams, setupHeader, timeString } from "/global.js";
(function()
{

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, apiPut, parseParams, setupHeader, timeString } from "/global.js";
import { settings, elements, apiGet, apiPut, parseParams, setupHeader, timeString } from "/global.js";
(function()
{

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, parseParams, setupHeader, timeString } from "/global.js";
import { settings, elements, apiGet, parseParams, setupHeader, timeString } from "/global.js";
(function()
{

View File

@ -46,7 +46,14 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
</tr>
<tr class="lmc-tr3">
<td>Region</td>
<td><select id="region"></select></td>
<td><select id="region">
<optgroup id="na" label="North America"></optgroup>
<optgroup id="eu" label="Europe"></optgroup>
<optgroup id="ap" label="Asia/Pacific"></optgroup>
<optgroup id="sa" label="South America"></optgroup>
<optgroup id="af" label="Africa"></optgroup>
<optgroup id="dc-other" label="Other"></optgroup>
</select></td>
<td class="info">
For fastest initial upload, select the region that is geographically closest to you.<br />
Once uploaded you will be able to deploy the image to other regions.

View File

@ -17,6 +17,10 @@
@import url('/global.css');
optgroup {
display: none;
}
.step-2 {
display: none;
}

View File

@ -15,11 +15,12 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, apiPost, objPut, parseParams, setupHeader } from "/global.js";
import { settings, elements, regionNames, countryContinents, apiGet, apiPost, objPut, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.dcOther = "dc-other";
elements.description = "description";
elements.imageFile = "image-file";
elements.label = "label";
@ -35,6 +36,7 @@ import { settings, elements, regionNames, apiGet, apiPost, objPut, parseParams,
// Static references to UI elements
var ui = {};
ui.dcOther = {};
ui.description = {};
ui.imageFile = {};
ui.label = {};
@ -49,11 +51,19 @@ import { settings, elements, regionNames, apiGet, apiPost, objPut, parseParams,
for (var i = 0; i < response.data.length; i++) {
var dc = document.createElement("option");
dc.value = response.data[i].id;
if (regionNames[response.data[i].id])
if (response.data[i].label && response.data[i].label.length)
dc.innerHTML = response.data[i].label;
else if (regionNames[response.data[i].id])
dc.innerHTML = regionNames[response.data[i].id];
else
dc.innerHTML = response.data[i].id;
ui.region.appendChild(dc);
var optgroup = null;
if (countryContinents[response.data[i].country])
optgroup = document.getElementById(countryContinents[response.data[i].country]);
if (!optgroup)
optgroup = ui.dcOther;
optgroup.style.display = "initial";
optgroup.appendChild(dc);
}
ui.nextButton.disabled = false;
@ -126,6 +136,7 @@ import { settings, elements, regionNames, apiGet, apiPost, objPut, parseParams,
}
// Get element references
ui.dcOther = document.getElementById(elements.dcOther);
ui.description = document.getElementById(elements.description);
ui.imageFile = document.getElementById(elements.imageFile);
ui.label = document.getElementById(elements.label);

View File

@ -2,7 +2,7 @@
<nav id="subnav" class="wrapper">
<a class="subnav-link" href="/account">Account</a>
<a class="subnav-link" href="/account/contact">Contact Info</a>
<a class="subnav-link" href="/account/creditcard">Update Credit Card</a>
<a class="subnav-link" href="/account/creditcard">Add Credit Card</a>
<a class="subnav-link" href="/account/make_a_payment">Make A Payment</a>
<a class="subnav-link" href="/account/billing_history">Billing History</a>
<a class="subnav-link" href="/user">Users</a>

View File

@ -89,6 +89,10 @@ h3 {
padding: 15px;
}
optgroup {
display: none;
}
#right-links {
float: right;
}

View File

@ -15,13 +15,14 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, regionNames, apiGet, apiPost, parseParams, setupHeader } from "/global.js";
import { settings, elements, regionNames, countryContinents, apiGet, apiPost, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.addButton = "add-button";
elements.datacenters = "datacenters";
elements.dcOther = "dc-other";
elements.instanceType = "instance-type";
elements.instanceTypeActive = "instance-type-active";
@ -33,6 +34,7 @@ import { settings, elements, regionNames, apiGet, apiPost, parseParams, setupHea
// Static references to UI elements
var ui = {};
ui.datacenters = {};
ui.dcOther = {};
var createLinodeTypeButton = function(type)
{
@ -67,16 +69,26 @@ import { settings, elements, regionNames, apiGet, apiPost, parseParams, setupHea
for (var i = 0; i < response.data.length; i++) {
var dc = document.createElement("option");
dc.value = response.data[i].id;
if (regionNames[response.data[i].id])
if (response.data[i].label && response.data[i].label.length)
dc.innerHTML = response.data[i].label;
else if (regionNames[response.data[i].id])
dc.innerHTML = regionNames[response.data[i].id];
else
dc.innerHTML = response.data[i].id;
ui.datacenters.appendChild(dc);
var optgroup = null;
if (countryContinents[response.data[i].country])
optgroup = document.getElementById(countryContinents[response.data[i].country]);
if (!optgroup)
optgroup = ui.dcOther;
optgroup.style.display = "initial";
optgroup.appendChild(dc);
}
// Request the next page if there are more pages
if (response.page != response.pages)
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), displayRegions, null);
return;
}
};
var displayTypes = function(response)
@ -135,6 +147,7 @@ import { settings, elements, regionNames, apiGet, apiPost, parseParams, setupHea
data.params = parseParams();
ui.datacenters = document.getElementById(elements.datacenters);
ui.dcOther = document.getElementById(elements.dcOther);
// Register add button handler
document.getElementById(elements.addButton).addEventListener("click", handleAddLinode);

View File

@ -65,7 +65,14 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
</div>
<div id="location">
<h2>Location</h2>
<select id="datacenters"></select>
<select id="datacenters">
<optgroup id="na" label="North America"></optgroup>
<optgroup id="eu" label="Europe"></optgroup>
<optgroup id="ap" label="Asia/Pacific"></optgroup>
<optgroup id="sa" label="South America"></optgroup>
<optgroup id="af" label="Africa"></optgroup>
<optgroup id="dc-other" label="Other"></optgroup>
</select>
</div>
<div id="submit">
<button id="add-button" type="button">Add this Linode!</button>

View File

@ -50,6 +50,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
data.backup = {};
data.linode = {};
data.linodes = [];
data.region = {};
data.types = [];
// Static references to UI elements
@ -89,6 +90,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
var plan = document.createElement("td");
if (linode.type) {
plan.innerHTML = linode.type;
for (var i = 0; i < data.types.length; i++) {
if (data.types[i].id == linode.type) {
plan.innerHTML = data.types[i].label;
@ -103,7 +105,9 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
row.appendChild(plan);
var location = document.createElement("td");
if (regionNames[linode.region])
if (data.region.label && data.region.label.length)
location.innerHTML = data.region.label;
else if (regionNames[linode.region])
location.innerHTML = regionNames[linode.region];
else
location.innerHTML = linode.region;
@ -136,6 +140,15 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.backupType.innerHTML = data.backup.type;
if (regionNames[data.backup.region]) {
ui.backupLocation.innerHTML = regionNames[data.backup.region];
ui.destLocation.innerHTML = regionNames[data.backup.region];
} else {
ui.backupLocation.innerHTML = data.backup.region;
ui.destLocation.innerHTML = data.backup.region;
}
apiGet("/regions/" + data.backup.region, displayRegion, null);
for (var i = 0; i < data.backup.configs.length; i++) {
var li = document.createElement("li");
li.innerHTML = data.backup.configs[i];
@ -152,7 +165,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.backupSize.innerHTML = data.backup.totalSize + " MB";
if (state.haveTypes && data.linode.id)
if (state.haveTypes)
insertTypes();
};
@ -169,18 +182,6 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.linodeTagLink.innerHTML = "(" + data.linode.tags[0] + ")";
ui.linodeTag.style.display = "inline";
}
// Display location
if (regionNames[data.linode.region]) {
ui.backupLocation.innerHTML = regionNames[data.linode.region];
ui.destLocation.innerHTML = regionNames[data.linode.region];
} else {
ui.backupLocation.innerHTML = data.linode.region;
ui.destLocation.innerHTML = data.linode.region;
}
if (state.haveTypes && data.backup.id)
insertTypes();
};
// Callback for linode disks API call
@ -238,7 +239,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
// Request the next page if there are more pages
if (response.page != response.pages) {
var filter = {
"region": data.linode.region
"region": data.backup.region
};
apiGet("/linode/instances?page=" + (response.page + 1), displayLinodes, filter);
return;
@ -254,6 +255,17 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
}
};
// Callback for region API call
var displayRegion = function(response)
{
data.region = response;
if (data.region.label && data.region.label.length) {
ui.backupLocation.innerHTML = response.label;
ui.destLocation.innerHTML = response.label;
}
};
// Callback for linode types API call
var displayTypes = function(response)
{
@ -267,7 +279,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
}
state.haveTypes = true;
if (data.backup.id && data.linode.id)
if (data.backup.id)
insertTypes();
};
@ -295,7 +307,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
var req = {
"label": ui.destLabel.value,
"type": ui.destPlan.value,
"region": data.linode.region,
"region": data.backup.region,
"backup_id": data.backup.id
};
var callback = function(response)
@ -348,11 +360,11 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.destPlan.appendChild(option);
}
updatePrice(null);
updateSpace(null);
ui.newLinode.disabled = false;
var filter = {
"region": data.linode.region
"region": data.backup.region
};
apiGet("/linode/instances", displayLinodes, filter);
};
@ -410,7 +422,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.newLinode = document.getElementById(elements.newLinode);
// Register event handlers
ui.destPlan.addEventListener("input", updatePrice);
ui.destPlan.addEventListener("input", updateSpace);
ui.newLinode.addEventListener("click", handleCreate);
// Get data from API
@ -431,7 +443,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
};
// Update the price display
var updatePrice = function(event)
var updateSpace = function(event)
{
// Find the selected type
var type = null;

View File

@ -15,7 +15,7 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNames, setupHeader, translateKernel } from "/global.js";
import { settings, elements, apiGet, apiPost, countryContinents, migrateETA, parseParams, regionNames, setupHeader, translateKernel } from "/global.js";
(function()
{
@ -25,6 +25,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
elements.configDiskRow = "config-disk-row";
elements.configsNone = "configs-none";
elements.configTable = "config-table-body";
elements.dcOther = "dc-other";
elements.destBackups = "dest-backups";
elements.destBackupsPrice = "dest-backups-price";
elements.destLabel = "dest-label";
@ -54,6 +55,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
data.configs = [];
data.disks = [];
data.linode = {};
data.regions = [];
data.types = [];
// Static references to UI elements
@ -62,6 +64,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
ui.configCloneTable = {};
ui.configsNone = {};
ui.configTable = {};
ui.dcOther = {};
ui.destBackups = {};
ui.destBackupsPrice = {};
ui.destLabel = {};
@ -241,7 +244,17 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
}
// Display the source location
if (regionNames[data.linode.region])
var region = null;
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == data.linode.region) {
region = data.regions[i];
break;
}
}
if (region && region.label && region.label.length)
ui.sourceLocation.innerHTML = region.label;
else if (regionNames[data.linode.region])
ui.sourceLocation.innerHTML = regionNames[data.linode.region];
else
ui.sourceLocation.innerHTML = data.linode.region;
@ -290,25 +303,8 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
// Callback for regions API call
var displayRegions = function(response)
{
for (var i = 0; i < response.data.length; i++) {
// Add regions to selector
var dc = document.createElement("option");
dc.value = response.data[i].id;
if (regionNames[response.data[i].id])
dc.innerHTML = regionNames[response.data[i].id];
else
dc.innerHTML = response.data[i].id;
ui.destLocation.appendChild(dc);
// Add optgroups to linode selector
var optgroup = document.createElement("optgroup");
optgroup.id = response.data[i].id;
if (regionNames[response.data[i].id])
optgroup.label = regionNames[response.data[i].id];
else
optgroup.label = response.data[i].label;
ui.destLinode.appendChild(optgroup);
}
// Add regions to array
data.regions = data.regions.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
@ -316,6 +312,37 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
return;
}
for (var i = 0; i < data.regions.length; i++) {
// Add regions to selector
var dc = document.createElement("option");
dc.value = data.regions[i].id;
if (data.regions[i].label && data.regions[i].label.length)
dc.innerHTML = data.regions[i].label;
else if (regionNames[data.regions[i].id])
dc.innerHTML = regionNames[data.regions[i].id];
else
dc.innerHTML = data.regions[i].id;
var group = null;
if (countryContinents[data.regions[i].country])
group = document.getElementById(countryContinents[data.regions[i].country]);
if (!group)
group = ui.dcOther;
group.style.display = "initial";
group.appendChild(dc);
// Add optgroups to linode selector
var optgroup = document.createElement("optgroup");
optgroup.id = data.regions[i].id;
if (data.regions[i].label && data.regions[i].label.length)
optgroup.label = data.regions[i].label;
else if (regionNames[data.regions[i].id])
optgroup.label = regionNames[data.regions[i].id];
else
optgroup.label = data.regions[i].label;
ui.destLinode.appendChild(optgroup);
}
apiGet("/linode/instances/" + data.params.lid, displayDetails, null);
apiGet("/linode/instances", displayLinodes, null);
};
@ -415,6 +442,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
ui.configCloneTable = document.getElementById(elements.configCloneTable);
ui.configsNone = document.getElementById(elements.configsNone);
ui.configTable = document.getElementById(elements.configTable);
ui.dcOther = document.getElementById(elements.dcOther);
ui.destBackups = document.getElementById(elements.destBackups);
ui.destBackupsPrice = document.getElementById(elements.destBackupsPrice);
ui.destLabel = document.getElementById(elements.destLabel);
@ -442,7 +470,6 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, regionNam
ui.destPlan.addEventListener("input", updatePrices);
// Get data from 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/types", displayTypes, null);

View File

@ -142,7 +142,14 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
</tr>
<tr class="lmc-tr3 new-linode">
<td>Location</td>
<td><select id="dest-location"></select></td>
<td><select id="dest-location">
<optgroup id="na" label="North America"></optgroup>
<optgroup id="eu" label="Europe"></optgroup>
<optgroup id="ap" label="Asia/Pacific"></optgroup>
<optgroup id="sa" label="South America"></optgroup>
<optgroup id="af" label="Africa"></optgroup>
<optgroup id="dc-other" label="Other"></optgroup>
</select></td>
</tr>
<tr class="lmc-tr3 new-linode">
<td>Plan</td>

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, drawSeries, eventTitles, parseParams, setupHeader, timeString, translateKernel } from "/global.js";
import { settings, elements, apiDelete, apiGet, apiPost, countSI, drawSeries, eventTitles, parseParams, setupHeader, timeString, translateKernel } from "/global.js";
(function()
{
@ -233,24 +233,6 @@ import { settings, elements, apiDelete, apiGet, apiPost, drawSeries, eventTitles
apiPost("/linode/instances/" + data.params.lid + "/boot", request, callback);
};
// 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 = "";
for (var i = 0; i < prefix.length; i++) {
if (count >= 1024) {
count /= 1024;
unit = prefix.charAt(i);
} else {
break;
}
}
return count.toFixed(2) + " " + unit;
};
// Generate a config profile table row
var createConfigRow = function(config)
{

View File

@ -48,6 +48,7 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
data.noTag = false;
data.notifications = [];
data.plans = [];
data.regions = [];
// Static references to UI elements
var ui = {};
@ -60,6 +61,8 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
var state = {};
state.haveLinodes = false;
state.haveNotifications = false;
state.haveRegions = false;
state.haveTypes = false;
var createLinodeRow = function(linode, alt)
{
@ -92,8 +95,10 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
plan.innerHTML = getPlanLabel(linode.type);
else
plan.innerHTML = "Unknown";
if (plan.innerHTML == "")
if (plan.innerHTML == "") {
plan.innerHTML = linode.type;
translatePlan(linode.type, plan);
}
var ip = document.createElement("td");
ip.innerHTML = linode.ipv4[0];
var ipCount = 0;
@ -110,8 +115,17 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
plus.innerHTML = " (+" + ipCount + ")";
ip.appendChild(plus);
}
var regionData = null;
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == linode.region) {
regionData = data.regions[i];
break;
}
}
var region = document.createElement("td");
if (regionNames[linode.region])
if (regionData && regionData.label && regionData.label.length)
region.innerHTML = regionData.label;
else if (regionNames[linode.region])
region.innerHTML = regionNames[linode.region];
else
region.innerHTML = linode.region;
@ -312,8 +326,19 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
body.innerHTML = data.notifications[i].message;
}
// Replace "this facility" with actual location for outages
if (data.notifications[i].type == "outage" && data.notifications[i].entity && data.notifications[i].entity.type == "region" && regionNames[data.notifications[i].entity.id])
header.innerHTML = header.innerHTML.replace("this facility", regionNames[data.notifications[i].entity.id]);
if (data.notifications[i].type == "outage" && data.notifications[i].entity && data.notifications[i].entity.type == "region") {
var region = null;
for (var j = 0; j < data.regions.length; j++) {
if (data.regions[j].id == data.notifications[i].entity.id) {
region = data.regions[j];
break;
}
}
if (region && region.label && region.label.length)
header.innerHTML = header.innerHTML.replace("this facility", region.label);
else if (regionNames[data.notifications[i].entity.id])
header.innerHTML = header.innerHTML.replace("this facility", regionNames[data.notifications[i].entity.id]);
}
notification.appendChild(header);
notification.appendChild(body);
ui.notifications.appendChild(notification);
@ -379,13 +404,42 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
return;
}
// Get linodes
var filters = null;
if (data.params.tag)
filters = {
"tags": data.params.tag
};
apiGet("/linode/instances", displayLinodes, filters);
state.haveTypes = true;
if (state.haveRegions) {
// Get linodes
var filters = null;
if (data.params.tag)
filters = {
"tags": data.params.tag
};
apiGet("/linode/instances", displayLinodes, filters);
}
};
var getRegions = function(response)
{
// Add regions to array
data.regions = data.regions.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), getRegions, null);
return;
}
state.haveRegions = true;
if (state.haveTypes) {
// Get linodes
var filters = null;
if (data.params.tag)
filters = {
"tags": data.params.tag
};
apiGet("/linode/instances", displayLinodes, filters);
}
// Get notifications
apiGet("/account/notifications", displayNotifications, null);
};
var insertLinodes = function()
@ -425,9 +479,9 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
setupHeader();
// Get linode and transfer info
apiGet("/regions", getRegions, null);
apiGet("/linode/types", getPlans, null);
apiGet("/account/transfer", displayTransfer, null);
apiGet("/account/notifications", displayNotifications, null);
};
var translatePlan = function(name, cell)

View File

@ -46,17 +46,19 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<br />
<h3>Standard</h3>
<div id="standard"></div>
</td>
<td>
<br />
<h3>Dedicated CPU</h3>
<div id="dedicated"></div>
<br />
</td>
<td>
<h3>High Memory</h3>
<div id="highmem"></div>
<br />
<h3>Dedicated GPU</h3>
<div id="gpu"></div>
<br />
<h3>Premium</h3>
<div id="premium"></div>
</td>
<td>
<h2>How it works</h2>

View File

@ -33,6 +33,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, setupHead
elements.linodeTag = "linode-tag";
elements.linodeTagLink = "linode-tag-link";
elements.nanode = "nanode";
elements.premium = "premium";
elements.resizeButton = "resize-button";
elements.standard = "standard";
@ -55,6 +56,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, setupHead
ui.linodeTag = {};
ui.linodeTagLink = {};
ui.nanode = {};
ui.premium = {};
ui.resizeButton = {};
ui.standard = {};
@ -234,6 +236,7 @@ import { settings, elements, apiGet, apiPost, migrateETA, parseParams, setupHead
ui.linodeTag = document.getElementById(elements.linodeTag);
ui.linodeTagLink = document.getElementById(elements.linodeTagLink);
ui.nanode = document.getElementById(elements.nanode);
ui.premium = document.getElementById(elements.premium);
ui.resizeButton = document.getElementById(elements.resizeButton);
ui.standard = document.getElementById(elements.standard);

View File

@ -53,7 +53,7 @@ import { clientID } from "/clientID.js";
params.set("response_type", "token");
params.set("state", localStorage.state);
location.href = settings.oauthURL + "/authorize" + "?" + params.toString();
location.href = settings.oauthURL + "/authorize?" + params.toString();
};
// Initial setup

72
nodebalancers/add/add.css Normal file
View File

@ -0,0 +1,72 @@
/*
* 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');
#add {
padding: 0px 15px 15px;
}
#add-button {
display: block;
font-size: 16px;
font-weight: bold;
margin: 0 auto;
padding: 5px;
}
#datacenters {
font-size: 18px;
margin-top: 10px;
}
h2 {
font-size: 18px;
margin: 0;
}
#location {
padding: 15px;
}
optgroup {
display: none;
}
#submit {
padding: 15px;
}
tbody td {
font-size: 14px;
}
tbody td:nth-of-type(2) {
color: green;
}
td {
font-weight: bold;
}
td:first-of-type {
text-align: center;
}
thead {
background: linear-gradient(#00EE00, #00BF00);
}

147
nodebalancers/add/add.js Normal file
View File

@ -0,0 +1,147 @@
/*
* 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, apiPost, countryContinents, parseParams, regionNames, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.addButton = "add-button";
elements.datacenters = "datacenters";
elements.dcOther = "dc-other";
elements.hourly = "hourly";
elements.monthly = "monthly";
// Data recieved from API calls
var data = {};
data.linodes = [];
data.regions = [];
// Static references to UI elements
var ui = {};
ui.addButton = {};
ui.datacenters = {};
ui.dcOther = {};
ui.hourly = {};
ui.monthly = {};
// Callback for regions API call
var displayRegions = function(response)
{
// Add regions that support block storage to array
for (var i = 0; i < response.data.length; i++) {
if (response.data[i].capabilities.includes("NodeBalancers"))
data.regions.push(response.data[i]);
}
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), displayRegions, null);
return;
}
// Add regions to selector
for (var i = 0; i < data.regions.length; i++) {
// Regional pricing
if (data.regions[i].id == "id-cgk") {
data.regions[i].hourly = "$0.018/hr";
data.regions[i].monthly = "$12.00/mo";
} else if (data.regions[i].id == "br-gru") {
data.regions[i].hourly = "$0.021/hr";
data.regions[i].monthly = "$14.00/mo";
} else {
data.regions[i].hourly = "$0.015/hr";
data.regions[i].monthly = "$10.00/mo";
}
var loc = document.createElement("option");
loc.value = data.regions[i].id;
if (data.regions[i].label && data.regions[i].label.length)
loc.innerHTML = data.regions[i].label;
else if (regionNames[data.regions[i].id])
loc.innerHTML = regionNames[data.regions[i].id];
else
loc.innerHTML = data.regions[i].id;
var optgroup = null;
if (countryContinents[data.regions[i].country])
optgroup = document.getElementById(countryContinents[data.regions[i].country]);
if (!optgroup)
optgroup = ui.dcOther;
optgroup.style.display = "initial";
optgroup.appendChild(loc);
}
ui.addButton.disabled = false;
};
// Click handler for add button
var handleAdd = function(event)
{
if (event.currentTarget.disabled)
return;
var req = {
"region": ui.datacenters.value
};
if (data.params.tag)
req.tags = [data.params.tag];
apiPost("/nodebalancers", req, function(response)
{
location.href = "/nodebalancers/balancer?nbid=" + response.id;
});
};
// Location select handler
var handleLocation = function(event)
{
// Update prices based on location
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == ui.datacenters.value) {
ui.hourly.innerHTML = data.regions[i].hourly;
ui.monthly.innerHTML = data.regions[i].monthly;
break;
}
}
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
setupHeader();
// Get element references
ui.addButton = document.getElementById(elements.addButton);
ui.datacenters = document.getElementById(elements.datacenters);
ui.dcOther = document.getElementById(elements.dcOther);
ui.hourly = document.getElementById(elements.hourly);
ui.monthly = document.getElementById(elements.monthly);
// Attach event handlers
ui.addButton.addEventListener("click", handleAdd);
ui.datacenters.addEventListener("input", handleLocation);
// Get data from API
apiGet("/regions", displayRegions, null);
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,66 @@
<!--
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 - Add a NodeBalancer</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="add.css" />
<script src="add.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <span class="top-links-title">Add a NodeBalancer</span></div>
<div id="add">
<table class="lmc-table">
<thead>
<tr>
<td></td>
<td>Plan</td>
<td>Hourly</td>
<td>Monthly</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td><input checked type="radio" name="plan" /></td>
<td>NodeBalancer</td>
<td id="hourly">$0.015/hr</td>
<td id="monthly">$10.00/mo</td>
</tr>
</tbody>
</table>
<div id="location">
<h2>Location</h2>
<select id="datacenters">
<optgroup id="na" label="North America"></optgroup>
<optgroup id="eu" label="Europe"></optgroup>
<optgroup id="ap" label="Asia/Pacific"></optgroup>
<optgroup id="sa" label="South America"></optgroup>
<optgroup id="af" label="Africa"></optgroup>
<optgroup id="dc-other" label="Other"></optgroup>
</select>
</div>
<div id="submit">
<button disabled id="add-button" type="button">Add this NodeBalancer!</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,62 @@
/*
* 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');
canvas {
height: 323px;
}
.center-cell {
text-align: center;
}
#cxn-color {
background-color: #906;
}
#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;
}
#nodebalancer-details {
padding: 0px 15px 20px;
}
#settings-table tr td:first-of-type {
font-weight: bold;
text-align: right;
}
#settings-table tr td:not(:last-of-type) {
white-space: nowrap;
}
#traffic-in-color {
background-color: #03C;
}
#traffic-out-color {
background-color: #32CD32;
}

View File

@ -0,0 +1,411 @@
/*
* 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, apiPut, countSI, drawSeries, parseParams, regionNames, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.centerCell = "center-cell";
elements.configTable = "config-table";
elements.cxnAvg = "cxn-avg";
elements.cxnGraph = "cxn-graph";
elements.cxnLast = "cxn-last";
elements.cxnMax = "cxn-max";
elements.hostname = "hostname";
elements.inputLabel = "input-label";
elements.ipv4 = "ipv4";
elements.ipv6 = "ipv6";
elements.lmcRow = "lmc-tr1";
elements.lmcRowAlt = "lmc-tr2";
elements.loadingConfigs = "loading-configs";
elements.location = "location";
elements.nodebalancerLabel = "nodebalancer-label";
elements.nodebalancerTag = "nodebalancer-tag";
elements.nodebalancerTagLink = "nodebalancer-tag-link";
elements.removePrefix = "remove-config-";
elements.saveButton = "save-button";
elements.tableLabel = "table-label";
elements.tags = "tags";
elements.throttle = "throttle";
elements.trafficGraph = "traffic-graph";
elements.trafficInAvg = "traffic-in-avg";
elements.trafficInLast = "traffic-in-last";
elements.trafficInMax = "traffic-in-max";
elements.trafficOutAvg = "traffic-out-avg";
elements.trafficOutLast = "traffic-out-last";
elements.trafficOutMax = "traffic-out-max";
elements.transferred = "transferred";
// Data recieved from API calls
var data = {};
data.configs = [];
data.nodebalancer = {};
data.params = {};
data.regions = [];
data.stats = {};
// Static references to UI elements
var ui = {};
ui.configTable = {};
ui.cxnAvg = {};
ui.cxnGraph = {};
ui.cxnLast = {};
ui.cxnMax = {};
ui.hostname = {};
ui.inputLabel = {};
ui.ipv4 = {};
ui.ipv6 = {};
ui.loadingConfigs = {};
ui.location = {};
ui.nodebalancerLabel = {};
ui.nodebalancerTag = {};
ui.nodebalancerTagLink = {};
ui.saveButton = {};
ui.tableLabel = {};
ui.tags = {};
ui.throttle = {};
ui.trafficGraph = {};
ui.trafficInAvg = {};
ui.trafficInLast = {};
ui.trafficInMax = {};
ui.trafficOutAvg = {};
ui.trafficOutLast = {};
ui.trafficOutMax = {};
ui.transferred = {};
// Temporary state
var state = {};
state.haveNodebalancer = false;
state.haveRegions = false;
// Generate a configuration table row
var createConfigRow = function(config, alt)
{
var row = document.createElement("tr");
if (alt)
row.className = elements.lmcRowAlt;
else
row.className = elements.lmcRow;
var port = document.createElement("td");
var portLink = document.createElement("a");
portLink.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + config.id;
portLink.innerHTML = "Port " + config.port;
port.appendChild(portLink);
row.appendChild(port);
var protocol = document.createElement("td");
protocol.innerHTML = config.protocol.toUpperCase();
row.appendChild(protocol);
var algorithmStrings = {
"roundrobin": "Round Robin",
"leastconn": "Least Connections",
"source": "Source IP"
};
var algorithm = document.createElement("td");
if (algorithmStrings[config.algorithm])
algorithm.innerHTML = algorithmStrings[config.algorithm];
else
algorithm.innerHTML = config.algorithm;
row.appendChild(algorithm);
var stickinessStrings = {
"none": "None",
"table": "Table",
"http_cookie": "HTTP Cookie"
};
var stickiness = document.createElement("td");
if (stickinessStrings[config.stickiness])
stickiness.innerHTML = stickinessStrings[config.stickiness];
else
stickiness.innerHTML = config.stickiness;
row.appendChild(stickiness);
var healthStrings = {
"none": "None",
"connection": "TCP Connection",
"http": "HTTP Valid Status",
"http_body": "HTTP Body Regex"
};
var healthCheck = document.createElement("td");
if (healthStrings[config.check])
healthCheck.innerHTML = healthStrings[config.check];
else
healthCheck.innerHTML = config.check;
row.appendChild(healthCheck);
var status = document.createElement("td");
status.innerHTML = config.nodes_status.up + " up, " + config.nodes_status.down + " down";
row.appendChild(status);
var options = document.createElement("td");
options.className = elements.centerCell;
var editLink = document.createElement("a");
editLink.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + config.id;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.href = "#";
removeLink.id = elements.removePrefix + config.id;
removeLink.innerHTML = "Remove";
removeLink.addEventListener("click", handleConfigRemove);
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Callback for nodebalancer configs 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 pages
if (response.page != response.pages) {
apiGet("/nodebalancers/" + data.params.nbid + "/configs?page=" + (response.page + 1), displayConfigs, null);
return;
}
// Remove loading row
ui.loadingConfigs.remove();
// Insert configuration rows into table
for (var i = 0; i < data.configs.length; i++)
ui.configTable.appendChild(createConfigRow(data.configs[i], i % 2));
};
// Callback for nodebalancer API call
var displayNodebalancer = function(response)
{
data.nodebalancer = response;
// Set page title and header stuff
if (document.title.indexOf("//") == -1)
document.title += " // Edit " + data.nodebalancer.label;
ui.nodebalancerLabel.innerHTML = data.nodebalancer.label;
if (data.nodebalancer.tags.length == 1) {
ui.nodebalancerTagLink.href = "/nodebalancers?tag=" + data.nodebalancer.tags[0];
ui.nodebalancerTagLink.innerHTML = "(" + data.nodebalancer.tags[0] + ")";
ui.nodebalancerTag.style.display = "inline";
} else {
ui.nodebalancerTag.style.display = "none";
}
// Populate info
ui.tableLabel.innerHTML = data.nodebalancer.label;
ui.hostname.innerHTML = data.nodebalancer.hostname;
ui.ipv4.innerHTML = data.nodebalancer.ipv4;
ui.ipv6.innerHTML = data.nodebalancer.ipv6;
ui.transferred.innerHTML = countSI(data.nodebalancer.transfer.in * 1048576) + "B in - " + countSI(data.nodebalancer.transfer.out * 1048576) + "B out (" + countSI(data.nodebalancer.transfer.total * 1048576) + "B total)";
ui.inputLabel.value = data.nodebalancer.label;
ui.throttle.value = data.nodebalancer.client_conn_throttle;
ui.tags.value = data.nodebalancer.tags.join(",");
ui.saveButton.disabled = false;
state.haveNodebalancer = true;
if (state.haveRegions)
insertRegion();
};
// Callback for regions API call
var displayRegions = function(response)
{
// Add regions to array
data.regions = data.regions.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), getRegions, null);
return;
}
state.haveRegions = true;
if (state.haveNodebalancer)
insertRegion();
};
// Callback for nodebalancer stats API call
var displayStats = function(response)
{
// Insert dummy points in case of blank data
if (!response.data.connections.length)
response.data.connections = [[0,0]];
if (!response.data.traffic.in.length)
response.data.traffic.in = [[0,0]];
if (!response.data.traffic.out.length)
reponse.data.traffic.out = [[0,0]];
data.stats.cxn = [{
"color": "#906",
"fill": true,
"points": response.data.connections
}];
data.stats.traffic = [
{
"color": "#32CD32",
"fill": true,
"points": response.data.traffic.out
},
{
"color": "#03C",
"fill": false,
"points": response.data.traffic.in
}
];
// Draw graphs
drawSeries(data.stats.cxn, ui.cxnGraph);
drawSeries(data.stats.traffic, ui.trafficGraph);
// Update tables
ui.cxnMax.innerHTML = data.stats.cxn[0].max;
ui.cxnAvg.innerHTML = data.stats.cxn[0].avg.toFixed(2);
ui.cxnLast.innerHTML = data.stats.cxn[0].points[data.stats.cxn[0].points.length - 1][1];
ui.trafficOutMax.innerHTML = countSI(data.stats.traffic[1].max) + "b/s";
ui.trafficOutAvg.innerHTML = countSI(data.stats.traffic[1].avg) + "b/s";
ui.trafficOutLast.innerHTML = countSI(data.stats.traffic[1].points[data.stats.traffic[1].points.length - 1][1]) + "b/s";
ui.trafficInMax.innerHTML = countSI(data.stats.traffic[0].max) + "b/s";
ui.trafficInAvg.innerHTML = countSI(data.stats.traffic[0].avg) + "b/s";
ui.trafficInLast.innerHTML = countSI(data.stats.traffic[0].points[data.stats.traffic[0].points.length - 1][1]) + "b/s";
};
// Click handler for config remove link
var handleConfigRemove = function(event)
{
if (!confirm("Are you sure you want to delete this config?"))
return;
var nbcid = event.currentTarget.id.substring(elements.removePrefix.length);
apiDelete("/nodebalancers/" + data.params.nbid + "/configs/" + nbcid, function()
{
location.reload();
});
};
// Click handler for save button
var handleSave = function(event)
{
if (event.currentTarget.disabled)
return;
var req = {
"label": ui.inputLabel.value,
"client_conn_throttle": parseInt(ui.throttle.value),
"tags": []
};
if (ui.tags.value.length)
req.tags = ui.tags.value.split(",");
apiPut("/nodebalancers/" + data.params.nbid, req, function(response)
{
location.reload();
});
};
// Display region info
var insertRegion = function()
{
var regionData = null;
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == data.nodebalancer.region) {
regionData = data.regions[i];
break;
}
}
if (regionData && regionData.label && regionData.label.length)
ui.location.innerHTML = regionData.label;
else if (regionNames[data.nodebalancer.region])
ui.location.innerHTML = regionNames[data.nodebalancer.region];
else
ui.location.innerHTML = data.nodebalancer.region;
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need a NodeBalancer ID, so die if we don't have it
if (!data.params.nbid) {
alert("No NodeBalancer 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("nbid=0", "nbid=" + data.params.nbid);
// Get element references
ui.configTable = document.getElementById(elements.configTable);
ui.cxnAvg = document.getElementById(elements.cxnAvg);
ui.cxnGraph = document.getElementById(elements.cxnGraph);
ui.cxnLast = document.getElementById(elements.cxnLast);
ui.cxnMax = document.getElementById(elements.cxnMax);
ui.hostname = document.getElementById(elements.hostname);
ui.inputLabel = document.getElementById(elements.inputLabel);
ui.ipv4 = document.getElementById(elements.ipv4);
ui.ipv6 = document.getElementById(elements.ipv6);
ui.loadingConfigs = document.getElementById(elements.loadingConfigs);
ui.location = document.getElementById(elements.location);
ui.nodebalancerLabel = document.getElementById(elements.nodebalancerLabel);
ui.nodebalancerTag = document.getElementById(elements.nodebalancerTag);
ui.nodebalancerTagLink = document.getElementById(elements.nodebalancerTagLink);
ui.saveButton = document.getElementById(elements.saveButton);
ui.tableLabel = document.getElementById(elements.tableLabel);
ui.tags = document.getElementById(elements.tags);
ui.throttle = document.getElementById(elements.throttle);
ui.trafficGraph = document.getElementById(elements.trafficGraph);
ui.trafficInAvg = document.getElementById(elements.trafficInAvg);
ui.trafficInLast = document.getElementById(elements.trafficInLast);
ui.trafficInMax = document.getElementById(elements.trafficInMax);
ui.trafficOutAvg = document.getElementById(elements.trafficOutAvg);
ui.trafficOutLast = document.getElementById(elements.trafficOutLast);
ui.trafficOutMax = document.getElementById(elements.trafficOutMax);
ui.transferred = document.getElementById(elements.transferred);
// Attach event handlers
ui.saveButton.addEventListener("click", handleSave);
// Set graph resolutions
ui.cxnGraph.height = ui.cxnGraph.clientHeight;
ui.cxnGraph.width = ui.cxnGraph.clientWidth;
ui.trafficGraph.height = ui.trafficGraph.clientHeight;
ui.trafficGraph.width = ui.trafficGraph.clientWidth;
// Get data from the API
apiGet("/nodebalancers/" + data.params.nbid, displayNodebalancer, null);
apiGet("/regions", displayRegions, null);
apiGet("/nodebalancers/" + data.params.nbid + "/configs", displayConfigs, null);
apiGet("/nodebalancers/" + data.params.nbid + "/stats", displayStats, null);
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,162 @@
<!--
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 - NodeBalancers</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="balancer.css" />
<script src="balancer.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <span id="nodebalancer-tag"><a id="nodebalancer-tag-link" href=""></a> » </span><span id="nodebalancer-label" class="top-links-title"></span></div>
<div id="nodebalancer-details">
<table class="lmc-table">
<thead>
<tr>
<td colspan="7">Configurations</td>
</tr>
<tr>
<td>Port</td>
<td>Protocol</td>
<td>Algorithm</td>
<td>Session Stickiness</td>
<td>Health Check Method</td>
<td>Node Status</td>
<td class="center-cell">Options</td>
</tr>
</thead>
<tbody id="config-table">
<tr id="loading-configs" class="lmc-tr3">
<td colspan="7">Loading configurations...</td>
</tr>
</tbody>
</table>
<p class="sub-links"><a href="/nodebalancers/config?nbid=0&nbcid=0">Create Configuration</a></p>
<table class="lmc-table">
<thead>
<tr>
<td id="table-label" colspan="3"></td>
</tr>
<tr>
<td colspan="3">NodeBalancer Settings</td>
</tr>
</thead>
<tbody id="settings-table">
<tr class="lmc-tr3">
<td>Hostname</td>
<td id="hostname"></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>IPv4 Address</td>
<td id="ipv4"></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>IPv6 Address</td>
<td id="ipv6"></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>Location</td>
<td id="location"></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>Transferred this Month</td>
<td id="transferred"></td>
<td></td>
</tr>
<tr class="lmc-tr3">
<td>NodeBalancer Label</td>
<td><input id="input-label" type="text" value="" size="24" /></td>
<td class="info">Rename your NodeBalancer</td>
</tr>
<tr class="lmc-tr3">
<td>Client Connection Throttle</td>
<td><input id="throttle" type="number" min="0" max="20" value="0" size="4" /></td>
<td class="info">To help mitigate abuse, throttle connections from a single client IP to this number per second. 0 to disable.</td>
</tr>
<tr class="lmc-tr3">
<td>Tags</td>
<td><input id="tags" type="text" size="24" /> (comma-separated)</td>
<td class="info">Group NodeBalancers together on the NodeBalancers tab using tags!</td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td><button disabled id="save-button" type="button">Save Changes</button></td>
<td></td>
</tr>
</tbody>
</table>
<h3>Graphs</h3>
<div class="lmc-graph">
<h4>Connections (CXN/s) - Last 24 Hours</h4>
<canvas id="cxn-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="cxn-color" class="lmc-graph-color"></div> Connections</td>
<td id="cxn-max"></td>
<td id="cxn-avg"></td>
<td id="cxn-last"></td>
</tr>
</tbody>
</table>
<h4>Traffic (bits/s) - Last 24 Hours</h4>
<canvas id="traffic-graph"></canvas>
<table>
<thead>
<tr>
<td></td>
<td>Max</td>
<td>Avg</td>
<td>Last</td>
</tr>
</thead>
<tbody>
<tr>
<td><div id="traffic-out-color" class="lmc-graph-color"></div> Outgoing</td>
<td id="traffic-out-max"></td>
<td id="traffic-out-avg"></td>
<td id="traffic-out-last"></td>
</tr>
<tr>
<td><div id="traffic-in-color" class="lmc-graph-color"></div> Incoming</td>
<td id="traffic-in-max"></td>
<td id="traffic-in-avg"></td>
<td id="traffic-in-last"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,43 @@
/*
* 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');
.center-cell {
text-align: center;
}
.check-show {
display: none;
}
#config {
padding: 0px 15px 15px;
}
#config-table tbody:not(.lmc-tbody-head) tr td:first-of-type {
font-weight: bold;
text-align: right;
}
#config-table tbody:not(.lmc-tbody-head) tr td:not(:last-of-type) {
white-space: nowrap;
}
.protocol-show {
display: none;
}

View File

@ -0,0 +1,394 @@
/*
* 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, apiPut, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.algorithm = "algorithm";
elements.centerCell = "center-cell";
elements.checkAttempts = "check-attempts";
elements.checkBody = "check-body";
elements.checkInterval = "check-interval";
elements.checkPath = "check-path";
elements.checkShow = "check-show";
elements.checkTimeout = "check-timeout";
elements.checkType = "check-type";
elements.cipherSuite = "cipher-suite";
elements.commonName = "common-name";
elements.configLabel = "config-label";
elements.fingerprint = "fingerprint";
elements.lmcRow = "lmc-tr1";
elements.lmcRowAlt = "lmc-tr2";
elements.nodebalancerLabel = "nodebalancer-label";
elements.nodebalancerTag = "nodebalancer-tag";
elements.nodebalancerTagLink = "nodebalancer-tag-link";
elements.nodesTable = "nodes-table";
elements.nonzero = "nonzero";
elements.passive = "passive";
elements.port = "port";
elements.protocol = "protocol";
elements.protocolShow = "protocol-show";
elements.proxyProtocol = "proxy-protocol";
elements.removePrefix = "remove-node-";
elements.replaceCert = "replace-cert";
elements.replaceCertOnly = "replace-cert-only";
elements.replaceLink = "replace-link";
elements.saveButton = "save-button";
elements.sslCert = "ssl-cert";
elements.sslKey = "ssl-key";
elements.stickiness = "stickiness";
// Data recieved from API calls
var data = {};
data.config = {};
data.nodebalancer = {};
data.nodes = [];
// Static references to UI elements
var ui = {};
ui.algorithm = {};
ui.checkAttempts = {};
ui.checkBody = {};
ui.checkInterval = {};
ui.checkPath = {};
ui.checkShow = [];
ui.checkTimeout = {};
ui.checkType = {};
ui.cipherSuite = {};
ui.commonName = {};
ui.configLabel = {};
ui.fingerprint = {};
ui.nodebalancerLabel = {};
ui.nodebalancerTag = {};
ui.nodebalancerTagLink = {};
ui.nodesTable = {};
ui.passive = {};
ui.port = {};
ui.protocol = {};
ui.protocolShow = [];
ui.proxyProtocol = {};
ui.saveButton = {};
ui.replaceLink = {};
ui.sslCert = {};
ui.sslKey = {};
ui.stickiness = {};
// Temporary State
var state = {};
state.replaceCert = true;
// Create a row for the nodes table
var createNodeRow = function(node, alt)
{
var row = document.createElement("tr");
if (alt)
row.className = elements.lmcRowAlt;
else
row.className = elements.lmcRow;
var label = document.createElement("td");
label.innerHTML = node.label;
row.appendChild(label);
var address = document.createElement("td");
var lastColon = node.address.lastIndexOf(":");
address.innerHTML = node.address.slice(0, lastColon);
row.appendChild(address);
var port = document.createElement("td");
port.innerHTML = node.address.slice(lastColon + 1);
row.appendChild(port);
var weight = document.createElement("td");
weight.innerHTML = node.weight;
row.appendChild(weight);
var mode = document.createElement("td");
mode.innerHTML = node.mode.charAt(0).toUpperCase() + node.mode.slice(1);
row.appendChild(mode);
var status = document.createElement("td");
status.innerHTML = node.status;
row.appendChild(status);
var options = document.createElement("td");
options.className = elements.centerCell;
var editLink = document.createElement("a");
editLink.href = "/nodebalancers/node?nbid=" + data.params.nbid + "&nbcid=" + data.params.nbcid + "&nbnid=" + node.id;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.id = elements.removePrefix + node.id;
removeLink.href = "#";
removeLink.innerHTML = "Remove";
removeLink.addEventListener("click", handleRemoveNode);
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Callback for config API call
var displayConfig = function(response)
{
data.config = response;
ui.configLabel.innerHTML = "Port " + data.config.port;
ui.port.value = data.config.port;
ui.protocol.value = data.config.protocol;
ui.proxyProtocol.value = data.config.proxy_protocol;
ui.algorithm.value = data.config.algorithm;
ui.stickiness.value = data.config.stickiness;
ui.commonName.innerHTML = data.config.ssl_commonname;
ui.fingerprint.innerHTML = data.config.ssl_fingerprint;
ui.cipherSuite.value = data.config.cipher_suite;
ui.checkType.value = data.config.check;
ui.checkInterval.value = data.config.check_interval;
ui.checkTimeout.value = data.config.check_timeout;
ui.checkAttempts.value = data.config.check_attempts;
ui.checkPath.value = data.config.check_path;
ui.checkBody.value = data.config.check_body;
ui.passive.checked = data.config.check_passive;
state.replaceCert = (!data.config.ssl_key || data.config.ssl_key.indexOf("<REDACTED>") == -1);
ui.saveButton.disabled = false;
showHideChecks();
showHideProtocol();
};
// Callback for nodebalancer API call
var displayNodebalancer = function(response)
{
data.nodebalancer = response;
// Set page title and header stuff
if (document.title.indexOf("//") == -1)
document.title += " // Edit " + data.nodebalancer.label;
ui.nodebalancerLabel.innerHTML = data.nodebalancer.label;
if (data.nodebalancer.tags.length == 1) {
ui.nodebalancerTagLink.href = "/nodebalancers?tag=" + data.nodebalancer.tags[0];
ui.nodebalancerTagLink.innerHTML = "(" + data.nodebalancer.tags[0] + ")";
ui.nodebalancerTag.style.display = "inline";
} else {
ui.nodebalancerTag.style.display = "none";
}
};
// Callback for nodes API call
var displayNodes = function(response)
{
data.nodes = data.nodes.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes?page=" + (response.page + 1), displayNodes, null);
return;
}
// Insert nodes into table
for (var i = 0; i < data.nodes.length; i++)
ui.nodesTable.appendChild(createNodeRow(data.nodes[i], i % 2));
};
// Remove node handler
var handleRemoveNode = function(event)
{
if (!confirm("Are you sure you want to remove this node?"))
return;
var nbnid = event.currentTarget.id.substring(elements.removePrefix.length);
apiDelete("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes/" + nbnid, function()
{
location.reload();
});
};
// Replace link handler
var handleReplace = function(event)
{
state.replaceCert = true;
showHideProtocol();
};
// Save button handler
var handleSave = function(event)
{
if (event.currentTarget.disabled)
return;
var req = {
"port": parseInt(ui.port.value),
"protocol": ui.protocol.value,
"algorithm": ui.algorithm.value,
"stickiness": ui.stickiness.value,
"check": ui.checkType.value,
"check_passive": ui.passive.checked
};
if (ui.protocol.value == "tcp") {
req.proxy_protocol = ui.proxyProtocol.value;
} else if (ui.protocol.value == "https") {
req.cipher_suite = ui.cipherSuite.value;
if (state.replaceCert) {
req.ssl_cert = ui.sslCert.value;
req.ssl_key = ui.sslKey.value;
}
}
switch (ui.checkType.value) {
case "http_body":
req.check_body = ui.checkBody.value;
case "http":
req.check_path = ui.checkPath.value;
case "connection":
req.check_interval = parseInt(ui.checkInterval.value);
req.check_timeout = parseInt(ui.checkTimeout.value);
req.check_attempts = parseInt(ui.checkAttempts.value);
}
if (data.params.nbcid == 0) {
apiPost("/nodebalancers/" + data.params.nbid + "/configs", req, function(response)
{
location.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + response.id;
});
} else {
apiPut("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid, req, function(response)
{
location.href = "/nodebalancers/balancer?nbid=" + data.params.nbid;
});
}
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need a NodeBalancer ID, so die if we don't have it
if (!data.params.nbid) {
alert("No NodeBalancer ID supplied!");
return;
}
// We also need a config ID
if (!data.params.nbcid) {
alert("No config 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("nbid=0", "nbid=" + data.params.nbid);
anchors[i].href = anchors[i].href.replace("nbcid=0", "nbcid=" + data.params.nbcid);
}
// Get element references
ui.algorithm = document.getElementById(elements.algorithm);
ui.checkAttempts = document.getElementById(elements.checkAttempts);
ui.checkBody = document.getElementById(elements.checkBody);
ui.checkInterval = document.getElementById(elements.checkInterval);
ui.checkPath = document.getElementById(elements.checkPath);
ui.checkShow = document.getElementsByClassName(elements.checkShow);
ui.checkTimeout = document.getElementById(elements.checkTimeout);
ui.checkType = document.getElementById(elements.checkType);
ui.cipherSuite = document.getElementById(elements.cipherSuite);
ui.commonName = document.getElementById(elements.commonName);
ui.configLabel = document.getElementById(elements.configLabel);
ui.fingerprint = document.getElementById(elements.fingerprint);
ui.nodebalancerLabel = document.getElementById(elements.nodebalancerLabel);
ui.nodebalancerTag = document.getElementById(elements.nodebalancerTag);
ui.nodebalancerTagLink = document.getElementById(elements.nodebalancerTagLink);
ui.nodesTable = document.getElementById(elements.nodesTable);
ui.passive = document.getElementById(elements.passive);
ui.port = document.getElementById(elements.port);
ui.protocol = document.getElementById(elements.protocol);
ui.protocolShow = document.getElementsByClassName(elements.protocolShow);
ui.proxyProtocol = document.getElementById(elements.proxyProtocol);
ui.replaceLink = document.getElementById(elements.replaceLink);
ui.saveButton = document.getElementById(elements.saveButton);
ui.sslCert = document.getElementById(elements.sslCert);
ui.sslKey = document.getElementById(elements.sslKey);
ui.stickiness = document.getElementById(elements.stickiness);
// Register event handlers
ui.checkType.addEventListener("input", showHideChecks);
ui.protocol.addEventListener("input", showHideProtocol);
ui.replaceLink.addEventListener("click", handleReplace);
ui.saveButton.addEventListener("click", handleSave);
// Get data from API
apiGet("/nodebalancers/" + data.params.nbid, displayNodebalancer, null);
if (parseInt(data.params.nbcid)) {
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid, displayConfig, null);
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes", displayNodes, null);
} else {
ui.configLabel.innerHTML = "Create Configuration";
ui.saveButton.disabled = false;
var hideElements = document.getElementsByClassName(elements.nonzero);
for (var i = 0; i < hideElements.length; i++)
hideElements[i].style.display = "none";
}
};
// Show/hide check rows
var showHideChecks = function(event)
{
for (var i = 0; i < ui.checkShow.length; i++) {
if (ui.checkShow[i].classList.contains(elements.checkShow + "-" + ui.checkType.value))
ui.checkShow[i].style.display = "table-row";
else
ui.checkShow[i].style.display = "none";
}
};
// Show/hide protocol stuff
var showHideProtocol = function(event)
{
for (var i = 0; i < ui.protocolShow.length; i++) {
if (ui.protocolShow[i].classList.contains(elements.protocolShow + "-" + ui.protocol.value)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCert)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCertOnly) == state.replaceCert)
ui.protocolShow[i].style.display = "table-row";
else
ui.protocolShow[i].style.display = "none";
} else {
ui.protocolShow[i].style.display = "table-row";
}
} else {
ui.protocolShow[i].style.display = "none";
}
}
if (data.params.nbcid == 0 && ui.protocol.value == "http")
ui.port.value = 80;
if (data.params.nbcid == 0 && ui.protocol.value == "https")
ui.port.value = 443;
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,222 @@
<!--
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 - NodeBalancers</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="config.css" />
<script src="config.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <span id="nodebalancer-tag"><a id="nodebalancer-tag-link" href=""></a> » </span><a id="nodebalancer-label" href="/nodebalancers/balancer?nbid=0"></a> » <span id="config-label" class="top-links-title"></span></div>
<div id="config">
<table class="lmc-table nonzero">
<thead>
<tr>
<td colspan="7">Nodes</td>
</tr>
<tr>
<td>Label</td>
<td>Address</td>
<td>Port</td>
<td>Weight</td>
<td>Mode</td>
<td>Status</td>
<td class="center-cell">Options</td>
</tr>
</thead>
<tbody id="nodes-table"></tbody>
</table>
<p class="sub-links nonzero"><a href="/nodebalancers/node?nbid=0&nbcid=0&nbnid=0">Add Node</a></p>
<table id="config-table" class="lmc-table">
<thead>
<tr>
<td colspan="3">Edit Configuration</td>
</tr>
<tr>
<td colspan="3">Configuration Settings</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td>Port</td>
<td><input id="port" type="number" min="1" max="65535" value="80" size="4" /></td>
<td class="info">Listen on this port</td>
</tr>
<tr class="lmc-tr3">
<td>Protocol</td>
<td>
<select id="protocol">
<option value="tcp">TCP</option>
<option selected value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</td>
<td></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-tcp">
<td>Proxy Protocol</td>
<td>
<select id="proxy-protocol">
<option selected value="none">None</option>
<option value="v1">v1</option>
<option value="v2">v2</option>
</select>
</td>
<td class="info">Proxy protocol preserves initial TCP connection information. Consult the <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/proxy-protocol/">Proxy Protocol guide</a> for information on the differences between each option.</td>
</tr>
<tr class="lmc-tr3">
<td>Algorithm</td>
<td>
<select id="algorithm">
<option selected value="roundrobin">Round Robin</option>
<option value="leastconn">Least Connections</option>
<option value="source">Source IP</option>
</select>
</td>
<td class="info">Roundrobin assigns connections to each backend sequentially. Least connections choose the backend with the least number of current connections. Source uses the client's IPv4 address</td>
</tr>
<tr class="lmc-tr3">
<td>Session Stickiness</td>
<td>
<select id="stickiness">
<option value="none">None</option>
<option selected value="table">Table</option>
<option value="http_cookie">HTTP Cookie</option>
</select>
</td>
<td class="info">Route subsequent requests from a client to the same backend</td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr class="protocol-show protocol-show-https">
<td colspan="3">SSL Settings</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td>Common Name</td>
<td id="common-name"></td>
<td class="info">Need help with SSL? - <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/configure/">NodeBalancer Reference Guide</a></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td>Fingerprint</td>
<td id="fingerprint" colspan="2"></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https">
<td>SSL Cipher Suite</td>
<td>
<select id="cipher-suite">
<option selected value="recommended">Recommended</option>
<option value="legacy">Legacy</option>
</select>
</td>
<td class="info">Select 'recommended' to use only strong encryption. Select 'legacy' to enable weak encryption that supports older browsers. You really want to select 'recommended'. <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/configure/">NodeBalancer Reference Guide</a></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td></td>
<td><a id="replace-link" href="#">Replace Certificate</a></td>
<td></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert replace-cert-only">
<td>SSL Certificate</td>
<td colspan="2"><textarea id="ssl-cert" rows="6" cols="64" placeholder="Please provide your SSL certificate (including chained intermediate certificates if needed)"></textarea></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert replace-cert-only">
<td>Private Key</td>
<td colspan="2"><textarea id="ssl-key" rows="6" cols="64" placeholder="Please provide your unpassphrased SSL private key"></textarea></td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Active Health Check</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Health Check Type</td>
<td>
<select id="check-type">
<option selected value="none">None</option>
<option value="connection">TCP Connection</option>
<option value="http">HTTP Valid Status</option>
<option value="http_body">HTTP Body Regex</option>
</select>
</td>
<td class="info">Active health checks proactively check the health of the backend nodes. 'TCP Connection' requires a successful TCP handshake. 'HTTP Valid Status' requires a 2xx or 3xx response from the backend node. 'HTTP Body Regex' uses a regex to match against an expected result body.</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Interval</td>
<td><input id="check-interval" type="number" min="1" value="5" size="4" /></td>
<td class="info">Seconds between health check probes</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Timeout</td>
<td><input id="check-timeout" type="number" min="1" max="30" value="3" size="4" /></td>
<td class="info">Seconds to wait before considering the probe a failure. 1-30. Must be less than check interval.</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Attempts</td>
<td><input id="check-attempts" type="number" min="1" max="30" value="2" size="4" /></td>
<td class="info">Number of failed probes before taking a node out of rotation. 1-30</td>
</tr>
<tr class="lmc-tr3 check-show check-show-http check-show-http_body">
<td>Check HTTP Path</td>
<td><input id="check-path" type="text" value="/" size="24" /></td>
<td class="info">When check=http, the path to request</td>
</tr>
<tr class="lmc-tr3 check-show check-show-http_body">
<td>Expected HTTP Body</td>
<td><textarea id="check-body" rows="4" cols="40"></textarea></td>
<td class="info">When check=http, a regex to match within the first 16KB of the response body</td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Passive Checks</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Enabled</td>
<td><input checked id="passive" type="checkbox" /></td>
<td class="info">Enable passive checks based on observing communication with backend nodes.</td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td><button disabled id="save-button" type="button">Save Changes</button></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

41
nodebalancers/index.shtml Normal file
View File

@ -0,0 +1,41 @@
<!--
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 - NodeBalancers</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="nodebalancers.css" />
<script src="nodebalancers.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="nodebalancers">
<span id="loading">Loading...</span>
</div>
<div id="transfer-pool">
<p id="transfer-header">This Month's Network Transfer Pool</p>
<div id="transfer-bar">
<div id="bar-used"></div><div id="bar-remaining"></div>
</div>
<p id="transfer-details"><span id="transfer-used"></span> Used, <span id="transfer-remaining"></span> Remaining, <span id="transfer-quota"></span> Quota</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,87 @@
<!--
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 - NodeBalancers</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="node.css" />
<script src="node.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <span id="nodebalancer-tag"><a id="nodebalancer-tag-link" href=""></a> » </span><a id="nodebalancer-label" href="/nodebalancers/balancer?nbid=0"></a> » <a id="config-label" href="/nodebalancers/config?nbid=0&nbcid=0">Port</a> » <span class="top-links-title">Edit Node</span></div>
<div id="node">
<table class="lmc-table">
<thead>
<tr>
<td colspan="3">Edit Node</td>
</tr>
<tr>
<td colspan="3">Node Settings</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td>Label</td>
<td><input id="label" type="text" size="30" /></td>
<td class="info">This backend node's label</td>
</tr>
<tr class="lmc-tr3">
<td>Address</td>
<td>
<select id="address">
<option selected disabled value="0">Select A Linode</option>
</select>
</td>
<td class="info">The private IP address of the backend Linode instance</td>
</tr>
<tr class="lmc-tr3">
<td>Port</td>
<td><input id="port" type="number" min="1" max="65535" size="4" /></td>
<td class="info">The backend port for this node</td>
</tr>
<tr class="lmc-tr3">
<td>Weight</td>
<td><input id="weight" type="number" min="1" max="255" value="100" size="4" /></td>
<td class="info">Load balancing weight, 1-255. Higher means more connections.</td>
</tr>
<tr class="lmc-tr3">
<td>Mode</td>
<td>
<select id="mode">
<option value="accept">Accept</option>
<option value="reject">Reject</option>
<option value="backup">Backup</option>
<option value="drain">Drain</option>
</select>
</td>
<td class="info">The connections mode for this node</td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td><button disabled id="save-button" type="button">Save Changes</button></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,27 @@
/*
* 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');
#node {
padding: 0px 15px 15px;
}
tbody tr td:first-of-type {
font-weight: bold;
text-align: right;
}

255
nodebalancers/node/node.js Normal file
View File

@ -0,0 +1,255 @@
/*
* 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, apiPost, apiPut, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.address = "address";
elements.configLabel = "config-label";
elements.label = "label";
elements.mode = "mode";
elements.nodebalancerLabel = "nodebalancer-label";
elements.nodebalancerTag = "nodebalancer-tag";
elements.nodebalancerTagLink = "nodebalancer-tag-link";
elements.port = "port";
elements.saveButton = "save-button";
elements.weight = "weight";
// Data recieved from API calls
var data = {};
data.config = {};
data.linodeFilters = {
"ipv4": {
"+contains": "192.168"
}
};
data.linodes = [];
data.nodebalancer = {};
data.node = {};
// Static references to UI elements
var ui = {};
ui.address = {};
ui.configLabel = {};
ui.label = {};
ui.mode = {};
ui.nodebalancerLabel = {};
ui.nodebalancerTag = {};
ui.nodebalancerTagLink = {};
ui.port = {};
ui.saveButton = {};
ui.weight = {};
// Callback for config API call
var displayConfig = function(response)
{
data.config = response;
ui.configLabel.innerHTML = "Port " + data.config.port;
if (!parseInt(data.params.nbnid))
ui.port.value = data.config.port;
};
// Callback for linodes API call
var displayLinodes = function(response)
{
data.linodes = data.linodes.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/linode/instances?page=" + (response.page + 1), displayLinodes, data.linodeFilters);
return;
}
// Add linodes to address selector
for (var i = 0; i < data.linodes.length; i++) {
var ipIndex = -1;
for (var j = 0; j < data.linodes[i].ipv4.length; j++) {
if (data.linodes[i].ipv4[j].indexOf("192.168.") == 0) {
ipIndex = j;
break;
}
}
if (ipIndex == -1)
continue;
var option = document.createElement("option");
option.value = data.linodes[i].ipv4[j];
option.innerHTML = data.linodes[i].ipv4[j] + " (" + data.linodes[i].label + ")";
ui.address.appendChild(option);
}
if (parseInt(data.params.nbnid))
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes/" + data.params.nbnid, displayNode, null);
else
ui.saveButton.disabled = false;
};
// Callback for nodebalancer API call
var displayNodebalancer = function(response)
{
data.nodebalancer = response;
// Set page title and header stuff
if (document.title.indexOf("//") == -1)
document.title += " // Edit " + data.nodebalancer.label;
ui.nodebalancerLabel.innerHTML = data.nodebalancer.label;
if (data.nodebalancer.tags.length == 1) {
ui.nodebalancerTagLink.href = "/nodebalancers?tag=" + data.nodebalancer.tags[0];
ui.nodebalancerTagLink.innerHTML = "(" + data.nodebalancer.tags[0] + ")";
ui.nodebalancerTag.style.display = "inline";
} else {
ui.nodebalancerTag.style.display = "none";
}
// Get linodes
data.linodeFilters.region = data.nodebalancer.region;
apiGet("/linode/instances", displayLinodes, data.linodeFilters);
};
// Callback for node API call
var displayNode = function(response)
{
data.node = response;
ui.label.value = data.node.label;
var lastColon = data.node.address.lastIndexOf(":");
ui.address.value = data.node.address.slice(0, lastColon);
ui.port.value = data.node.address.slice(lastColon + 1);
ui.weight.value = data.node.weight;
ui.mode.value = data.node.mode;
ui.saveButton.disabled = false;
};
// Save button handler
var handleSave = function(event)
{
if (event.currentTarget.disabled)
return;
if (ui.address.value == "0")
return;
var req = {
"label": ui.label.value,
"address": ui.address.value + ":" + ui.port.value,
"weight": parseInt(ui.weight.value),
"mode": ui.mode.value
};
if (data.params.nbnid == 0) {
apiPost("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes", req, function(response)
{
location.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + data.params.nbcid;
});
} else {
apiPut("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes/" + data.params.nbnid, req, function(response)
{
location.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + data.params.nbcid;
});
}
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need a NodeBalancer ID, so die if we don't have it
if (!data.params.nbid) {
alert("No NodeBalancer ID supplied!");
return;
}
// We also need a config ID
if (!data.params.nbcid) {
alert("No config ID supplied!");
return;
}
// We also need a node ID
if (!data.params.nbnid) {
alert("No node 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("nbid=0", "nbid=" + data.params.nbid);
anchors[i].href = anchors[i].href.replace("nbcid=0", "nbcid=" + data.params.nbcid);
}
// Get element references
ui.address = document.getElementById(elements.address);
ui.configLabel = document.getElementById(elements.configLabel);
ui.label = document.getElementById(elements.label);
ui.mode = document.getElementById(elements.mode);
ui.nodebalancerLabel = document.getElementById(elements.nodebalancerLabel);
ui.nodebalancerTag = document.getElementById(elements.nodebalancerTag);
ui.nodebalancerTagLink = document.getElementById(elements.nodebalancerTagLink);
ui.port = document.getElementById(elements.port);
ui.saveButton = document.getElementById(elements.saveButton);
ui.weight = document.getElementById(elements.weight);
// Register event handlers
ui.saveButton.addEventListener("click", handleSave);
// Get data from API
apiGet("/nodebalancers/" + data.params.nbid, displayNodebalancer, null);
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid, displayConfig, null);
};
// Show/hide check rows
var showHideChecks = function(event)
{
for (var i = 0; i < ui.checkShow.length; i++) {
if (ui.checkShow[i].classList.contains(elements.checkShow + "-" + ui.checkType.value))
ui.checkShow[i].style.display = "table-row";
else
ui.checkShow[i].style.display = "none";
}
};
// Show/hide protocol stuff
var showHideProtocol = function(event)
{
for (var i = 0; i < ui.protocolShow.length; i++) {
if (ui.protocolShow[i].classList.contains(elements.protocolShow + "-" + ui.protocol.value)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCert)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCertOnly) == state.replaceCert)
ui.protocolShow[i].style.display = "table-row";
else
ui.protocolShow[i].style.display = "none";
} else {
ui.protocolShow[i].style.display = "table-row";
}
} else {
ui.protocolShow[i].style.display = "none";
}
}
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,64 @@
/*
* 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');
#transfer-bar {
border: 3px solid #999;
margin: 0 auto;
width: 70%;
}
#bar-remaining {
background-color: #EFEFEF;
border: 2px solid #EFEFEF;
display: none;
font-size: 16px;
line-height: 25px;
text-align: center;
white-space: nowrap;
}
#bar-used {
background-color: #ADD370;
border: 2px dashed #008000;
display: none;
font-size: 16px;
line-height: 25px;
text-align: center;
white-space: nowrap;
}
.center-cell {
text-align: center;
}
#nodebalancers {
padding: 15px;
}
#transfer-details {
margin: 5px 0px;
padding-bottom: 15px;
text-align: center;
}
#transfer-header {
font-weight: bold;
margin: 50px 0px 5px;
text-align: center;
}

View File

@ -0,0 +1,366 @@
/*
* 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, regionNames, apiGet, countSI, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.barRemaining = "bar-remaining";
elements.barUsed = "bar-used";
elements.centerCell = "center-cell";
elements.info = "info";
elements.lmcRow = "lmc-tr1";
elements.lmcRowAlt = "lmc-tr2";
elements.lmcTable = "lmc-table";
elements.loading = "loading";
elements.nodebalancerPortsPrefix = "nodebalancer-ports-";
elements.nodebalancers = "nodebalancers";
elements.nodebalancerStatusPrefix = "nodebalancer-nodestatus-";
elements.nodebalancerTagPrefix = "nodebalancer-tag-";
elements.subLinks = "sub-links";
elements.transferQuota = "transfer-quota";
elements.transferRemaining = "transfer-remaining";
elements.transferUsed = "transfer-used";
// Data recieved from API calls
var data = {};
data.params = {};
data.nodebalancers = [];
data.nodebalancerTags = [];
data.noTag = false;
data.regions = [];
// Static references to UI elements
var ui = {};
ui.barRemaining = {};
ui.barUsed = {};
ui.loading = {};
ui.nodebalancers = {};
ui.nodebalancerTables = {};
ui.transferQuota = {};
ui.transferRemaining = {};
ui.transferUsed = {};
// Temporary state
var state = {};
state.haveNodebalancers = false;
state.haveRegions = false;
var createNodebalancerRow = function(nodebalancer, alt)
{
var row = document.createElement("tr");
if (alt)
row.className = elements.lmcRowAlt;
else
row.className = elements.lmcRow;
var name = document.createElement("td");
var nameLink = document.createElement("a");
nameLink.href = "/nodebalancers/balancer?nbid=" + nodebalancer.id;
nameLink.innerHTML = nodebalancer.label;
name.appendChild(nameLink);
var regionData = null;
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == nodebalancer.region) {
regionData = data.regions[i];
break;
}
}
var region = document.createElement("td");
if (regionData && regionData.label && regionData.label.length)
region.innerHTML = regionData.label;
else if (regionNames[nodebalancer.region])
region.innerHTML = regionNames[nodebalancer.region];
else
region.innerHTML = nodebalancer.region;
var ip = document.createElement("td");
ip.innerHTML = nodebalancer.ipv4;
var ports = document.createElement("td");
ports.id = elements.nodebalancerPortsPrefix + nodebalancer.id;
var nodeStatus = document.createElement("td");
nodeStatus.id = elements.nodebalancerStatusPrefix + nodebalancer.id;
var transferred = document.createElement("td");
transferred.innerHTML = countSI(nodebalancer.transfer.total * 1048576) + "B";
var options = document.createElement("td");
options.className = elements.centerCell;
var editLink = document.createElement("a");
editLink.href = "/nodebalancers/balancer?nbid=" + nodebalancer.id;
editLink.innerHTML = "Edit";
var optionsSeparator = document.createElement("span");
optionsSeparator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.href = "/nodebalancers/remove?nbid=" + nodebalancer.id;
removeLink.innerHTML = "Remove";
options.appendChild(editLink);
options.appendChild(optionsSeparator);
options.appendChild(removeLink);
row.appendChild(name);
row.appendChild(region);
row.appendChild(ip);
row.appendChild(ports);
row.appendChild(nodeStatus);
row.appendChild(transferred);
row.appendChild(options);
return row;
};
var createNodebalancerTable = function(tag)
{
var table = document.createElement("table");
table.id = elements.nodebalancerTagPrefix + tag;
table.className = elements.lmcTable;
var thead = document.createElement("thead");
var headRow1 = document.createElement("tr");
var title = document.createElement("td");
if (tag.length == 0)
title.innerHTML = "NodeBalancers";
else
title.innerHTML = tag;
headRow1.appendChild(title);
var headRow2 = document.createElement("tr");
var cells = ["Label", "Location", "IP", "Ports", "Node Status", "Transferred", "Options"];
title.colSpan = cells.length;
for (var i = 0; i < cells.length; i++) {
var cell = document.createElement("td");
if (cells[i] == "Options")
cell.className = elements.centerCell;
cell.innerHTML = cells[i];
headRow2.appendChild(cell);
}
thead.appendChild(headRow1);
thead.appendChild(headRow2);
var tbody = document.createElement("tbody");
table.appendChild(thead);
table.appendChild(tbody);
ui.nodebalancerTables[tag] = tbody;
var subLinks = document.createElement("p");
subLinks.className = elements.subLinks;
var addNodebalancer = document.createElement("a");
addNodebalancer.href = "/nodebalancers/add";
if (tag.length > 0)
addNodebalancer.href += "?tag=" + tag;
addNodebalancer.innerHTML = "Add a NodeBalancer";
subLinks.appendChild(addNodebalancer);
ui.nodebalancers.appendChild(table);
ui.nodebalancers.appendChild(subLinks);
};
// Callback for NB configs API call
var displayConfigs = function(response)
{
// Find the index of this NB in the array
var nbid = parseInt(response['_endpoint'].split("/")[2]);
var nbindex = -1;
for (var i = 0; i < data.nodebalancers.length; i++) {
if (data.nodebalancers[i].id == nbid) {
nbindex = i;
break;
}
}
if (nbindex == -1)
return;
// Add configs to object
data.nodebalancers[nbindex].configs = data.nodebalancers[nbindex].configs.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/nodebalancers/" + nbid + "/configs?page=" + (response.page + 1), displayConfigs, null);
return;
}
var ports = document.getElementById(elements.nodebalancerPortsPrefix + nbid);
var status = document.getElementById(elements.nodebalancerStatusPrefix + nbid);
var upTotal = 0;
var downTotal = 0;
// Count the backend totals and insert port/config links
if (!data.nodebalancers[nbindex].configs.length) {
var addLink = document.createElement("a");
addLink.href = "/nodebalancers/config?nbid=" + nbid + "&nbcid=0";
addLink.innerHTML = "Add...";
ports.appendChild(addLink);
}
for (var i = 0; i < data.nodebalancers[nbindex].configs.length; i++) {
upTotal += data.nodebalancers[nbindex].configs[i].nodes_status.up;
downTotal += data.nodebalancers[nbindex].configs[i].nodes_status.down;
if (i > 0) {
var separator = document.createElement("span");
separator.innerHTML = ", ";
ports.appendChild(separator);
}
var port = document.createElement("a");
port.href = "/nodebalancers/config?nbid=" + nbid + "&nbcid=" + data.nodebalancers[nbindex].configs[i].id;
port.innerHTML = data.nodebalancers[nbindex].configs[i].port;
ports.appendChild(port);
}
status.innerHTML = upTotal + " up, " + downTotal + " down";
};
var displayNodebalancers = function(response)
{
// Add linodes to array
data.nodebalancers = data.nodebalancers.concat(response.data);
// Add new tags to array
for (var i = 0; i < response.data.length; i++) {
if (response.data[i].tags.length == 0)
data.noTag = true;
for (var j = 0; j < response.data[i].tags.length; j++) {
if (!data.nodebalancerTags.includes(response.data[i].tags[j]))
data.nodebalancerTags.push(response.data[i].tags[j]);
}
}
// Request the next page if there are more pages
if (response.page != response.pages) {
var progress = (response.page / response.pages) * 100;
progress = progress.toFixed(0);
ui.loading.innerHTML = "Loading " + progress + "%...";
var filters = null;
if (data.params.tag)
filters = {
"tags": data.params.tag
};
apiGet("/nodebalancers?page=" + (response.page + 1), displayNodebalancers, filters);
return;
}
// Remove tag filter if there are no results, otherwise redirect to add page
if (!data.nodebalancers.length) {
if (data.params.tag)
location.href = "/nodebalancers";
else
location.href = "/nodebalancers/add";
}
// Sort
data.nodebalancerTags.sort();
data.nodebalancers.sort(function(a, b)
{
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
});
// Create tables
ui.loading.remove();
if (data.noTag)
createNodebalancerTable("");
for (var i = 0; i < data.nodebalancerTags.length; i++)
createNodebalancerTable(data.nodebalancerTags[i]);
state.haveNodebalancers = true;
// Insert linodes
if (state.haveRegions)
insertNodebalancers();
};
var displayRegions = function(response)
{
// Add regions to array
data.regions = data.regions.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), getRegions, null);
return;
}
state.haveRegions = true;
if (state.haveNodebalancers)
insertNodebalancers();
};
var displayTransfer = function(response)
{
// Get border width of bar segments from CSS sheet
var remainingBorderWidth = 0;
var usedBorderWidth = 0;
for (var i = 0; i < document.styleSheets[0].cssRules.length; i++) {
if (document.styleSheets[0].cssRules[i].selectorText == "#" + elements.barRemaining)
remainingBorderWidth = Number.parseInt(document.styleSheets[0].cssRules[i].style.borderWidth) * 2;
else if (document.styleSheets[0].cssRules[i].selectorText == "#" + elements.barUsed)
usedBorderWidth = Number.parseInt(document.styleSheets[0].cssRules[i].style.borderWidth) * 2;
}
var usage = (response.used / response.quota) * 100;
usage = usage.toFixed(0);
if (usage != 0) {
ui.barUsed.style = "display: inline-block; width: calc(" + usage + "% - " + usedBorderWidth + "px);";
ui.barUsed.innerHTML = usage + "% Used";
}
if (usage != 100) {
ui.barRemaining.style = "display: inline-block; width: calc(" + (100 - usage) + "% - " + remainingBorderWidth + "px);";
ui.barRemaining.innerHTML = (100 - usage) + "% Remaining";
}
ui.transferUsed.innerHTML = response.used + "GB";
ui.transferRemaining.innerHTML = (response.quota - response.used) + "GB";
ui.transferQuota.innerHTML = response.quota + "GB";
};
var insertNodebalancers = function()
{
// Insert linodes into tables
for (var i = 0; i < data.nodebalancers.length; i++) {
if (data.nodebalancers[i].tags.length == 0)
ui.nodebalancerTables[""].appendChild(createNodebalancerRow(data.nodebalancers[i], ui.nodebalancerTables[""].children.length % 2));
for (var j = 0; j < data.nodebalancers[i].tags.length; j++)
ui.nodebalancerTables[data.nodebalancers[i].tags[j]].appendChild(createNodebalancerRow(data.nodebalancers[i], ui.nodebalancerTables[data.nodebalancers[i].tags[j]].children.length % 2));
data.nodebalancers[i].configs = [];
apiGet("/nodebalancers/" + data.nodebalancers[i].id + "/configs", displayConfigs, null);
}
};
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
ui.barRemaining = document.getElementById(elements.barRemaining);
ui.barUsed = document.getElementById(elements.barUsed);
ui.loading = document.getElementById(elements.loading);
ui.nodebalancers = document.getElementById(elements.nodebalancers);
ui.transferQuota = document.getElementById(elements.transferQuota);
ui.transferRemaining = document.getElementById(elements.transferRemaining);
ui.transferUsed = document.getElementById(elements.transferUsed);
setupHeader();
// Get linode and transfer info
apiGet("/regions", displayRegions, null);
apiGet("/account/transfer", displayTransfer, null);
var filters = null;
if (data.params.tag)
filters = {
"tags": data.params.tag
};
apiGet("/nodebalancers", displayNodebalancers, filters);
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,36 @@
<!--
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 - Remove NodeBalancer</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="remove.css" />
<script src="remove.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <a id="toplink-label" href="/nodebalancers/balancer?nbid=0"></a> » <span class="top-links-title">Remove</span></div>
<div id="remove">
<p>Are you sure you want to delete the "<span id="label"></span>" NodeBalancer?</p>
<button id="delete-button" type="button">Yes, delete this sucker</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
/*
* 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');
#remove {
padding: 0px 15px 15px;
}

View File

@ -0,0 +1,85 @@
/*
* 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, apiDelete, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.deleteButton = "delete-button";
elements.label = "label";
elements.toplinkLabel = "toplink-label";
// Data recieved from API calls
var data = {};
// Static references to UI elements
var ui = {};
ui.deleteButton = {};
ui.label = {};
ui.toplinkLabel = {};
// Callback for image API call
var displayNodebalancer = function(response)
{
ui.toplinkLabel.innerHTML = response.label;
ui.label.innerHTML = response.label;
};
// Handler for delete button
var handleDelete = function(event)
{
apiDelete("/nodebalancers/" + data.params.nbid, function(response)
{
location.href = "/nodebalancers";
});
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need an image ID, so die if we don't have it
if (!data.params.nbid) {
alert("No NodeBalancer ID supplied!");
return;
}
setupHeader();
// Update links on page to include proper volume ID
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++)
anchors[i].href = anchors[i].href.replace("nbid=0", "nbid=" + data.params.nbid);
// Get element references
ui.deleteButton = document.getElementById(elements.deleteButton);
ui.label = document.getElementById(elements.label);
ui.toplinkLabel = document.getElementById(elements.toplinkLabel);
// Attach event handlers
ui.deleteButton.addEventListener("click", handleDelete);
// Get data from API
apiGet("/nodebalancers/" + data.params.nbid, displayNodebalancer, null);
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -21,6 +21,10 @@
padding: 0px 15px 15px;
}
optgroup {
display: none;
}
tbody tr td:first-of-type {
font-weight: bold;
text-align: right;

View File

@ -15,13 +15,14 @@
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHeader } from "/global.js";
import { settings, elements, apiGet, apiPost, countryContinents, parseParams, regionNames, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.addButton = "add-button";
elements.attachment = "attachment";
elements.dcOther = "dc-other";
elements.label = "label";
elements.location = "location";
elements.size = "size";
@ -35,6 +36,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
var ui = {};
ui.addButton = {};
ui.attachment = {};
ui.dcOther = {};
ui.label = {};
ui.location = {};
ui.size = {};
@ -90,11 +92,19 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
for (var i = 0; i < data.regions.length; i++) {
var loc = document.createElement("option");
loc.value = data.regions[i].id;
if (regionNames[data.regions[i].id])
if (data.regions[i].label && data.regions[i].label.length)
loc.innerHTML = data.regions[i].label;
else if (regionNames[data.regions[i].id])
loc.innerHTML = regionNames[data.regions[i].id];
else
loc.innerHTML = data.regions[i].id;
ui.location.appendChild(loc);
var optgroup = null;
if (countryContinents[data.regions[i].country])
optgroup = document.getElementById(countryContinents[data.regions[i].country]);
if (!optgroup)
optgroup = ui.dcOther;
optgroup.style.display = "initial";
optgroup.appendChild(loc);
}
// Request linodes in all the supported regions
@ -180,6 +190,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
// Get element references
ui.addButton = document.getElementById(elements.addButton);
ui.attachment = document.getElementById(elements.attachment);
ui.dcOther = document.getElementById(elements.dcOther);
ui.label = document.getElementById(elements.label);
ui.location = document.getElementById(elements.location);
ui.size = document.getElementById(elements.size);

View File

@ -50,6 +50,12 @@ along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
<td>
<select id="location">
<option selected disabled value="0">Choose A Location</option>
<optgroup id="na" label="North America"></optgroup>
<optgroup id="eu" label="Europe"></optgroup>
<optgroup id="ap" label="Asia/Pacific"></optgroup>
<optgroup id="sa" label="South America"></optgroup>
<optgroup id="af" label="Africa"></optgroup>
<optgroup id="dc-other" label="Other"></optgroup>
</select>
</td>
<td class="info">The datacenter where the new volume should be created</td>

View File

@ -39,6 +39,13 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.size = {};
ui.volumeLabel = {};
// Callback for region API call
var displayRegion = function(response)
{
if (response.label && response.label.length)
ui.location.innerHTML = response.label;
};
// Callback for volume API call
var displayVolume = function(response)
{
@ -55,6 +62,7 @@ import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHea
ui.location.innerHTML = response.region;
ui.cloneButton.disabled = false;
apiGet("/regions/" + response.region, displayRegion, null);
};
// Click handler for clone button

View File

@ -83,6 +83,13 @@ import { settings, elements, regionNames, apiGet, apiPost, apiPut, parseParams,
}
};
// Callback for region API call
var displayRegion = function(response)
{
if (response.label && response.label.length)
ui.location.innerHTML = response.label;
};
// Callback for volume API call
var displayVolume = function(response)
{
@ -117,6 +124,8 @@ import { settings, elements, regionNames, apiGet, apiPost, apiPut, parseParams,
}
ui.saveButton.disabled = false;
apiGet("/regions/" + data.volume.region, displayRegion, null);
};
// Click handler for save button

View File

@ -28,6 +28,7 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
// Data recieved from API calls
var data = {};
data.linodes = {};
data.regions = [];
data.volumes = [];
// Static references to UI elements
@ -35,6 +36,11 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
ui.loading = {};
ui.volumeBody = {};
// Temporary state
var state = {};
state.haveRegions = false;
state.haveVolumes = false;
// Generates a table row for a volume
var createVolumeRow = function(volume, alt)
{
@ -59,8 +65,17 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
size.innerHTML = volume.size + " GiB";
row.appendChild(size);
var regionData = null;
for (var i = 0; i < data.regions.length; i++) {
if (data.regions[i].id == volume.region) {
regionData = data.regions[i];
break;
}
}
var region = document.createElement("td");
if (regionNames[volume.region])
if (regionData && regionData.label && regionData.label.length)
region.innerHTML = regionData.label;
else if (regionNames[volume.region])
region.innerHTML = regionNames[volume.region];
else
region.innerHTML = volume.region;
@ -108,6 +123,27 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
return row;
};
// Callback for regions API call
var displayRegions = function(response)
{
// Add regions to array
data.regions = data.regions.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/regions?page=" + (response.page + 1), displayRegions, null);
return;
}
state.haveRegions = true;
if (state.haveVolumes) {
// Insert volumes
ui.loading.remove();
for (var i = 0; i < data.volumes.length; i++)
ui.volumeBody.appendChild(createVolumeRow(data.volumes[i], i % 2));
}
};
// Callback for volumes API call
var displayVolumes = function(response)
{
@ -127,10 +163,13 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
if (data.volumes.length == 0)
location.href = "/volumes/add";
// Insert volumes
ui.loading.remove();
for (var i = 0; i < data.volumes.length; i++)
ui.volumeBody.appendChild(createVolumeRow(data.volumes[i], i % 2));
state.haveVolumes = true;
if (state.haveRegions) {
// Insert volumes
ui.loading.remove();
for (var i = 0; i < data.volumes.length; i++)
ui.volumeBody.appendChild(createVolumeRow(data.volumes[i], i % 2));
}
};
// Initial setup
@ -147,6 +186,7 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro
// Get data from API
apiGet("/volumes", displayVolumes, null);
apiGet("/regions", displayRegions, null);
};
// Get the label of a given linode ID and update a cell's contents