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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4c1e6ea  Tm ui improvements (#4403)
4c1e6ea is described below

commit 4c1e6eaaaf20f8f2e2413780530bf6fd298a04df
Author: ocket8888 <[email protected]>
AuthorDate: Thu Apr 2 10:15:35 2020 -0600

    Tm ui improvements (#4403)
    
    * Fixed unavailable cache servers not being highlighted in red
    
    * Reverted unrelated change
    
    * Removed unused library
    
    * Split styling out into a separate file
    
    Also improved the 'getEvents' function and removed a bunch of unused ids 
and classes
    
    * Removed a bunch of unused ids and classes from the cache-states table
    
    * Moved TM UI script into its own file
    
    * Added some JSDoc comments, switched 'var' to 'const' on some constants
    
    * Greatly simplified 'getDSStats'
    
    * Added JSDoc comment to 'ajax'
    
    * Fixed missing license header
    
    * Rearranged things so that nothing is used before it is defined
    
    * Stop modifying text nodes with '.innerHTML'
    
    * Formatted some long lines
    
    * Remove obsolete attributes on TH elements
    
    * removed some unused IDs
    
    * removed element names from rules that don't need them, and one unused rule
    
    * removed disallowed aria roles
    
    * removed unused classes
    
    * Use addEventListener on Window instead of onload on body
    
    * Sectioned off the styling
    
    * Fixed reference error and syntax error in script.js
    
    * Added strict mode declaration
    
    * Removed licensing for removed file
    
    * fixed specfile
    
    * Fixed broken references in srvhttp.go, added new ones
    
    * Added TM binary to gitignore
    
    * Added MIME Types for CSS and JS data
    
    * Properly setting MIME Type for static files
    
    * Fixed typo
    
    * Fixed DSStats insertion
---
 LICENSE                                    |   4 -
 lib/go-rfc/mimetype.go                     |  12 +
 licenses/MIT-sorttable                     |  30 --
 traffic_monitor/.gitignore                 |   2 +
 traffic_monitor/build/traffic_monitor.spec |   5 +-
 traffic_monitor/srvhttp/srvhttp.go         |  36 +-
 traffic_monitor/static/index.html          | 519 ++---------------------------
 traffic_monitor/static/script.js           | 302 +++++++++++++++++
 traffic_monitor/static/sorttable.js        | 495 ---------------------------
 traffic_monitor/static/style.css           | 149 +++++++++
 10 files changed, 532 insertions(+), 1022 deletions(-)

diff --git a/LICENSE b/LICENSE
index dce4fac..544a666 100644
--- a/LICENSE
+++ b/LICENSE
@@ -229,10 +229,6 @@ For the bootstrap component:
 /*! normalize.css v3.0.1 | MIT License | git.io/normalize */
 ./licenses/MIT-normalize
 
-For the sorttable component:
-@traffic_monitor/static/sorttable.js
-./licenses/MIT-sorttable
-
 For the jQuery component:
 ./licenses/MIT-jquery
 
diff --git a/lib/go-rfc/mimetype.go b/lib/go-rfc/mimetype.go
index aa769a5..ff7eeb0 100644
--- a/lib/go-rfc/mimetype.go
+++ b/lib/go-rfc/mimetype.go
@@ -257,3 +257,15 @@ var MIME_HTML = MimeType{
        Name:       "text/html",
        Parameters: map[string]string{"charset": "utf-8"},
 }
+
+// MIME_CSS is a pre-defined MimeType for CSS data.
+var MIME_CSS = MimeType{
+       Name: "text/css",
+       Parameters: map[string]string{"charset": "utf-8"},
+}
+
+// MIME_JS is a pre-defined MimeType for JavaScript data.
+var MIME_JS = MimeType{
+       Name: "text/javascript",
+       Parameters: map[string]string{"charset": "utf-8"},
+}
diff --git a/licenses/MIT-sorttable b/licenses/MIT-sorttable
deleted file mode 100644
index 36dcf0d..0000000
--- a/licenses/MIT-sorttable
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
-  SortTable
-  version 2
-  7th April 2007
-  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
-
-  Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
-  This basically means: do what you want with it.
-*/
-
-The MIT Licence, for code from kryogenix.org
-
-Code downloaded from the Browser Experiments section of kryogenix.org is
-licenced under the so-called MIT licence. The licence is below.
-
-Copyright (c) 1997-date Stuart Langridge
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/traffic_monitor/.gitignore b/traffic_monitor/.gitignore
new file mode 100644
index 0000000..5c82061
--- /dev/null
+++ b/traffic_monitor/.gitignore
@@ -0,0 +1,2 @@
+#TM binary
+traffic_monitor
diff --git a/traffic_monitor/build/traffic_monitor.spec 
b/traffic_monitor/build/traffic_monitor.spec
index c848401..abf8cfe 100644
--- a/traffic_monitor/build/traffic_monitor.spec
+++ b/traffic_monitor/build/traffic_monitor.spec
@@ -55,8 +55,9 @@ mkdir -p "${RPM_BUILD_ROOT}"/etc/logrotate.d
 
 src=src/github.com/apache/trafficcontrol/traffic_monitor
 cp -p "$src"/traffic_monitor               
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/bin/traffic_monitor
-cp  "$src"/static/index.html               
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/static/index.html
-cp  "$src"/static/sorttable.js             
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/static/sorttable.js
+cp "$src"/static/index.html                
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/static/index.html
+cp "$src"/static/script.js                 
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/static/script.js
+cp "$src"/static/style.css                 
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/static/style.css
 cp "$src"/conf/traffic_ops.cfg             
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/conf/traffic_ops.cfg
 cp "$src"/conf/traffic_monitor.cfg         
"${RPM_BUILD_ROOT}"/opt/traffic_monitor/conf/traffic_monitor.cfg
 cp "$src"/build/traffic_monitor.init       
"${RPM_BUILD_ROOT}"/etc/init.d/traffic_monitor
diff --git a/traffic_monitor/srvhttp/srvhttp.go 
b/traffic_monitor/srvhttp/srvhttp.go
index f6992f1..c1fdff1 100644
--- a/traffic_monitor/srvhttp/srvhttp.go
+++ b/traffic_monitor/srvhttp/srvhttp.go
@@ -30,6 +30,7 @@ import (
        "time"
 
        "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/lib/go-rfc"
        "github.com/hydrogen18/stoppableListener"
 )
 
@@ -61,9 +62,13 @@ func (s *Server) registerEndpoints(sm *http.ServeMux, 
endpoints map[string]http.
        if err != nil {
                return fmt.Errorf("Error getting root endpoint: %v", err)
        }
-       handleSortableJs, err := s.handleSortableFunc(staticFileDir)
+       handleScript, err := s.handleScriptFunc(staticFileDir)
        if err != nil {
-               return fmt.Errorf("Error getting sortable endpoint: %v", err)
+               return fmt.Errorf("Error getting script endpoint: %v", err)
+       }
+       handleStyle, err := s.handleStyleFunc(staticFileDir)
+       if err != nil {
+               return fmt.Errorf("Error getting style endpoint: %v", err)
        }
 
        for path, f := range endpoints {
@@ -71,7 +76,8 @@ func (s *Server) registerEndpoints(sm *http.ServeMux, 
endpoints map[string]http.
        }
 
        sm.HandleFunc("/", handleRoot)
-       sm.HandleFunc("/sorttable.js", handleSortableJs)
+       sm.HandleFunc("/script.js", handleScript)
+       sm.HandleFunc("/style.css", handleStyle)
 
        return nil
 }
@@ -223,11 +229,29 @@ func DateStr(t time.Time) string {
 }
 
 func (s *Server) handleRootFunc(staticFileDir string) (http.HandlerFunc, 
error) {
-       return s.handleFile(staticFileDir + "/index.html")
+       return s.handleFile(staticFileDir + "index.html")
+}
+
+func (s *Server) handleScriptFunc(staticFileDir string) (http.HandlerFunc, 
error) {
+       bytes, err := ioutil.ReadFile(staticFileDir + "script.js")
+       if err != nil {
+               return nil, err
+       }
+       return func(w http.ResponseWriter, req *http.Request) {
+               w.Header().Set(rfc.ContentType, rfc.MIME_JS.String())
+               w.Write(bytes)
+       }, nil
 }
 
-func (s *Server) handleSortableFunc(staticFileDir string) (http.HandlerFunc, 
error) {
-       return s.handleFile(staticFileDir + "/sorttable.js")
+func (s *Server) handleStyleFunc(staticFileDir string) (http.HandlerFunc, 
error) {
+       bytes, err := ioutil.ReadFile(staticFileDir + "style.css")
+       if err != nil {
+               return nil, err
+       }
+       return func(w http.ResponseWriter, req *http.Request) {
+               w.Header().Set(rfc.ContentType, rfc.MIME_CSS.String())
+               w.Write(bytes)
+       }, nil
 }
 
 func (s *Server) handleFile(name string) (http.HandlerFunc, error) {
diff --git a/traffic_monitor/static/index.html 
b/traffic_monitor/static/index.html
index 809c993..ef1b851 100644
--- a/traffic_monitor/static/index.html
+++ b/traffic_monitor/static/index.html
@@ -19,467 +19,14 @@ specific language governing permissions and limitations
 under the License.
 -->
 
-
-<html>
+<html lang="en">
 <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: #ced;
-               }
-               tbody tr:nth-child(odd) {
-                       background: #fff;
-               }
-               #cache-states td:nth-child(n+4) {
-                       text-align: right;
-               }
-               #cache-states td:first-child {
-                       white-space: nowrap;
-               }
-
-
-               li.endpoint {
-                       margin: 4px 0;
-               }
-
-               div#top-bar {
-                       display: inline-flex;
-                       justify-content: space-around;
-                       align-items: center;
-                       width: 100%;
-                       margin: 15px 0;
-               }
-
-               div#links {
-                       display: grid;
-                       grid-template-columns: 1fr 1fr;
-                       max-width: 100ch;
-               }
-               div#links div {
-                       margin-left: 4px;
-               }
-               div#links a {
-                       display: block;
-               }
-
-               tbody tr.error {
-                       background-color: #f00;
-               }
-               tbody tr.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: background-color ease-in-out 0.3s;
-                       text-align: center;
-                       border: 1px solid green;
-               }
-               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;
-                       border-bottom-width: 0;
-               }
-               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) {
-                               const servers = new 
Map(Object.entries(JSON.parse(r)));
-                               const table = document.createElement('TBODY');
-                               table.id = "cache-states"
-
-                               // TODO: I'm not sure all these IDs are 
actually necessary anymore
-                               for (const [serverName, server] of servers) {
-                                       const row = table.insertRow(0);
-                                       row.classList.add("stripes");
-                                       row.id = "cache-states-" + serverName;
-
-                                       let cell = row.insertCell(0);
-                                       cell.id = row.id + "-server";
-                                       cell.textContent = serverName;
-
-                                       cell = row.insertCell(1);
-                                       cell.id = row.id + "-type";
-                                       cell.textContent = server.type || 
"UNKNOWN";
-
-                    cell = row.insertCell(2);
-                    cell.id = row.id + "-ipv4";
-                    if (server.status.indexOf("ONLINE") !== 0) {
-                        cell.textContent = server.ipv4_available;
-                    } else {
-                        cell.textContent = "N/A";
-                    }
-
-                    cell = row.insertCell(3);
-                    cell.id = row.id + "-ipv6";
-                    if (server.status.indexOf("ONLINE") !== 0) {
-                        cell.textContent = server.ipv6_available;
-                    } else {
-                        cell.textContent = "N/A";
-                    }
-                                       cell = row.insertCell(4);
-                                       cell.id = row.id + "-status";
-                                       if 
(Object.prototype.hasOwnProperty.call(server, "status")) {
-                                               cell.textContent = 
server.status;
-                                               if 
(server.status.indexOf("ADMIN_DOWN") !== -1 || server.status.indexOf("OFFLINE") 
!== -1) {
-                                                       
row.classList.add("warning");
-                                                       
row.classList.remove("error");
-                                                       
row.classList.remove("stripes");
-                                               } else if 
(!server.combined_available && server.status.indexOf("ONLINE") !== 0) {
-                                                       
row.classList.add("error");
-                                                       
row.classList.remove("warning");
-                                                       
row.classList.remove("stripes");
-                                               } else if 
(server.status.indexOf(" availableBandwidth") !== -1) {
-                                                       
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");
-                                               }
-                                       }
-
-                                       cell = row.insertCell(5);
-                                       cell.id = row.id + "-load-average";
-                                       cell.textContent = server.load_average 
|| "";
-
-                                       cell = row.insertCell(6);
-                                       cell.id = row.id + "-query-time";
-                                       cell.textContent = server.query_time_ms 
|| "";
-
-                                       cell = row.insertCell(7);
-                                       cell.id = row.id + "-health-time";
-                                       cell.textContent = 
server.health_time_ms || "";
-
-                                       cell = row.insertCell(8);
-                                       cell.id = row.id + "-stat-time";
-                                       cell.textContent = server.stat_time_ms 
|| "";
-
-                                       cell = row.insertCell(9);
-                                       cell.id = row.id + "-health-span";
-                                       cell.textContent = 
server.health_span_ms || "";
-
-                                       cell = row.insertCell(10);
-                                       cell.id = row.id + "-stat-span";
-                                       cell.textContent = server.stat_span_ms 
|| "";
-
-                                       cell = row.insertCell(11);
-                                       cell.id = row.id + "-bandwidth";
-                                       if 
(Object.prototype.hasOwnProperty.call(server, "bandwidth_kbps")) {
-                                               const kbps = 
(server.bandwidth_kbps / kilobitsInMegabit).toFixed(2);
-                                               const max = 
numberStrWithCommas((server.bandwidth_capacity_kbps / 
kilobitsInMegabit).toFixed(0));
-                                               cell.textContent = `${kbps} / 
${max}`;
-                                       } else {
-                                               cell.textContent = "N/A";
-                                       }
-
-                                       cell = row.insertCell(12);
-                                       cell.id = row.id + "-connection-count";
-                                       cell.textContent = 
server.connection_count || "N/A";
-                               }
-
-
-                               const oldtable = 
document.getElementById("cache-states");
-                               oldtable.parentNode.replaceChild(table, 
oldtable);
-
-                       })
-               }
-
-               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(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>
+       <link rel="stylesheet" type="text/css" href="./style.css"/>
+       <script src="./script.js"></script>
 </head>
-<body onload="init()">
+<body>
        <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>
@@ -515,14 +62,16 @@ under the License.
                </div>
        </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>
+       <!-- TODO: There's no reference to these in the script nor styling; 
safe to remove? -->
+       <div>Number of updates: <span>0</span></div>
+       <div>Last Val: <span>0</span></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>
+               <li class="tab" id="cache-states-content-tab">
+                       <input type="radio" name="tabs" class="tab" 
id="cache-states-input" checked/>
+                       <label for="cache-states-input">Cache States</label>
                        <div id="cache-states-content" class="tabcontent">
-                               <table class="tab-grid sortable">
+                               <table>
                                        <thead>
                                                <tr>
                                                        <th>Server</th>
@@ -530,14 +79,14 @@ under the License.
                                                        <th>IPv4</th>
                                                        <th>IPv6</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>
+                                                       <th>Load Average</th>
+                                                       <th>Query Time (ms)</th>
+                                                       <th>Health Time 
(ms)</th>
+                                                       <th>Stat Time (ms)</th>
+                                                       <th>Health Span 
(ms)</th>
+                                                       <th>Stat Span (ms)</th>
+                                                       <th>Bandwidth 
(mbps)</th>
+                                                       <th>Connection 
Count</th>
                                                </tr>
                                        </thead>
                                        <tbody id="cache-states"></tbody>
@@ -545,22 +94,22 @@ under the License.
                        </div>
                </li>
 
-               <li class="tab tab-header" 
id="deliveryservice-stats-content-tab">
+               <li class="tab" 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>
+                       <label for="deliveryservice-stats-input">Delivery 
Service States</label>
                        <div id="deliveryservice-stats-content" 
class="tabcontent">
-                               <table class="tab-grid sortable">
+                               <table>
                                        <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>Caches 
Reporting/Available/Configured</th>
+                                                       <th>Bandwidth 
(mbps)</th>
+                                                       <th>t/sec</th>
+                                                       <th>2xx/sec</th>
+                                                       <th>3xx/sec</th>
+                                                       <th>4xx/sec</th>
+                                                       <th>5xx/sec</th>
                                                        <th>Disabled 
Locations</th>
                                                </tr>
                                        </thead>
@@ -569,18 +118,18 @@ under the License.
                        </div>
                </li>
 
-               <li id="event-log-content-tab" class="tab tab-header">
+               <li id="event-log-content-tab" class="tab">
                        <input type="radio" name="tabs" class="tab" 
id="event-log-input"/>
-                       <label for="event-log-input" 
aria-selected="false">Event Log</label>
+                       <label for="event-log-input">Event Log</label>
                        <div id="event-log-content" class="tabcontent">
-                               <table class="tab-grid sortable">
+                               <table>
                                        <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>
+                                                       <th>Event Time</th>
                                                </tr>
                                        </thead>
                                        <tbody id="event-log"></tbody>
diff --git a/traffic_monitor/static/script.js b/traffic_monitor/static/script.js
new file mode 100644
index 0000000..8a3698a
--- /dev/null
+++ b/traffic_monitor/static/script.js
@@ -0,0 +1,302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+"use strict";
+
+const millisecondsInSecond = 1000;
+const kilobitsInGigabit = 1000000;
+const kilobitsInMegabit = 1000;
+
+/**
+ * This is the index of the latest event already catalogued by the UI. TM 
doesn't
+ * provide a way to fetch event logs "older than x" etc., so this is how we 
keep
+ * track of what we've seen.
+*/
+var lastEvent = 0;
+
+/**
+ * Adds a comma for every 3rd power of ten in a number represented as a string.
+ * e.g. numberStrWithCommas("100000") outputs "100,000".
+ * (source: http://stackoverflow.com/a/2901298/292623)
+ * @param x A string containing a number which will be formatted.
+*/
+function numberStrWithCommas(x) {
+       return x.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+}
+
+/**
+ * ajax performs an XMLHttpRequest and passes the result to a function - *only 
if the response code is EXACTLY 200*.
+ * @param endpoint An API endpoint relative to the root of the TM webserver 
e.g. /publish/DsStats
+ * @param f A function that takes a single argument which will be the entire 
response body as a string.
+ */
+function ajax(endpoint, f) {
+       const xhttp = new XMLHttpRequest();
+       xhttp.onreadystatechange = () => {
+               if (xhttp.readyState === 4 && xhttp.status === 200) {
+                       f(xhttp.responseText);
+               }
+       };
+       xhttp.open("GET", endpoint, true);
+       xhttp.send();
+}
+
+function getCacheCount() {
+       ajax("/api/cache-count", function(r) {
+               document.getElementById("cache-count").textContent = r;
+       });
+}
+
+function getCacheAvailableCount() {
+       ajax("/api/cache-available-count", function(r) {
+               document.getElementById("cache-available").textContent = r;
+       });
+}
+
+function getBandwidth() {
+       ajax("/api/bandwidth-kbps", function(r) {
+               document.getElementById("bandwidth").textContent = 
numberStrWithCommas((parseFloat(r) / kilobitsInGigabit).toFixed(2));
+       });
+}
+
+function getBandwidthCapacity() {
+       ajax("/api/bandwidth-capacity-kbps", function(r) {
+               document.getElementById("bandwidth-capacity").textContent = 
numberStrWithCommas((r / kilobitsInGigabit).toString());
+       });
+}
+
+function getCacheDownCount() {
+       ajax("/api/cache-down-count", function(r) {
+               document.getElementById("cache-down").textContent = r;
+       });
+}
+
+function getVersion() {
+       ajax("/api/version", function(r) {
+               document.getElementById("version").textContent = r;
+       });
+}
+
+function getTrafficOpsUri() {
+       ajax("/api/traffic-ops-uri", function(r) {
+               // This used to be done by setting the element's `innerHTML`, 
but that doesn't remove
+               // the child nodes. They're orphaned, but continue to take up 
memory.
+               const link = document.createElement('A');
+               link.href = r;
+               link.textContent = r;
+               const sourceURISpan = document.getElementById('source-uri');
+               while (sourceURISpan.lastChild) {
+                       sourceURISpan.removeChild(sourceURISpan.lastChild);
+               }
+               sourceURISpan.appendChild(link);
+       });
+}
+
+function getTrafficOpsCdn() {
+       ajax("/publish/ConfigDoc", function(r) {
+               document.getElementById("cdn-name").textContent = 
JSON.parse(r).cdnName || "unknown";
+       });
+}
+
+/**
+ * Fetches the event log from TM and updates the "Event Log" table with the new
+ * results.
+*/
+function getEvents() {
+       /// \todo add /api/events-since/{index} (and change Traffic Monitor to 
keep latest
+       ajax("/publish/EventLog", function(r) {
+               const events = JSON.parse(r).events || [];
+               for (const event of events.slice(lastEvent+1)) {
+                       lastEvent = event.index
+                       const row = 
document.getElementById("event-log").insertRow(0);
+
+                       row.insertCell(0).textContent = event.name;
+                       row.insertCell(1).textContent = event.type;
+
+                       const cell = row.insertCell(2);
+                       if(event.isAvailable) {
+                               cell.textContent = "available";
+                       } else {
+                               cell.textContent = "offline";
+                               row.classList.add("error");
+                       }
+
+                       row.insertCell(3).textContent = event.description;
+                       row.insertCell(4).textContent = new Date(event.time * 
1000).toISOString();
+               }
+       });
+}
+
+/**
+ * Fetches the current cache server states and statistics from TM and updates
+ * the "Cache States" table with the results - replacing the current content.
+*/
+function getCacheStates() {
+       ajax("/api/cache-statuses", function(r) {
+               const servers = new Map(Object.entries(JSON.parse(r)));
+               const table = document.createElement('TBODY');
+               table.id = "cache-states"
+
+               for (const [serverName, server] of servers) {
+                       const row = table.insertRow(0);
+
+                       row.insertCell(0).textContent = serverName;
+                       row.insertCell(1).textContent = server.type || 
"UNKNOWN";
+                       row.insertCell(2).textContent = 
server.status.indexOf("ONLINE") !== 0 ? server.ipv4_available : "N/A";
+                       row.insertCell(3).textContent = 
server.status.indexOf("ONLINE") !== 0 ?  server.ipv6_available : "N/A";
+                       row.insertCell(4).textContent = server.status || "";
+                       if (Object.prototype.hasOwnProperty.call(server, 
"status")) {
+                               if (server.status.indexOf("ADMIN_DOWN") !== -1 
|| server.status.indexOf("OFFLINE") !== -1) {
+                                       row.classList.add("warning");
+                               } else if (!server.combined_available && 
server.status.indexOf("ONLINE") !== 0) {
+                                       row.classList.add("error");
+                               } else if (server.status.indexOf(" 
availableBandwidth") !== -1) {
+                                       row.classList.add("error");
+                               }
+                       }
+
+                       row.insertCell(5).textContent = server.load_average || 
"";
+                       row.insertCell(6).textContent = server.query_time_ms || 
"";
+                       row.insertCell(7).textContent = server.health_time_ms 
|| "";
+                       row.insertCell(8).textContent = server.stat_time_ms || 
"";
+                       row.insertCell(9).textContent = server.health_span_ms 
|| "";
+                       row.insertCell(10).textContent = server.stat_span_ms || 
"";
+
+                       if (Object.prototype.hasOwnProperty.call(server, 
"bandwidth_kbps")) {
+                               const kbps = (server.bandwidth_kbps / 
kilobitsInMegabit).toFixed(2);
+                               const max = 
numberStrWithCommas((server.bandwidth_capacity_kbps / 
kilobitsInMegabit).toFixed(0));
+                               row.insertCell(11).textContent = `${kbps} / 
${max}`;
+                       } else {
+                               row.insertCell(11).textContent = "N/A";
+                       }
+
+                       row.insertCell(12).textContent = 
server.connection_count || "N/A";
+               }
+
+
+               const oldtable = document.getElementById("cache-states");
+               oldtable.parentNode.replaceChild(table, oldtable);
+
+       });
+}
+
+/**
+ * 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.
+ * @param f The floating point number to format
+*/
+const dsDisplayFloat = (f) => { return f === 0 ? "" : f.toFixed(2); }
+
+/**
+ * Attempts to extract data from a deliveryService object, but falls back on 
"N/A" if it doesn't have that
+ * property.
+ * @param ds The Delivery Service from which to extract data
+ * @param prop The property being extracted. Technically, the extracted 
property is dsDisplayFloat(parseFloat(ds[prop][0].value)).
+*/
+function getDSProperty(ds, prop) {
+       try {
+               return dsDisplayFloat(parseFloat(ds[prop][0].value));
+       } catch (e) {
+               console.error(e);
+       }
+       return "N/A";
+}
+
+/**
+ * Fetches the current Delivery Service stats from TM and updates the 
"Delivery Service States"
+ * table with the results - replacing the current content.
+*/
+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) {
+               const deliveryServices = new 
Map(Object.entries(JSON.parse(r).deliveryService));
+               const table = document.createElement('TBODY');
+               table.id = "deliveryservice-stats";
+
+               for (const [dsName, deliveryService] of deliveryServices) {
+                       const row = table.insertRow(0);
+                       const available = !deliveryService.isAvailable || 
!deliveryService.isAvailable[0] || !deliveryService.isAvailable[0].value === 
"true";
+                       if (available) {
+                               row.classList.add("error");
+                       }
+
+                       row.insertCell(0).textContent = dsName;
+                       row.insertCell(1).textContent = available ? "available" 
: `unavailable - ${deliveryService["error-string"][0].value}`;
+                       row.insertCell(2).textContent = 
(Object.prototype.hasOwnProperty.call(deliveryService, "caches-reporting") &&
+                                                        
Object.prototype.hasOwnProperty.call(deliveryService, "caches-available") &&
+                                                        
Object.prototype.hasOwnProperty.call(deliveryService, "caches-configured")) ?
+                                                        
`${deliveryService['caches-reporting'][0].value} / 
${deliveryService['caches-available'][0].value} / 
${deliveryService['caches-configured'][0].value}` : "N/A";
+
+                       row.insertCell(3).textContent = 
Object.prototype.hasOwnProperty.call(deliveryService, "total.kbps") ? 
(deliveryService['total.kbps'][0].value / kilobitsInMegabit).toFixed(2) : "N/A";
+                       row.insertCell(4).textContent = 
getDSProperty(deliveryService, "total.tps_total");
+                       row.insertCell(5).textContent = 
getDSProperty(deliveryService, "total.tps_2xx");
+                       row.insertCell(6).textContent = 
getDSProperty(deliveryService, "total.tps_3xx");
+                       row.insertCell(7).textContent = 
getDSProperty(deliveryService, "total.tps_4xx");
+                       row.insertCell(8).textContent = 
getDSProperty(deliveryService, "total.tps_5xx");
+                       row.insertCell(9);
+                       // \todo implement disabled locations
+               }
+
+               const oldtable = 
document.getElementById("deliveryservice-stats");
+               oldtable.parentNode.replaceChild(table, oldtable);
+       });
+}
+
+/**
+ * Fetches not only the "Cache States" but also the aggregate cache server 
statistics used in the
+ * informational section at the top of the page.
+ */
+function getCacheStatuses() {
+       getCacheCount();
+       getCacheAvailableCount();
+       getCacheDownCount();
+       getCacheStates();
+}
+
+/**
+ * Fetches the metadata information used at the very top of the page.
+ */
+function getTopBar() {
+       getVersion();
+       getTrafficOpsUri();
+       getTrafficOpsCdn();
+       getCacheStatuses();
+}
+
+/**
+ * Runs immediately after content is loaded, fetching initial information and 
setting intervals for
+ * for gathering other data.
+ */
+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);
+}
+
+window.addEventListener('load', init);
diff --git a/traffic_monitor/static/sorttable.js 
b/traffic_monitor/static/sorttable.js
deleted file mode 100644
index 38b0fc6..0000000
--- a/traffic_monitor/static/sorttable.js
+++ /dev/null
@@ -1,495 +0,0 @@
-/*
-  SortTable
-  version 2
-  7th April 2007
-  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
-
-  Instructions:
-  Download this file
-  Add <script src="sorttable.js"></script> to your HTML
-  Add class="sortable" to any table you'd like to make sortable
-  Click on the headers to sort
-
-  Thanks to many, many people for contributions and suggestions.
-  Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
-  This basically means: do what you want with it.
-*/
-
-
-var stIsIE = /*@cc_on!@*/false;
-
-sorttable = {
-  init: function() {
-    // quit if this function has already been called
-    if (arguments.callee.done) return;
-    // flag this function so we don't do the same thing twice
-    arguments.callee.done = true;
-    // kill the timer
-    if (_timer) clearInterval(_timer);
-
-    if (!document.createElement || !document.getElementsByTagName) return;
-
-    sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
-
-    forEach(document.getElementsByTagName('table'), function(table) {
-      if (table.className.search(/\bsortable\b/) != -1) {
-        sorttable.makeSortable(table);
-      }
-    });
-
-  },
-
-  makeSortable: function(table) {
-    if (table.getElementsByTagName('thead').length == 0) {
-      // table doesn't have a tHead. Since it should have, create one and
-      // put the first table row in it.
-      the = document.createElement('thead');
-      the.appendChild(table.rows[0]);
-      table.insertBefore(the,table.firstChild);
-    }
-    // Safari doesn't support table.tHead, sigh
-    if (table.tHead == null) table.tHead = 
table.getElementsByTagName('thead')[0];
-
-    if (table.tHead.rows.length != 1) return; // can't cope with two header 
rows
-
-    // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
-    // "total" rows, for example). This is B&R, since what you're supposed
-    // to do is put them in a tfoot. So, if there are sortbottom rows,
-    // for backwards compatibility, move them to tfoot (creating it if needed).
-    sortbottomrows = [];
-    for (var i=0; i<table.rows.length; i++) {
-      if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
-        sortbottomrows[sortbottomrows.length] = table.rows[i];
-      }
-    }
-    if (sortbottomrows) {
-      if (table.tFoot == null) {
-        // table doesn't have a tfoot. Create one.
-        tfo = document.createElement('tfoot');
-        table.appendChild(tfo);
-      }
-      for (var i=0; i<sortbottomrows.length; i++) {
-        tfo.appendChild(sortbottomrows[i]);
-      }
-      delete sortbottomrows;
-    }
-
-    // work through each column and calculate its type
-    headrow = table.tHead.rows[0].cells;
-    for (var i=0; i<headrow.length; i++) {
-      // manually override the type with a sorttable_type attribute
-      if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this 
col
-        mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
-        if (mtch) { override = mtch[1]; }
-             if (mtch && typeof sorttable["sort_"+override] == 'function') {
-               headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
-             } else {
-               headrow[i].sorttable_sortfunction = 
sorttable.guessType(table,i);
-             }
-             // make it clickable to sort
-             headrow[i].sorttable_columnindex = i;
-             headrow[i].sorttable_tbody = table.tBodies[0];
-             dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = 
function(e) {
-
-          if (this.className.search(/\bsorttable_sorted\b/) != -1) {
-            // if we're already sorted by this column, just
-            // reverse the table, which is quicker
-            sorttable.reverse(this.sorttable_tbody);
-            this.className = this.className.replace('sorttable_sorted',
-                                                    
'sorttable_sorted_reverse');
-            this.removeChild(document.getElementById('sorttable_sortfwdind'));
-            sortrevind = document.createElement('span');
-            sortrevind.id = "sorttable_sortrevind";
-            sortrevind.innerHTML = stIsIE ? '&nbsp<font 
face="webdings">5</font>' : '&nbsp;&#x25B4;';
-            this.appendChild(sortrevind);
-            return;
-          }
-          if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
-            // if we're already sorted by this column in reverse, just
-            // re-reverse the table, which is quicker
-            sorttable.reverse(this.sorttable_tbody);
-            this.className = this.className.replace('sorttable_sorted_reverse',
-                                                    'sorttable_sorted');
-            this.removeChild(document.getElementById('sorttable_sortrevind'));
-            sortfwdind = document.createElement('span');
-            sortfwdind.id = "sorttable_sortfwdind";
-            sortfwdind.innerHTML = stIsIE ? '&nbsp<font 
face="webdings">6</font>' : '&nbsp;&#x25BE;';
-            this.appendChild(sortfwdind);
-            return;
-          }
-
-          // remove sorttable_sorted classes
-          theadrow = this.parentNode;
-          forEach(theadrow.childNodes, function(cell) {
-            if (cell.nodeType == 1) { // an element
-              cell.className = 
cell.className.replace('sorttable_sorted_reverse','');
-              cell.className = cell.className.replace('sorttable_sorted','');
-            }
-          });
-          sortfwdind = document.getElementById('sorttable_sortfwdind');
-          if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
-          sortrevind = document.getElementById('sorttable_sortrevind');
-          if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
-
-          this.className += ' sorttable_sorted';
-          sortfwdind = document.createElement('span');
-          sortfwdind.id = "sorttable_sortfwdind";
-          sortfwdind.innerHTML = stIsIE ? '&nbsp<font 
face="webdings">6</font>' : '&nbsp;&#x25BE;';
-          this.appendChild(sortfwdind);
-
-               // build an array to sort. This is a Schwartzian transform 
thing,
-               // i.e., we "decorate" each row with the actual sort key,
-               // sort based on the sort keys, and then put the rows back in 
order
-               // which is a lot faster because you only do getInnerText once 
per row
-               row_array = [];
-               col = this.sorttable_columnindex;
-               rows = this.sorttable_tbody.rows;
-               for (var j=0; j<rows.length; j++) {
-                 row_array[row_array.length] = 
[sorttable.getInnerText(rows[j].cells[col]), rows[j]];
-               }
-               /* If you want a stable sort, uncomment the following line */
-               //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
-               /* and comment out this one */
-               row_array.sort(this.sorttable_sortfunction);
-
-               tb = this.sorttable_tbody;
-               for (var j=0; j<row_array.length; j++) {
-                 tb.appendChild(row_array[j][1]);
-               }
-
-               delete row_array;
-             });
-           }
-    }
-  },
-
-  guessType: function(table, column) {
-    // guess the type of a column based on its first non-blank row
-    sortfn = sorttable.sort_alpha;
-    for (var i=0; i<table.tBodies[0].rows.length; i++) {
-      text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
-      if (text != '') {
-        if (text.match(/^-?[�$�]?[\d,.]+%?$/)) {
-          return sorttable.sort_numeric;
-        }
-        // check for a date: dd/mm/yyyy or dd/mm/yy
-        // can have / or . or - as separator
-        // can be mm/dd as well
-        possdate = text.match(sorttable.DATE_RE)
-        if (possdate) {
-          // looks like a date
-          first = parseInt(possdate[1]);
-          second = parseInt(possdate[2]);
-          if (first > 12) {
-            // definitely dd/mm
-            return sorttable.sort_ddmm;
-          } else if (second > 12) {
-            return sorttable.sort_mmdd;
-          } else {
-            // looks like a date, but we can't tell which, so assume
-            // that it's dd/mm (English imperialism!) and keep looking
-            sortfn = sorttable.sort_ddmm;
-          }
-        }
-      }
-    }
-    return sortfn;
-  },
-
-  getInnerText: function(node) {
-    // gets the text we want to use for sorting for a cell.
-    // strips leading and trailing whitespace.
-    // this is *not* a generic getInnerText function; it's special to 
sorttable.
-    // for example, you can override the cell text with a customkey attribute.
-    // it also gets .value for <input> fields.
-
-    if (!node) return "";
-
-    hasInputs = (typeof node.getElementsByTagName == 'function') &&
-                 node.getElementsByTagName('input').length;
-
-    if (node.getAttribute("sorttable_customkey") != null) {
-      return node.getAttribute("sorttable_customkey");
-    }
-    else if (typeof node.textContent != 'undefined' && !hasInputs) {
-      return node.textContent.replace(/^\s+|\s+$/g, '');
-    }
-    else if (typeof node.innerText != 'undefined' && !hasInputs) {
-      return node.innerText.replace(/^\s+|\s+$/g, '');
-    }
-    else if (typeof node.text != 'undefined' && !hasInputs) {
-      return node.text.replace(/^\s+|\s+$/g, '');
-    }
-    else {
-      switch (node.nodeType) {
-        case 3:
-          if (node.nodeName.toLowerCase() == 'input') {
-            return node.value.replace(/^\s+|\s+$/g, '');
-          }
-        case 4:
-          return node.nodeValue.replace(/^\s+|\s+$/g, '');
-          break;
-        case 1:
-        case 11:
-          var innerText = '';
-          for (var i = 0; i < node.childNodes.length; i++) {
-            innerText += sorttable.getInnerText(node.childNodes[i]);
-          }
-          return innerText.replace(/^\s+|\s+$/g, '');
-          break;
-        default:
-          return '';
-      }
-    }
-  },
-
-  reverse: function(tbody) {
-    // reverse the rows in a tbody
-    newrows = [];
-    for (var i=0; i<tbody.rows.length; i++) {
-      newrows[newrows.length] = tbody.rows[i];
-    }
-    for (var i=newrows.length-1; i>=0; i--) {
-       tbody.appendChild(newrows[i]);
-    }
-    delete newrows;
-  },
-
-  /* sort functions
-     each sort function takes two parameters, a and b
-     you are comparing a[0] and b[0] */
-  sort_numeric: function(a,b) {
-    aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
-    if (isNaN(aa)) aa = 0;
-    bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
-    if (isNaN(bb)) bb = 0;
-    return aa-bb;
-  },
-  sort_alpha: function(a,b) {
-    if (a[0]==b[0]) return 0;
-    if (a[0]<b[0]) return -1;
-    return 1;
-  },
-  sort_ddmm: function(a,b) {
-    mtch = a[0].match(sorttable.DATE_RE);
-    y = mtch[3]; m = mtch[2]; d = mtch[1];
-    if (m.length == 1) m = '0'+m;
-    if (d.length == 1) d = '0'+d;
-    dt1 = y+m+d;
-    mtch = b[0].match(sorttable.DATE_RE);
-    y = mtch[3]; m = mtch[2]; d = mtch[1];
-    if (m.length == 1) m = '0'+m;
-    if (d.length == 1) d = '0'+d;
-    dt2 = y+m+d;
-    if (dt1==dt2) return 0;
-    if (dt1<dt2) return -1;
-    return 1;
-  },
-  sort_mmdd: function(a,b) {
-    mtch = a[0].match(sorttable.DATE_RE);
-    y = mtch[3]; d = mtch[2]; m = mtch[1];
-    if (m.length == 1) m = '0'+m;
-    if (d.length == 1) d = '0'+d;
-    dt1 = y+m+d;
-    mtch = b[0].match(sorttable.DATE_RE);
-    y = mtch[3]; d = mtch[2]; m = mtch[1];
-    if (m.length == 1) m = '0'+m;
-    if (d.length == 1) d = '0'+d;
-    dt2 = y+m+d;
-    if (dt1==dt2) return 0;
-    if (dt1<dt2) return -1;
-    return 1;
-  },
-
-  shaker_sort: function(list, comp_func) {
-    // A stable sort function to allow multi-level sorting of data
-    // see: http://en.wikipedia.org/wiki/Cocktail_sort
-    // thanks to Joseph Nahmias
-    var b = 0;
-    var t = list.length - 1;
-    var swap = true;
-
-    while(swap) {
-        swap = false;
-        for(var i = b; i < t; ++i) {
-            if ( comp_func(list[i], list[i+1]) > 0 ) {
-                var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
-                swap = true;
-            }
-        } // for
-        t--;
-
-        if (!swap) break;
-
-        for(var i = t; i > b; --i) {
-            if ( comp_func(list[i], list[i-1]) < 0 ) {
-                var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
-                swap = true;
-            }
-        } // for
-        b++;
-
-    } // while(swap)
-  }
-}
-
-/* ******************************************************************
-   Supporting functions: bundled here to avoid depending on a library
-   ****************************************************************** */
-
-// Dean Edwards/Matthias Miller/John Resig
-
-/* for Mozilla/Opera9 */
-if (document.addEventListener) {
-    document.addEventListener("DOMContentLoaded", sorttable.init, false);
-}
-
-/* for Internet Explorer */
-/*@cc_on @*/
-/*@if (@_win32)
-    document.write("<script id=__ie_onload defer 
src=javascript:void(0)><\/script>");
-    var script = document.getElementById("__ie_onload");
-    script.onreadystatechange = function() {
-        if (this.readyState == "complete") {
-            sorttable.init(); // call the onload handler
-        }
-    };
-/*@end @*/
-
-/* for Safari */
-if (/WebKit/i.test(navigator.userAgent)) { // sniff
-    var _timer = setInterval(function() {
-        if (/loaded|complete/.test(document.readyState)) {
-            sorttable.init(); // call the onload handler
-        }
-    }, 10);
-}
-
-/* for other browsers */
-window.onload = sorttable.init;
-
-// written by Dean Edwards, 2005
-// with input from Tino Zijdel, Matthias Miller, Diego Perini
-
-// http://dean.edwards.name/weblog/2005/10/add-event/
-
-function dean_addEvent(element, type, handler) {
-       if (element.addEventListener) {
-               element.addEventListener(type, handler, false);
-       } else {
-               // assign each event handler a unique ID
-               if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
-               // create a hash table of event types for the element
-               if (!element.events) element.events = {};
-               // create a hash table of event handlers for each element/event 
pair
-               var handlers = element.events[type];
-               if (!handlers) {
-                       handlers = element.events[type] = {};
-                       // store the existing event handler (if there is one)
-                       if (element["on" + type]) {
-                               handlers[0] = element["on" + type];
-                       }
-               }
-               // store the event handler in the hash table
-               handlers[handler.$$guid] = handler;
-               // assign a global event handler to do all the work
-               element["on" + type] = handleEvent;
-       }
-};
-// a counter used to create unique IDs
-dean_addEvent.guid = 1;
-
-function removeEvent(element, type, handler) {
-       if (element.removeEventListener) {
-               element.removeEventListener(type, handler, false);
-       } else {
-               // delete the event handler from the hash table
-               if (element.events && element.events[type]) {
-                       delete element.events[type][handler.$$guid];
-               }
-       }
-};
-
-function handleEvent(event) {
-       var returnValue = true;
-       // grab the event object (IE uses a global event object)
-       event = event || fixEvent(((this.ownerDocument || this.document || 
this).parentWindow || window).event);
-       // get a reference to the hash table of event handlers
-       var handlers = this.events[event.type];
-       // execute each event handler
-       for (var i in handlers) {
-               this.$$handleEvent = handlers[i];
-               if (this.$$handleEvent(event) === false) {
-                       returnValue = false;
-               }
-       }
-       return returnValue;
-};
-
-function fixEvent(event) {
-       // add W3C standard event methods
-       event.preventDefault = fixEvent.preventDefault;
-       event.stopPropagation = fixEvent.stopPropagation;
-       return event;
-};
-fixEvent.preventDefault = function() {
-       this.returnValue = false;
-};
-fixEvent.stopPropagation = function() {
-  this.cancelBubble = true;
-}
-
-// Dean's forEach: http://dean.edwards.name/base/forEach.js
-/*
-       forEach, version 1.0
-       Copyright 2006, Dean Edwards
-       License: http://www.opensource.org/licenses/mit-license.php
-*/
-
-// array-like enumeration
-if (!Array.forEach) { // mozilla already supports this
-       Array.forEach = function(array, block, context) {
-               for (var i = 0; i < array.length; i++) {
-                       block.call(context, array[i], i, array);
-               }
-       };
-}
-
-// generic enumeration
-Function.prototype.forEach = function(object, block, context) {
-       for (var key in object) {
-               if (typeof this.prototype[key] == "undefined") {
-                       block.call(context, object[key], key, object);
-               }
-       }
-};
-
-// character enumeration
-String.forEach = function(string, block, context) {
-       Array.forEach(string.split(""), function(chr, index) {
-               block.call(context, chr, index, string);
-       });
-};
-
-// globally resolve forEach enumeration
-var forEach = function(object, block, context) {
-       if (object) {
-               var resolve = Object; // default
-               if (object instanceof Function) {
-                       // functions have a "length" property
-                       resolve = Function;
-               } else if (object.forEach instanceof Function) {
-                       // the object implements a custom forEach method so use 
that
-                       object.forEach(block, context);
-                       return;
-               } else if (typeof object == "string") {
-                       // the object is a string
-                       resolve = String;
-               } else if (typeof object.length == "number") {
-                       // the object is array-like
-                       resolve = Array;
-               }
-               resolve.forEach(object, block, context);
-       }
-};
-
diff --git a/traffic_monitor/static/style.css b/traffic_monitor/static/style.css
new file mode 100644
index 0000000..87de588
--- /dev/null
+++ b/traffic_monitor/static/style.css
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+       font-family: "Lato", sans-serif;
+       font-size: 14px;
+       margin: 0;
+       max-width: 100vw;
+}
+
+/*****************/
+/* Table Styling */
+/*****************/
+table {
+       border-collapse: separate;
+       border-spacing: 0px 0;
+       width: 100%;
+}
+
+th, td {
+       padding:5px 20px 5px 5px;
+}
+
+tbody tr:nth-child(even) {
+       background: #ced;
+}
+tbody tr:nth-child(odd) {
+       background: #fff;
+}
+.error {
+       background-color: #f00;
+}
+
+.warning {
+       background-color: #f80;
+}
+
+#cache-states td:nth-child(n+4),
+#cache-states-content td:nth-child(n+4),
+#event-log td:last-child,
+#deliveryservice-stats td:nth-child(n+3),
+#deliveryservice-stats-content th:nth-child(n+3) {
+       text-align: right;
+}
+#cache-states td:first-child,
+#event-log td:first-child,
+#event-log td:last-child,
+#deliveryservice-stats td:first-child,
+th {
+       white-space: nowrap;
+}
+#event-log-content th:last-child {
+       text-align: center;
+}
+
+/*****************/
+/*   Top Bar     */
+/*****************/
+#top-bar {
+       display: inline-flex;
+       justify-content: space-around;
+       align-items: center;
+       width: 100%;
+       margin: 15px 0;
+}
+
+/*****************/
+/*     Links     */
+/*****************/
+#links {
+       display: grid;
+       grid-template-columns: 1fr 1fr;
+       max-width: 100ch;
+}
+#links div {
+       margin-left: 4px;
+}
+#links a {
+       display: block;
+}
+
+/*****************/
+/*     Tabs      */
+/*****************/
+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: background-color ease-in-out 0.3s;
+       text-align: center;
+       border: 1px solid green;
+}
+label:hover {
+       background-color: #cfd;
+}
+.tabs {
+       list-style: none;
+       max-width: 100%;
+       border: 1px solid #ccc;
+       background-color: #f1f1f1;
+       position: relative;
+}
+.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;
+       border-bottom-width: 0;
+}
+.tabs li {
+       float: left;
+       display: block;
+}

Reply via email to