You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ro...@apache.org on 2018/10/18 18:10:23 UTC

[trafficcontrol] 01/02: Tabs now use CSS instead of JS

This is an automated email from the ASF dual-hosted git repository.

rob pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit d09c806f8556f9766b4df5e45d8ab09b5c031242
Author: ocket8888 <oc...@gmail.com>
AuthorDate: Tue Oct 2 08:39:04 2018 -0600

    Tabs now use CSS instead of JS
---
 traffic_monitor/static/index.html | 1089 +++++++++++++++++++------------------
 1 file changed, 546 insertions(+), 543 deletions(-)

diff --git a/traffic_monitor/static/index.html b/traffic_monitor/static/index.html
index 900d887..ceb91d3 100644
--- a/traffic_monitor/static/index.html
+++ b/traffic_monitor/static/index.html
@@ -21,551 +21,554 @@ under the License.
 
 
 <html>
-	<head>
-		<!-- <script src="sorttable.js"></script> -->
-		<meta charset="UTF-8">
-		<title>Traffic Monitor</title>
-		<style>
-		 body {
-			 font-family: "Lato", sans-serif;
-			 font-size: 14px;
-		 }
-
-		 ul.tab {
-			 list-style-type: none;
-			 margin: 0;
-			 padding: 0;
-			 overflow: hidden;
-			 border: 1px solid #ccc;
-			 background-color: #f1f1f1;
-		 }
-
-		 ul.tab li {float: left;}
-
-		 ul.tab li a {
-			 display: inline-block;
-			 color: black;
-			 text-align: center;
-			 padding: 8px 8px;
-			 text-decoration: none;
-			 transition: 0.3s;
-		 }
-
-		 ul.tab li a:hover {
-			 background-color: #cfd;
-		 }
-
-		 .tab-header-selected {
-			 background-color: #adb;
-		 }
-
-		 .tabcontent {
-			 display: none;
-			 padding: 6px 6px;
-			 border: 1px solid #ccc;
-			 border-top: none;
-		 }
-
-		 table, th, td {
-			 border: 0px solid black;
-		 }
-
-		 table {
-			 border-collapse: separate;
-			 border-spacing: 0px 0;
-			 width: 100%;
-		 }
-
-		 th, td {
-			 padding:5px 20px 5px 5px;
-		 }
-
-		 th {
-			 white-space: nowrap;
-		 }
-
-		 tr.stripes:nth-child(even) {
-			 background: #dfe
-		 }
-		 tr.stripes:nth-child(odd) {
-			 background: #fff
-		 }
-
-		 li.endpoint {
-			 margin: 4px 0;
-		 }
-
-		 ul {
-			 list-style: none;
-		 }
-
-		 #top-bar {
-			 border-collapse: collapse;
-			 border-color: #adb;;
-			 border-width: 0px 0px 1px 0px;
-			 padding-bottom: 10px;
-		 }
-
-		 .links {
-			 display: table;
-		 }
-		 .links ul {
-			 display: table-cell;
-			 vertical-align: top;
-		 }
-
-		 .error {
-			 background-color: #f00;
-		 }
-		 .warning {
-			 background-color: #f80;
-		 }
-		</style>
-		<script>
-		 function init() {
-			 openTab('cache-states-content');
-			 getTopBar();
-			 setInterval(getCacheCount, 4755);
-			 setInterval(getCacheAvailableCount, 4800);
-			 setInterval(getBandwidth, 4621);
-			 setInterval(getBandwidthCapacity, 4591);
-			 setInterval(getCacheDownCount, 4832);
-			 setInterval(getVersion, 10007); // change to retry on failure, and only do on startup
-			 setInterval(getTrafficOpsUri, 10019); // change to retry on failure, and only do on startup
-			 setInterval(getTrafficOpsCdn, 10500); // change to retry on failure, and only do on startup
-			 setInterval(getEvents, 2004); // change to retry on failure, and only do on startup
-			 setInterval(getCacheStatuses, 5009);
-			 setInterval(getDsStats, 4003);
-		 }
-
-		 // source: http://stackoverflow.com/a/2901298/292623
-		 function numberStrWithCommas(x) {
-			 return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
-		 }
-
-		 function openTab(tabName) {
-			 var i, x, tablinks;
-			 x = document.getElementsByClassName("tabcontent");
-			 for (i = 0; i < x.length; i++) {
-				 x[i].style.display = "none";
-			 }
-			 tablinks = document.getElementsByClassName("tablinks");
-			 for (i = 0; i < x.length; i++) {
-				 tablinks[i].className = tablinks[i].className.replace(" tab-selected", "");
-			 }
-
-			 tabheaders = document.getElementsByClassName("tab-header");
-			 for (i = 0; i < tabheaders.length; i++) {
-				 tabheaders[i].className = tabheaders[i].className.replace(" tab-header-selected", "");
-			 }
-
-			 document.getElementById(tabName).style.display = "block";
-			 document.getElementById(tabName).className += " tab-selected";
-			 document.getElementById(tabName + "-tab").className += " tab-header-selected";
-		 }
-
-		 function getCacheCount() {
-			 ajax("/api/cache-count", function(r) {
-				 document.getElementById("cache-count").innerHTML = r;
-			 });
-		 }
-
-		 function getCacheAvailableCount() {
-			 ajax("/api/cache-available-count", function(r) {
-				 document.getElementById("cache-available").innerHTML = r;
-			 });
-		 }
-
-		 function getBandwidth() {
-			 ajax("/api/bandwidth-kbps", function(r) {
-				 document.getElementById("bandwidth").innerHTML = numberStrWithCommas((parseFloat(r) / kilobitsInGigabit).toFixed(2));
-			 });
-		 }
-
-		 function getBandwidthCapacity() {
-			 ajax("/api/bandwidth-capacity-kbps", function(r) {
-				 document.getElementById("bandwidth-capacity").innerHTML = numberStrWithCommas((r / kilobitsInGigabit).toString());
-			 });
-		 }
-
-		 function getCacheDownCount() {
-			 ajax("/api/cache-down-count", function(r) {
-				 document.getElementById("cache-down").innerHTML = r;
-			 });
-		 }
-
-		 function getVersion() {
-			 ajax("/api/version", function(r) {
-				 document.getElementById("version").innerHTML = r;
-			 });
-		 }
-
-		 function getTrafficOpsUri() {
-			 ajax("/api/traffic-ops-uri", function(r) {
-				 document.getElementById("source-uri").innerHTML = "<a href='" + r + "'>" + r + "</a>";
-			 });
-		 }
-
-
-		 function getTrafficOpsCdn() {
-			 ajax("/publish/ConfigDoc", function(r) {
-				 var j = JSON.parse(r);
-				 document.getElementById("cdn-name").innerHTML = j.cdnName;
-			 });
-		 }
-
-		 var lastEvent = 0;
-		 function getEvents() {
-			 /// \todo add /api/events-since/{index} (and change Traffic Monitor to keep latest
-			 ajax("/publish/EventLog", function(r) {
-				 var jdata = JSON.parse(r);
-				 for (i = jdata.events.length - 1; i >= 0; i--) {
-					 var event = jdata.events[i];
-					 if (event.index <= lastEvent) {
-						 continue;
-					 }
-					 lastEvent = event.index
-					 var row = document.getElementById("event-log").insertRow(1); //document.createElement("tr");
-					 row.classList.add("stripes");
-					 row.insertCell(0).id = row.id + "-name";
-					 document.getElementById(row.id + "-name").textContent = event.name;
-					 document.getElementById(row.id + "-name").style.whiteSpace = "nowrap";
-					 row.insertCell(1).textContent = event.type;
-					 row.insertCell(2).textContent = event.isAvailable ? "available" : "offline";
-					 if(event.isAvailable) {
-						 row.classList.add("stripes");
-						 row.classList.remove("error");
-					 } else {
-						 row.classList.add("error");
-						 row.classList.remove("stripes");
-					 }
-					 row.insertCell(3).textContent = event.description;
-					 row.insertCell(4).id = row.id + "-last";
-					 document.getElementById(row.id + "-last").textContent = new Date(event.time * 1000).toISOString();
-					 document.getElementById(row.id + "-last").style.whiteSpace = "nowrap";
-					 document.getElementById(row.id + "-last").style.textAlign = "right";
-				 }
-			 });
-		 }
-
-		 function getCacheStates() {
-			 ajax("/api/cache-statuses", function(r) {
-				 var jdata = JSON.parse(r);
-				 var servers = Object.keys(jdata); //debug
-				 var table = document.getElementById("cache-states");
-				 for (i = 0; i < servers.length; i++) {
-					 var server = servers[i];
-					 if (!document.getElementById("cache-states-" + server)) {
-						 var row = table.insertRow(1); //document.createElement("tr");
-						 row.classList.add("stripes");
-						 row.id = "cache-states-" + server
-						 row.insertCell(0).id = row.id + "-server";
-						 row.insertCell(1).id = row.id + "-type";
-						 row.insertCell(2).id = row.id + "-status";
-						 row.insertCell(3).id = row.id + "-load-average";
-						 row.insertCell(4).id = row.id + "-query-time";
-						 row.insertCell(5).id = row.id + "-health-time";
-						 row.insertCell(6).id = row.id + "-stat-time";
-						 row.insertCell(7).id = row.id + "-health-span";
-						 row.insertCell(8).id = row.id + "-stat-span";
-						 row.insertCell(9).id = row.id + "-bandwidth";
-						 row.insertCell(10).id = row.id + "-connection-count";
-						 document.getElementById(row.id + "-server").textContent = server;
-						 document.getElementById(row.id + "-server").style.whiteSpace = "nowrap";
-						 document.getElementById(row.id + "-load-average").style.textAlign = "right";
-						 document.getElementById(row.id + "-query-time").style.textAlign = "right";
-						 document.getElementById(row.id + "-health-time").style.textAlign = "right";
-						 document.getElementById(row.id + "-stat-time").style.textAlign = "right";
-						 document.getElementById(row.id + "-health-span").style.textAlign = "right";
-						 document.getElementById(row.id + "-stat-span").style.textAlign = "right";
-						 document.getElementById(row.id + "-bandwidth").style.textAlign = "right";
-						 document.getElementById(row.id + "-connection-count").style.textAlign = "right";
-					 }
-
-					 /* \todo change to iterate over members, and make cells id constructed from these*/
-					 if (jdata[server].hasOwnProperty("type")) {
-						 document.getElementById("cache-states-" + server + "-type").textContent = jdata[server].type;
-					 }
-					 if (jdata[server].hasOwnProperty("status")) {
-						 document.getElementById("cache-states-" + server + "-status").textContent = jdata[server].status;
-						 var row = document.getElementById("cache-states-" + server);
-						 if (jdata[server].status.indexOf("ADMIN_DOWN") != -1 || jdata[server].status.indexOf("OFFLINE") != -1) {
-							 row.classList.add("warning");
-							 row.classList.remove("error");
-							 row.classList.remove("stripes");
-						 } else if (jdata[server].status.indexOf(" available") == -1 && jdata[server].status.indexOf("ONLINE") != 0) {
-							 row.classList.add("error");
-							 row.classList.remove("warning");
-							 row.classList.remove("stripes");
-						 } else {
-							 row.classList.add("stripe");
-							 row.classList.remove("warning");
-							 row.classList.remove("error");
-						 }
-					 }
-					 if (jdata[server].hasOwnProperty("load_average")) {
-						 document.getElementById("cache-states-" + server + "-load-average").textContent = jdata[server].load_average;
-					 }
-					 if (jdata[server].hasOwnProperty("query_time_ms")) {
-						 document.getElementById("cache-states-" + server + "-query-time").textContent = jdata[server].query_time_ms;
-					 }
-					 if (jdata[server].hasOwnProperty("health_time_ms")) {
-						 document.getElementById("cache-states-" + server + "-health-time").textContent = jdata[server].health_time_ms;
-					 }
-					 if (jdata[server].hasOwnProperty("stat_time_ms")) {
-						 document.getElementById("cache-states-" + server + "-stat-time").textContent = jdata[server].stat_time_ms;
-					 }
-					 if (jdata[server].hasOwnProperty("health_span_ms")) {
-						 document.getElementById("cache-states-" + server + "-health-span").textContent = jdata[server].health_span_ms;
-					 }
-					 if (jdata[server].hasOwnProperty("stat_span_ms")) {
-						 document.getElementById("cache-states-" + server + "-stat-span").textContent = jdata[server].stat_span_ms;
-					 }
-					 if (jdata[server].hasOwnProperty("bandwidth_kbps")) {
-						 var kbps = (jdata[server].bandwidth_kbps / kilobitsInMegabit).toFixed(2);
-						 var max = numberStrWithCommas((jdata[server].bandwidth_capacity_kbps / kilobitsInMegabit).toFixed(0));
-						 document.getElementById("cache-states-" + server + "-bandwidth").textContent = '' + kbps + ' / ' + max;
-					 } else {
-						 document.getElementById("cache-states-" + server + "-bandwidth").textContent = "N/A";
-					 }
-					 if (jdata[server].hasOwnProperty("connection_count")) {
-						 document.getElementById("cache-states-" + server + "-connection-count").textContent = jdata[server].connection_count;
-					 } else {
-						 document.getElementById("cache-states-" + server + "-connection-count").textContent = "N/A";
-					 }
-				 }
-
-				 for (var i = 1, row; row = table.rows[i]; i++) { // start at 1, because row[0] is the header
-					 var server = row.id.replace(/^cache-states-/, '');
-					 if(!(server in jdata)) {
-						 table.deleteRow(i);
-					 }
-				 }
-
-			 })
-		 }
-
-		 var millisecondsInSecond = 1000;
-		 var kilobitsInGigabit = 1000000;
-		 var kilobitsInMegabit = 1000;
-
-		 // dsDisplayFloat takes a float, and returns the string to display. For nonzero values, it returns two decimal places. For zero values, it returns an empty string, to make nonzero values more visible.
-		 function dsDisplayFloat(f) {
-			 var s = f
-			 if (f != 0.0) {
-				 s = f.toFixed(2);
-			 }
-			 return s
-		 }
-
-		 function getDsStats() {
-			 var now = Date.now();
-
-			 /// \todo add /api/delivery-service-stats which only returns the data needed by the UI, for efficiency
-			 ajax("/publish/DsStats", function(r) {
-				 var j = JSON.parse(r);
-				 var jds = j.deliveryService
-				 var deliveryServiceNames = Object.keys(jds); //debug
-				 //decrementing for loop so DsNames are alphabetical A-Z
-				 //TODO allow for filtering of columns so this isn't necessary
+<head>
+	<!-- <script src="sorttable.js"></script> -->
+	<meta charset="UTF-8">
+	<title>Traffic Monitor</title>
+	<style>
+		body {
+			font-family: "Lato", sans-serif;
+			font-size: 14px;
+			margin: 0;
+			max-width: 100vw;
+		}
+
+		table {
+			border-collapse: separate;
+			border-spacing: 0px 0;
+			width: 100%;
+		}
+
+		th, td {
+			padding:5px 20px 5px 5px;
+		}
+
+		th {
+			white-space: nowrap;
+		}
+
+		tbody tr:nth-child(even) {
+			background: #dfe
+		}
+		tbody tr:nth-child(odd) {
+			background: #fff
+		}
+
+		li.endpoint {
+			margin: 4px 0;
+		}
+
+		div#top-bar {
+			display: inline-flex;
+			justify-content: space-around;
+			align-items: center;
+			width: 100%;
+		}
+
+		div#links {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			max-width: 100ch;
+		}
+		div#links div {
+			margin-left: 4px;
+		}
+		div#links a {
+			display: block;
+		}
+
+		.error {
+			background-color: #f00;
+		}
+		.warning {
+			background-color: #f80;
+		}
+
+	 	input[type=radio] {
+			visibility: hidden;
+			display: none;
+		}
+		label {
+			display: block;
+			padding: 14px 21px;
+			border-radius: 2px 2px 0 0;
+			cursor: pointer;
+			position: relative;
+			top: 4px;
+			transition: 0.3s;
+			text-align: center;
+		}
+		label:hover {
+			background-color: #cfd;
+		}
+		ul.tabs {
+			list-style: none;
+			max-width: 100%;
+			border: 1px solid #ccc;
+			background-color: #f1f1f1;
+			position: relative;
+		}
+		div.tabcontent {
+			z-index: 2;
+			display: none;
+			visibility: hidden;
+			overflow: hidden;
+			width: 100%;
+			position: absolute;
+			top: 53px;
+			left: 0;
+			padding: 6px 0;
+			border-top: none;
+		}
+		input.tab:checked ~ div.tabcontent {
+			display: block;
+			visibility: visible;
+		}
+		input.tab:checked ~ label{
+			background-color: #adb;
+		}
+		ul.tabs li {
+			float: left;
+			display: block;
+		}
+	</style>
+	<script>
+		function init() {
+			getTopBar();
+			setInterval(getCacheCount, 4755);
+			setInterval(getCacheAvailableCount, 4800);
+			setInterval(getBandwidth, 4621);
+			setInterval(getBandwidthCapacity, 4591);
+			setInterval(getCacheDownCount, 4832);
+			setInterval(getVersion, 10007); // change to retry on failure, and only do on startup
+			setInterval(getTrafficOpsUri, 10019); // change to retry on failure, and only do on startup
+			setInterval(getTrafficOpsCdn, 10500); // change to retry on failure, and only do on startup
+			setInterval(getEvents, 2004); // change to retry on failure, and only do on startup
+			setInterval(getCacheStatuses, 5009);
+			setInterval(getDsStats, 4003);
+		}
+
+		// source: http://stackoverflow.com/a/2901298/292623
+		function numberStrWithCommas(x) {
+			return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+		}
+
+		function getCacheCount() {
+			ajax("/api/cache-count", function(r) {
+				document.getElementById("cache-count").innerHTML = r;
+			});
+		}
+
+		function getCacheAvailableCount() {
+			ajax("/api/cache-available-count", function(r) {
+				document.getElementById("cache-available").innerHTML = r;
+			});
+		}
+
+		function getBandwidth() {
+			ajax("/api/bandwidth-kbps", function(r) {
+				document.getElementById("bandwidth").innerHTML = numberStrWithCommas((parseFloat(r) / kilobitsInGigabit).toFixed(2));
+			});
+		}
+
+		function getBandwidthCapacity() {
+			ajax("/api/bandwidth-capacity-kbps", function(r) {
+				document.getElementById("bandwidth-capacity").innerHTML = numberStrWithCommas((r / kilobitsInGigabit).toString());
+			});
+		}
+
+		function getCacheDownCount() {
+			ajax("/api/cache-down-count", function(r) {
+				document.getElementById("cache-down").innerHTML = r;
+			});
+		}
+
+		function getVersion() {
+			ajax("/api/version", function(r) {
+				document.getElementById("version").innerHTML = r;
+			});
+		}
+
+		function getTrafficOpsUri() {
+			ajax("/api/traffic-ops-uri", function(r) {
+				document.getElementById("source-uri").innerHTML = "<a href='" + r + "'>" + r + "</a>";
+			});
+		}
+
+
+		function getTrafficOpsCdn() {
+			ajax("/publish/ConfigDoc", function(r) {
+				var j = JSON.parse(r);
+				document.getElementById("cdn-name").innerHTML = j.cdnName;
+			});
+		}
+
+		var lastEvent = 0;
+		function getEvents() {
+			/// \todo add /api/events-since/{index} (and change Traffic Monitor to keep latest
+			ajax("/publish/EventLog", function(r) {
+				var jdata = JSON.parse(r);
+				for (i = jdata.events.length - 1; i >= 0; i--) {
+					var event = jdata.events[i];
+					if (event.index <= lastEvent) {
+						continue;
+					}
+					lastEvent = event.index
+					var row = document.getElementById("event-log").insertRow(0); //document.createElement("tr");
+					row.classList.add("stripes");
+					row.insertCell(0).id = row.id + "-name";
+					document.getElementById(row.id + "-name").textContent = event.name;
+					document.getElementById(row.id + "-name").style.whiteSpace = "nowrap";
+					row.insertCell(1).textContent = event.type;
+					row.insertCell(2).textContent = event.isAvailable ? "available" : "offline";
+					if(event.isAvailable) {
+						row.classList.add("stripes");
+						row.classList.remove("error");
+					} else {
+						row.classList.add("error");
+						row.classList.remove("stripes");
+					}
+					row.insertCell(3).textContent = event.description;
+					row.insertCell(4).id = row.id + "-last";
+					document.getElementById(row.id + "-last").textContent = new Date(event.time * 1000).toISOString();
+					document.getElementById(row.id + "-last").style.whiteSpace = "nowrap";
+					document.getElementById(row.id + "-last").style.textAlign = "right";
+				}
+			});
+		}
+
+		function getCacheStates() {
+			ajax("/api/cache-statuses", function(r) {
+				var jdata = JSON.parse(r);
+				var servers = Object.keys(jdata); //debug
+				var table = document.getElementById("cache-states");
+				for (i = 0; i < servers.length; i++) {
+					var server = servers[i];
+					if (!document.getElementById("cache-states-" + server)) {
+						var row = table.insertRow(0); //document.createElement("tr");
+						row.classList.add("stripes");
+						row.id = "cache-states-" + server
+						row.insertCell(0).id = row.id + "-server";
+						row.insertCell(1).id = row.id + "-type";
+						row.insertCell(2).id = row.id + "-status";
+						row.insertCell(3).id = row.id + "-load-average";
+						row.insertCell(4).id = row.id + "-query-time";
+						row.insertCell(5).id = row.id + "-health-time";
+						row.insertCell(6).id = row.id + "-stat-time";
+						row.insertCell(7).id = row.id + "-health-span";
+						row.insertCell(8).id = row.id + "-stat-span";
+						row.insertCell(9).id = row.id + "-bandwidth";
+						row.insertCell(10).id = row.id + "-connection-count";
+						document.getElementById(row.id + "-server").textContent = server;
+						document.getElementById(row.id + "-server").style.whiteSpace = "nowrap";
+						document.getElementById(row.id + "-load-average").style.textAlign = "right";
+						document.getElementById(row.id + "-query-time").style.textAlign = "right";
+						document.getElementById(row.id + "-health-time").style.textAlign = "right";
+						document.getElementById(row.id + "-stat-time").style.textAlign = "right";
+						document.getElementById(row.id + "-health-span").style.textAlign = "right";
+						document.getElementById(row.id + "-stat-span").style.textAlign = "right";
+						document.getElementById(row.id + "-bandwidth").style.textAlign = "right";
+						document.getElementById(row.id + "-connection-count").style.textAlign = "right";
+					}
+
+					/* \todo change to iterate over members, and make cells id constructed from these*/
+					if (jdata[server].hasOwnProperty("type")) {
+						document.getElementById("cache-states-" + server + "-type").textContent = jdata[server].type;
+					}
+					if (jdata[server].hasOwnProperty("status")) {
+						document.getElementById("cache-states-" + server + "-status").textContent = jdata[server].status;
+						var row = document.getElementById("cache-states-" + server);
+						if (jdata[server].status.indexOf("ADMIN_DOWN") != -1 || jdata[server].status.indexOf("OFFLINE") != -1) {
+							row.classList.add("warning");
+							row.classList.remove("error");
+							row.classList.remove("stripes");
+						} else if (jdata[server].status.indexOf(" available") == -1 && jdata[server].status.indexOf("ONLINE") != 0) {
+							row.classList.add("error");
+							row.classList.remove("warning");
+							row.classList.remove("stripes");
+						} else {
+							row.classList.add("stripe");
+							row.classList.remove("warning");
+							row.classList.remove("error");
+						}
+					}
+					if (jdata[server].hasOwnProperty("load_average")) {
+						document.getElementById("cache-states-" + server + "-load-average").textContent = jdata[server].load_average;
+					}
+					if (jdata[server].hasOwnProperty("query_time_ms")) {
+						document.getElementById("cache-states-" + server + "-query-time").textContent = jdata[server].query_time_ms;
+					}
+					if (jdata[server].hasOwnProperty("health_time_ms")) {
+						document.getElementById("cache-states-" + server + "-health-time").textContent = jdata[server].health_time_ms;
+					}
+					if (jdata[server].hasOwnProperty("stat_time_ms")) {
+						document.getElementById("cache-states-" + server + "-stat-time").textContent = jdata[server].stat_time_ms;
+					}
+					if (jdata[server].hasOwnProperty("health_span_ms")) {
+						document.getElementById("cache-states-" + server + "-health-span").textContent = jdata[server].health_span_ms;
+					}
+					if (jdata[server].hasOwnProperty("stat_span_ms")) {
+						document.getElementById("cache-states-" + server + "-stat-span").textContent = jdata[server].stat_span_ms;
+					}
+					if (jdata[server].hasOwnProperty("bandwidth_kbps")) {
+						var kbps = (jdata[server].bandwidth_kbps / kilobitsInMegabit).toFixed(2);
+						var max = numberStrWithCommas((jdata[server].bandwidth_capacity_kbps / kilobitsInMegabit).toFixed(0));
+						document.getElementById("cache-states-" + server + "-bandwidth").textContent = '' + kbps + ' / ' + max;
+					} else {
+						document.getElementById("cache-states-" + server + "-bandwidth").textContent = "N/A";
+					}
+					if (jdata[server].hasOwnProperty("connection_count")) {
+						document.getElementById("cache-states-" + server + "-connection-count").textContent = jdata[server].connection_count;
+					} else {
+						document.getElementById("cache-states-" + server + "-connection-count").textContent = "N/A";
+					}
+				}
+
+				for (var i = 1, row; row = table.rows[i]; i++) { // start at 1, because row[0] is the header
+					var server = row.id.replace(/^cache-states-/, '');
+					if(!(server in jdata)) {
+						table.deleteRow(i);
+					}
+				}
+
+			})
+		}
+
+		var millisecondsInSecond = 1000;
+		var kilobitsInGigabit = 1000000;
+		var kilobitsInMegabit = 1000;
+
+		// dsDisplayFloat takes a float, and returns the string to display. For nonzero values, it returns two decimal places. For zero values, it returns an empty string, to make nonzero values more visible.
+		function dsDisplayFloat(f) {
+			var s = f
+			if (f != 0.0) {
+				s = f.toFixed(2);
+			}
+			return s
+		}
+
+		function getDsStats() {
+			var now = Date.now();
+
+			/// \todo add /api/delivery-service-stats which only returns the data needed by the UI, for efficiency
+			ajax("/publish/DsStats", function(r) {
+				var j = JSON.parse(r);
+				var jds = j.deliveryService
+				var deliveryServiceNames = Object.keys(jds); //debug
+				//decrementing for loop so DsNames are alphabetical A-Z
+				//TODO allow for filtering of columns so this isn't necessary
 					for (var i = deliveryServiceNames.length - 1; i >= 0; i--) {
-					 var deliveryService = deliveryServiceNames[i];
-
-					 if (!document.getElementById("deliveryservice-stats-" + deliveryService)) {
-						 var row = document.getElementById("deliveryservice-stats").insertRow(1); //document.createElement("tr");
-						 row.id = "deliveryservice-stats-" + deliveryService
-						 row.insertCell(0).id = row.id + "-delivery-service";
-						 row.insertCell(1).id = row.id + "-status";
-						 row.insertCell(2).id = row.id + "-caches-reporting";
-						 row.insertCell(3).id = row.id + "-bandwidth";
-						 row.insertCell(4).id = row.id + "-tps";
-						 row.insertCell(5).id = row.id + "-2xx";
-						 row.insertCell(6).id = row.id + "-3xx";
-						 row.insertCell(7).id = row.id + "-4xx";
-						 row.insertCell(8).id = row.id + "-5xx";
-						 row.insertCell(9).id = row.id + "-disabled-locations";
-						 document.getElementById(row.id + "-delivery-service").textContent = deliveryService;
-						 document.getElementById(row.id + "-delivery-service").style.whiteSpace = "nowrap";
-						 document.getElementById(row.id + "-caches-reporting").style.textAlign = "right";
-						 document.getElementById(row.id + "-bandwidth").style.textAlign = "right";
-						 document.getElementById(row.id + "-tps").style.textAlign = "right";
-						 document.getElementById(row.id + "-2xx").style.textAlign = "right";
-						 document.getElementById(row.id + "-3xx").style.textAlign = "right";
-						 document.getElementById(row.id + "-4xx").style.textAlign = "right";
-						 document.getElementById(row.id + "-5xx").style.textAlign = "right";
-					 }
-
-					 // \todo check that array has a member before dereferencing [0]
-					 if (jds[deliveryService].hasOwnProperty("isAvailable")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-status").textContent = jds[deliveryService]["isAvailable"][0].value == "true" ? "available" : "unavailable - " + jds[deliveryService]["error-string"][0].value;
-					 }
-					 if (jds[deliveryService].hasOwnProperty("caches-reporting") && jds[deliveryService].hasOwnProperty("caches-available") && jds[deliveryService].hasOwnProperty("caches-configured")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-caches-reporting").textContent = jds[deliveryService]['caches-reporting'][0].value + " / " + jds[deliveryService]['caches-available'][0].value + " / " + jds[deliveryService]['caches-configured'][0].value;
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.kbps")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-bandwidth").textContent = (jds[deliveryService]['total.kbps'][0].value / kilobitsInMegabit).toFixed(2);
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-bandwidth").textContent = "N/A";
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.tps_total")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-tps").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_total'][0].value));
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-tps").textContent = "N/A";
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.tps_2xx")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-2xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_2xx'][0].value));
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-2xx").textContent = "N/A";
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.tps_3xx")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-3xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_3xx'][0].value));
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-3xx").textContent = "N/A";
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.tps_4xx")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-4xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_4xx'][0].value));
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-4xx").textContent = "N/A";
-					 }
-					 if (jds[deliveryService].hasOwnProperty("total.tps_5xx")) {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-5xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_5xx'][0].value));
-					 } else {
-						 document.getElementById("deliveryservice-stats-" + deliveryService + "-5xx").textContent = "N/A";
-					 }
-
-					 // \todo implement disabled locations
-
-					 var row = document.getElementById("deliveryservice-stats-" + deliveryService);
-					 if (jds[deliveryService]["isAvailable"][0].value == "true") {
-						 row.classList.add("stripes");
-						 row.classList.remove("error");
-					 } else {
-						 row.classList.add("error");
-						 row.classList.remove("stripes");
-					 }
-				 }
-			 })
-		 }
-
-		 function getCacheStatuses() {
-			 getCacheCount();
-			 getCacheAvailableCount();
-			 getCacheDownCount();
-			 getCacheStates();
-		 }
-
-		 function getTopBar() {
-			 getVersion();
-			 getTrafficOpsUri();
-			 getTrafficOpsCdn();
-			 getCacheStatuses();
-		 }
-
-		 function ajax(endpoint, f) {
-			 var xhttp = new XMLHttpRequest();
-			 xhttp.onreadystatechange = function() {
-				 if (xhttp.readyState == 4 && xhttp.status == 200) {
-					 f(xhttp.responseText);
-				 }
-			 };
-			 xhttp.open("GET", endpoint, true);
-			 xhttp.send();
-		 }
-		</script>
-	</head>
-	<body onload="init()">
-
-		<table id="top-bar">
-			<tr>
-				<td>Caches: count=<span id="cache-count">0</span> available=<span id="cache-available">0</span> down=<span id="cache-down">0</span> </td>
-				<td>Bandwidth: <span id="bandwidth">0</span> / <span id="bandwidth-capacity">∞</span> gbps</td>
-				<td>Traffic Ops: <span id="source-uri">unknown</span></td>
-				<td>CDN: <span id="cdn-name">unknown</span></td>
-				<td>Version: <span id="version">unknown</span></td>
-			</tr>
-		</table>
-
-		<div class="links">
-			<ul>
-				<li class="endpoint"><a href="/publish/EventLog">EventLog</a></li>
-				<li class="endpoint"><a href="/publish/CacheStats">CacheStats</a></li>
-				<li class="endpoint"><a href="/publish/DsStats">DsStats</a></li>
-				<li class="endpoint"><a href="/publish/CrStates">CrStates (as published to Traffic Routers)</a></li>
-				<li class="endpoint"><a href="/publish/CrConfig">CrConfig (json)</a></li>
-				<li class="endpoint"><a href="/publish/PeerStates">PeerStates</a></li>
-				<li class="endpoint"><a href="/publish/Stats">Stats</a></li>
-				<li class="endpoint"><a href="/publish/StatSummary">StatSummary</a></li>
-				<li class="endpoint"><a href="/publish/ConfigDoc">ConfigDoc</a></li>
-			</ul>
-
-			<ul>
-				<li class="endpoint"><a href="/api/cache-count">/api/cache-count</a></li>
-				<li class="endpoint"><a href="/api/cache-available-count">/api/cache-available-count</a></li>
-				<li class="endpoint"><a href="/api/cache-down-count">/api/cache-down-count</a></li>
-				<li class="endpoint"><a href="/api/version">/api/version</a></li>
-				<li class="endpoint"><a href="/api/traffic-ops-uri">/api/traffic-ops-uri</a></li>
-				<li class="endpoint"><a href="/api/cache-statuses">/api/cache-statuses</a></li>
-				<li class="endpoint"><a href="/api/bandwidth-kbps">/api/bandwidth-kbps</a></li>
-				<li class="endpoint"><a href="/api/bandwidth-capacity-kbps">/api/bandwidth-capacity-kbps</a></li>
-				<li class="endpoint"><a href="/api/monitor-config">/api/monitor-config</a></li>
-				<li class="endpoint"><a href="/api/crconfig-history">/api/crconfig-history</a></li>
-			</ul>
+					var deliveryService = deliveryServiceNames[i];
+
+					if (!document.getElementById("deliveryservice-stats-" + deliveryService)) {
+						var row = document.getElementById("deliveryservice-stats").insertRow(0); //document.createElement("tr");
+						row.id = "deliveryservice-stats-" + deliveryService
+						row.insertCell(0).id = row.id + "-delivery-service";
+						row.insertCell(1).id = row.id + "-status";
+						row.insertCell(2).id = row.id + "-caches-reporting";
+						row.insertCell(3).id = row.id + "-bandwidth";
+						row.insertCell(4).id = row.id + "-tps";
+						row.insertCell(5).id = row.id + "-2xx";
+						row.insertCell(6).id = row.id + "-3xx";
+						row.insertCell(7).id = row.id + "-4xx";
+						row.insertCell(8).id = row.id + "-5xx";
+						row.insertCell(9).id = row.id + "-disabled-locations";
+						document.getElementById(row.id + "-delivery-service").textContent = deliveryService;
+						document.getElementById(row.id + "-delivery-service").style.whiteSpace = "nowrap";
+						document.getElementById(row.id + "-caches-reporting").style.textAlign = "right";
+						document.getElementById(row.id + "-bandwidth").style.textAlign = "right";
+						document.getElementById(row.id + "-tps").style.textAlign = "right";
+						document.getElementById(row.id + "-2xx").style.textAlign = "right";
+						document.getElementById(row.id + "-3xx").style.textAlign = "right";
+						document.getElementById(row.id + "-4xx").style.textAlign = "right";
+						document.getElementById(row.id + "-5xx").style.textAlign = "right";
+					}
+
+					// \todo check that array has a member before dereferencing [0]
+					if (jds[deliveryService].hasOwnProperty("isAvailable")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-status").textContent = jds[deliveryService]["isAvailable"][0].value == "true" ? "available" : "unavailable - " + jds[deliveryService]["error-string"][0].value;
+					}
+					if (jds[deliveryService].hasOwnProperty("caches-reporting") && jds[deliveryService].hasOwnProperty("caches-available") && jds[deliveryService].hasOwnProperty("caches-configured")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-caches-reporting").textContent = jds[deliveryService]['caches-reporting'][0].value + " / " + jds[deliveryService]['caches-available'][0].value + " / " + jds[deliveryService]['caches-configured'][0].value;
+					}
+					if (jds[deliveryService].hasOwnProperty("total.kbps")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-bandwidth").textContent = (jds[deliveryService]['total.kbps'][0].value / kilobitsInMegabit).toFixed(2);
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-bandwidth").textContent = "N/A";
+					}
+					if (jds[deliveryService].hasOwnProperty("total.tps_total")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-tps").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_total'][0].value));
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-tps").textContent = "N/A";
+					}
+					if (jds[deliveryService].hasOwnProperty("total.tps_2xx")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-2xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_2xx'][0].value));
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-2xx").textContent = "N/A";
+					}
+					if (jds[deliveryService].hasOwnProperty("total.tps_3xx")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-3xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_3xx'][0].value));
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-3xx").textContent = "N/A";
+					}
+					if (jds[deliveryService].hasOwnProperty("total.tps_4xx")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-4xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_4xx'][0].value));
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-4xx").textContent = "N/A";
+					}
+					if (jds[deliveryService].hasOwnProperty("total.tps_5xx")) {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-5xx").textContent = dsDisplayFloat(parseFloat(jds[deliveryService]['total.tps_5xx'][0].value));
+					} else {
+						document.getElementById("deliveryservice-stats-" + deliveryService + "-5xx").textContent = "N/A";
+					}
+
+					// \todo implement disabled locations
+
+					var row = document.getElementById("deliveryservice-stats-" + deliveryService);
+					if (jds[deliveryService]["isAvailable"][0].value == "true") {
+						row.classList.add("stripes");
+						row.classList.remove("error");
+					} else {
+						row.classList.add("error");
+						row.classList.remove("stripes");
+					}
+				}
+			})
+		}
+
+		function getCacheStatuses() {
+			getCacheCount();
+			getCacheAvailableCount();
+			getCacheDownCount();
+			getCacheStates();
+		}
+
+		function getTopBar() {
+			getVersion();
+			getTrafficOpsUri();
+			getTrafficOpsCdn();
+			getCacheStatuses();
+		}
+
+		function ajax(endpoint, f) {
+			var xhttp = new XMLHttpRequest();
+			xhttp.onreadystatechange = function() {
+				if (xhttp.readyState == 4 && xhttp.status == 200) {
+					f(xhttp.responseText);
+				}
+			};
+			xhttp.open("GET", endpoint, true);
+			xhttp.send();
+		}
+	</script>
+</head>
+<body onload="init()">
+	<div id="top-bar">
+		<div>Caches: count=<span id="cache-count">0</span> available=<span id="cache-available">0</span> down=<span id="cache-down">0</span> </div>
+		<div>Bandwidth: <span id="bandwidth">0</span> / <span id="bandwidth-capacity">∞</span> gbps</div>
+		<div>Traffic Ops: <span id="source-uri">unknown</span></div>
+		<div>CDN: <span id="cdn-name">unknown</span></div>
+		<div>Version: <span id="version">unknown</span></div>
+	</div>
+
+	<div id="links">
+		<div>
+			<a href="/publish/EventLog">EventLog</a>
+			<a href="/publish/CacheStats">CacheStats</a>
+			<a href="/publish/DsStats">DsStats</a>
+			<a href="/publish/CrStates">CrStates (as published to Traffic Routers)</a>
+			<a href="/publish/CrConfig">CrConfig (json)</a>
+			<a href="/publish/PeerStates">PeerStates</a>
+			<a href="/publish/Stats">Stats</a>
+			<a href="/publish/StatSummary">StatSummary</a>
+			<a href="/publish/ConfigDoc">ConfigDoc</a>
 		</div>
 
-		<ul class="tab">
-			<li id="cache-states-content-tab" class="tab-header"><a href="#" onclick="openTab('cache-states-content')" class="tablinks">Cache States</a></li>
-			<li id="deliveryservice-stats-content-tab" class="tab-header"><a href="#" onclick="openTab('deliveryservice-stats-content')" class="tablinks">Delivery Service States</a></li>
-			<li id="event-log-content-tab" class="tab-header"><a href="#" onclick="openTab('event-log-content')" class="tablinks">Event Log</a></li>
-		</ul>
-
-		<div id="cache-states-content" class="tabcontent">
-			<table id="cache-states" class="tab-grid sortable">
-				<tr>
-					<th>Server</th>
-					<th>Type</th>
-					<th>Status</th>
-					<th align="right">Load Average</th>
-					<th align="right">Query Time (ms)</th>
-					<th align="right">Health Time (ms)</th>
-					<th align="right">Stat Time (ms)</th>
-					<th align="right">Health Span (ms)</th>
-					<th align="right">Stat Span (ms)</th>
-					<th align="right">Bandwidth (mbps)</th>
-					<th align="right">Connection Count</th>
-				</tr>
-			</table>
+		<div>
+			<a href="/api/cache-count">/api/cache-count</a>
+			<a href="/api/cache-available-count">/api/cache-available-count</a>
+			<a href="/api/cache-down-count">/api/cache-down-count</a>
+			<a href="/api/version">/api/version</a>
+			<a href="/api/traffic-ops-uri">/api/traffic-ops-uri</a>
+			<a href="/api/cache-statuses">/api/cache-statuses</a>
+			<a href="/api/bandwidth-kbps">/api/bandwidth-kbps</a>
+			<a href="/api/bandwidth-capacity-kbps">/api/bandwidth-capacity-kbps</a>
+			<a href="/api/monitor-config">/api/monitor-config</a>
+			<a href="/api/crconfig-history">/api/crconfig-history</a>
 		</div>
-		<div id="deliveryservice-stats-content" class="tabcontent">
-			<table id="deliveryservice-stats" class="tab-grid sortable">
-				<tr>
-					<th>Delivery Service</th>
-					<th>Status</th>
-					<th align="right">Caches Reporting/Available/Configured</th>
-					<th align="right">Bandwidth (mbps)</th>
-					<th align="right">t/sec</th>
-					<th align="right">2xx/sec</th>
-					<th align="right">3xx/sec</th>
-					<th align="right">4xx/sec</th>
-					<th align="right">5xx/sec</th>
-					<th>Disabled Locations</th>
-				</tr>
-			</table>
-		</div>
-
-		<div id="event-log-content" class="tabcontent">
-			<table id="event-log" class="tab-grid sortable">
-				<tr>
-					<th>Name</th>
-					<th>Type</th>
-					<th>Status</th>
-					<th>Description</th>
-					<th align="center" id="event-log-last-header">Event Time</th>
-				</tr>
-			</table>
-		</div>
-
-		<div id="update-num-text">Number of updates: <span id="update-num">0</span></div>
-		<div id="last-val-text">Last Val: <span id="last-val">0</span></div>
-		<a href="/">Refresh Server List</a>
-	</body>
+	</div>
+
+	<ul class="tabs" role="tablist">
+		<li class="tab tab-header" id="cache-states-content-tab">
+			<input type="radio" name="tabs" class="tab" id="cache-states-input" checked />
+			<label for="cache-states-input" aria-selected="true">Cache States</label>
+			<div id="cache-states-content" class="tabcontent">
+				<table class="tab-grid sortable">
+					<thead>
+						<tr>
+							<th>Server</th>
+							<th>Type</th>
+							<th>Status</th>
+							<th align="right">Load Average</th>
+							<th align="right">Query Time (ms)</th>
+							<th align="right">Health Time (ms)</th>
+							<th align="right">Stat Time (ms)</th>
+							<th align="right">Health Span (ms)</th>
+							<th align="right">Stat Span (ms)</th>
+							<th align="right">Bandwidth (mbps)</th>
+							<th align="right">Connection Count</th>
+						</tr>
+					</thead>
+					<tbody id="cache-states"></tbody>
+				</table>
+			</div>
+		</li>
+
+		<li class="tab tab-header" id="deliveryservice-stats-content-tab">
+			<input type="radio" name="tabs" class="tab" id="deliveryservice-stats-input"/>
+			<label for="deliveryservice-stats-input" aria-selected="false">Delivery Service States</label>
+			<div id="deliveryservice-stats-content" class="tabcontent">
+				<table class="tab-grid sortable">
+					<thead>
+						<tr>
+							<th>Delivery Service</th>
+							<th>Status</th>
+							<th align="right">Caches Reporting/Available/Configured</th>
+							<th align="right">Bandwidth (mbps)</th>
+							<th align="right">t/sec</th>
+							<th align="right">2xx/sec</th>
+							<th align="right">3xx/sec</th>
+							<th align="right">4xx/sec</th>
+							<th align="right">5xx/sec</th>
+							<th>Disabled Locations</th>
+						</tr>
+					</thead>
+					<tbody id="deliveryservice-stats"></tbody>
+				</table>
+			</div>
+		</li>
+
+		<li id="event-log-content-tab" class="tab tab-header">
+			<input type="radio" name="tabs" class="tab" id="event-log-input"/>
+			<label for="event-log-input" aria-selected="false">Event Log</label>
+			<div id="event-log-content" class="tabcontent">
+				<table class="tab-grid sortable">
+					<thead>
+						<tr>
+							<th>Name</th>
+							<th>Type</th>
+							<th>Status</th>
+							<th>Description</th>
+							<th align="center" id="event-log-last-header">Event Time</th>
+						</tr>
+					</thead>
+					<tbody id="event-log"></tbody>
+				</table>
+			</div>
+		</li>
+	</ul>
+
+	<div id="update-num-text">Number of updates: <span id="update-num">0</span></div>
+	<div id="last-val-text">Last Val: <span id="last-val">0</span></div>
+</body>
 </html>