wui: improved columns in welcome page server list 129/head
authorLeif Ryge <leif@synthesize.us>
Sun, 23 Nov 2014 09:42:16 +0000 (09:42 +0000)
committerLeif Ryge <leif@synthesize.us>
Sun, 23 Nov 2014 09:54:36 +0000 (09:54 +0000)
As discussed at https://tahoe-lafs.org/trac/tahoe-lafs/ticket/1973

 - replace lengthy timestamps with human-readable deltas (eg 1h2m3s)
 - replace "Since" column with "Status" (addresses usability problem in #1961)
 - replace "announced" column with "Last RX" column
 - remove service column (it always said the same thing, "storage")

This is a squashed commit of 3fe9053134b2429904f673df561e602a50f83c7e
Thanks to an anonymous contributor who wrote some of the tests.

src/allmydata/interfaces.py
src/allmydata/storage_client.py
src/allmydata/test/test_util.py
src/allmydata/test/test_web.py
src/allmydata/util/time_format.py
src/allmydata/web/root.py
src/allmydata/web/static/css/new-tahoe.css
src/allmydata/web/welcome.xhtml
src/allmydata/webish.py

index a8dbdfebeb942f360e0dfe21ee427bf2094aba11..f19415d681dca0f7edf45f91f1308efe72ff02d1 100644 (file)
@@ -417,7 +417,6 @@ class IStorageBroker(Interface):
         public attributes::
 
           service_name: the type of service provided, like 'storage'
-          announcement_time: when we first heard about this service
           last_connect_time: when we last established a connection
           last_loss_time: when we last lost a connection
 
index e532db1a68d8658d60d06cd347c25f90ba6b2fa0..12eaca2c4c2c7f6bf721bc63e4a0fd350d98d3dd 100644 (file)
@@ -164,7 +164,6 @@ class NativeStorageServer:
     the their version information. I remember information about when we were
     last connected too, even if we aren't currently connected.
 
-    @ivar announcement_time: when we first heard about this service
     @ivar last_connect_time: when we last established a connection
     @ivar last_loss_time: when we last lost a connection
 
@@ -212,7 +211,6 @@ class NativeStorageServer:
             self._long_description = tubid_s
             self._short_description = tubid_s[:6]
 
-        self.announcement_time = time.time()
         self.last_connect_time = None
         self.last_loss_time = None
         self.remote_host = None
@@ -263,8 +261,11 @@ class NativeStorageServer:
         return self.last_connect_time
     def get_last_loss_time(self):
         return self.last_loss_time
-    def get_announcement_time(self):
-        return self.announcement_time
+    def get_last_received_data_time(self):
+        if self.rref is None:
+            return None
+        else:
+            return self.rref.getDataLastReceivedAt()
 
     def get_available_space(self):
         version = self.get_version()
index 2a5ba19978b5eb5e5f1f78735dcf29a92a3cf179..46fa16ee58c2465cb662fce92261b3036870e19a 100644 (file)
@@ -955,6 +955,44 @@ class TimeFormat(unittest.TestCase):
     def test_parse_date(self):
         self.failUnlessEqual(time_format.parse_date("2010-02-21"), 1266710400)
 
+    def test_format_delta(self):
+        time_1 = 1389812723
+        time_5s_delta = 1389812728
+        time_28m7s_delta = 1389814410
+        time_1h_delta = 1389816323
+        time_1d21h46m49s_delta = 1389977532
+        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
+        time_1_isostr = time.strftime(TIME_FORMAT, time.localtime(time_1))
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_5s_delta),
+            (time_1_isostr, '5s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_28m7s_delta),
+            (time_1_isostr, '28m7s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_1h_delta),
+            (time_1_isostr, '1h0m0s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_1d21h46m49s_delta),
+            (time_1_isostr, '1d21h46m49s'))
+
+        # time_1 with a decimal fraction will make the delta 1s less
+        time_1decimal = 1389812723.383963
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_5s_delta),
+            (time_1_isostr, '4s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_28m7s_delta),
+            (time_1_isostr, '28m6s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_1h_delta),
+            (time_1_isostr, '59m59s'))
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_1d21h46m49s_delta),
+            (time_1_isostr, '1d21h46m48s'))
+
 class CacheDir(unittest.TestCase):
     def test_basic(self):
         basedir = "test_util/CacheDir/test_basic"
index 8c5ead4ba62ff89b24be7ac24511e73ea0bfbfce..397356d5b68811e8c1bbc7b55adb88a6de8705de 100644 (file)
@@ -172,21 +172,28 @@ class FakeHistory:
         return []
 
 class FakeDisplayableServer(StubServer):
-    def __init__(self, serverid, nickname):
+    def __init__(self, serverid, nickname, connected,
+                            last_connect_time, last_lost_time, last_rx):
         StubServer.__init__(self, serverid)
         self.announcement = {"my-version": "allmydata-tahoe-fake",
                              "service-name": "storage",
                              "nickname": nickname}
+        self.connected = connected
+        self.last_lost_time = last_lost_time
+        self.last_rx = last_rx
+        self.last_connect_time = last_connect_time
     def is_connected(self):
-        return True
+        return self.connected
     def get_permutation_seed(self):
         return ""
     def get_remote_host(self):
         return ""
     def get_last_loss_time(self):
-        return None
-    def get_announcement_time(self):
-        return None
+        return self.last_lost_time
+    def get_last_received_data_time(self):
+        return self.last_rx
+    def get_last_connect_time(self):
+        return self.last_connect_time
     def get_announcement(self):
         return self.announcement
     def get_nickname(self):
@@ -242,7 +249,13 @@ class FakeClient(Client):
         self.storage_broker = StorageFarmBroker(None, permute_peers=True)
         # fake knowledge of another server
         self.storage_broker.test_add_server("other_nodeid",
-                                            FakeDisplayableServer("other_nodeid", u"other_nickname \u263B"))
+            FakeDisplayableServer(
+                serverid="other_nodeid", nickname=u"other_nickname \u263B", connected = True,
+                last_connect_time = 10, last_lost_time = 20, last_rx = 30))
+        self.storage_broker.test_add_server("disconnected_nodeid",
+            FakeDisplayableServer(
+                serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False,
+                last_connect_time = 15, last_lost_time = 25, last_rx = 35))
         self.introducer_client = None
         self.history = FakeHistory()
         self.uploader = FakeUploader()
@@ -274,8 +287,9 @@ class WebMixin(object):
         self.s.startService()
         self.staticdir = self.mktemp()
         self.clock = Clock()
+        self.fakeTime = 86460 # 1d 0h 1m 0s
         self.ws = webish.WebishServer(self.s, "0", staticdir=self.staticdir,
-                                      clock=self.clock)
+                                      clock=self.clock, now=lambda:self.fakeTime)
         self.ws.setServiceParent(self.s)
         self.webish_port = self.ws.getPortnum()
         self.webish_url = self.ws.getURL()
@@ -603,6 +617,7 @@ class WebMixin(object):
 
 
 class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase):
+
     def test_create(self):
         pass
 
@@ -619,6 +634,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
             res_u = res.decode('utf-8')
             self.failUnlessIn(u'<td>fake_nickname \u263A</td>', res_u)
             self.failUnlessIn(u'<div class="nickname">other_nickname \u263B</div>', res_u)
+            self.failUnlessIn('Connected to <span>1</span>\n              of <span>2</span> known storage servers', res_u)
+            self.failUnlessIn('<div class="status-indicator service-Connected"></div>\n<div class="status-description">Connected<br /><a class="timestamp" title="00:00:10 01-Jan-1970">1d0h0m50s</a></div></td>', res_u)
+            self.failUnlessIn('<div class="status-indicator service-Disconnected"></div>\n<div class="status-description">Disconnected<br /><a class="timestamp" title="00:00:25 01-Jan-1970">1d0h0m35s</a></div></td>', res_u)
+            self.failUnlessIn('<td class="service-last-received-data"><a class="timestamp" title="00:00:30 01-Jan-1970">1d0h0m30s</a></td>', res)
+            self.failUnlessIn('<td class="service-last-received-data"><a class="timestamp" title="00:00:35 01-Jan-1970">1d0h0m25s</a></td>', res)
             self.failUnlessIn(u'\u00A9 <a href="https://tahoe-lafs.org/">Tahoe-LAFS Software Foundation', res_u)
             self.failUnlessIn('<td><h3>Available</h3></td>', res)
             self.failUnlessIn('123.5kB', res)
index 0f8f2f387dd89735e25ae208e8e6064851fd6878..0820bd8b01f23895970137ec03aa773240778919 100644 (file)
@@ -68,3 +68,28 @@ def parse_date(s):
     # day
     return int(iso_utc_time_to_seconds(s + "T00:00:00"))
 
+def format_delta(time_1, time_2):
+    TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
+    if time_1 is None:
+        absolute_str, relative_str = "N/A", "N/A"
+    else:
+        delta = int( time_2 - time_1 )
+        seconds = delta % 60
+        delta  -= seconds
+        minutes = (delta / 60) % 60
+        delta  -= minutes * 60
+        hours   = delta / (60*60) % 24
+        delta  -=  hours * 24
+        days    = delta / (24*60*60)
+        if not days:
+            if not hours:
+                if not minutes:
+                    relative_str = "%ss" % (seconds)
+                else:
+                    relative_str = "%sm%ss" % (minutes, seconds)
+            else:
+                relative_str = "%sh%sm%ss" % (hours, minutes, seconds)
+        else:
+            relative_str = "%sd%sh%sm%ss" % (days, hours, minutes, seconds)
+        absolute_str = time.strftime(TIME_FORMAT, time.localtime(time_1))
+    return absolute_str, relative_str
index 5f1d236719c473ec17192c85c5b8af236322cb38..7e666b7627a406b11a55a804834fa31ecdd9dd53 100644 (file)
@@ -15,6 +15,7 @@ from allmydata.web import filenode, directory, unlinked, status, operations
 from allmydata.web import storage
 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
      get_arg, RenderMixin, get_format, get_mutable_type, TIME_FORMAT
+from allmydata.util.time_format import format_delta
 
 
 class URIHandler(RenderMixin, rend.Page):
@@ -132,12 +133,15 @@ class Root(rend.Page):
     addSlash = True
     docFactory = getxmlfile("welcome.xhtml")
 
-    def __init__(self, client, clock=None):
+    def __init__(self, client, clock=None, now=None):
         rend.Page.__init__(self, client)
         self.client = client
         # If set, clock is a twisted.internet.task.Clock that the tests
         # use to test ophandle expiration.
         self.child_operations = operations.OphandleTable(clock)
+        self.now = now
+        if self.now is None:
+            self.now = time.time
         try:
             s = client.getServiceNamed("storage")
         except KeyError:
@@ -270,7 +274,7 @@ class Root(rend.Page):
         ctx.fillSlots("peerid", server.get_longname())
         ctx.fillSlots("nickname", server.get_nickname())
         rhost = server.get_remote_host()
-        if rhost:
+        if server.is_connected():
             if nodeid == self.client.nodeid:
                 rhost_s = "(loopback)"
             elif isinstance(rhost, address.IPv4Address):
@@ -278,30 +282,29 @@ class Root(rend.Page):
             else:
                 rhost_s = str(rhost)
             addr = rhost_s
-            connected = "yes"
-            since = server.get_last_connect_time()
+            service_connection_status = "Connected"
+            service_connection_status_abs_time, service_connection_status_rel_time = format_delta(server.get_last_connect_time(), self.now())
         else:
             addr = "N/A"
-            connected = "no"
-            since = server.get_last_loss_time()
-        announced = server.get_announcement_time()
+            service_connection_status = "Disconnected"
+            service_connection_status_abs_time, service_connection_status_rel_time = format_delta(server.get_last_loss_time(), self.now())
+
+        last_received_data_abs_time, last_received_data_rel_time = format_delta(server.get_last_received_data_time(), self.now())
+
         announcement = server.get_announcement()
         version = announcement["my-version"]
-        service_name = announcement["service-name"]
         available_space = server.get_available_space()
         if available_space is None:
             available_space = "N/A"
         else:
             available_space = abbreviate_size(available_space)
         ctx.fillSlots("address", addr)
-        ctx.fillSlots("connected", connected)
-        ctx.fillSlots("connected-bool", bool(rhost))
-        ctx.fillSlots("since", time.strftime(TIME_FORMAT,
-                                             time.localtime(since)))
-        ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
-                                                 time.localtime(announced)))
+        ctx.fillSlots("service_connection_status", service_connection_status)
+        ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
+        ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
+        ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
+        ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
         ctx.fillSlots("version", version)
-        ctx.fillSlots("service_name", service_name)
         ctx.fillSlots("available_space", available_space)
 
         return ctx.tag
index fa1269a10796902374e5e1efc246e5afb18188de..580008278bb90a85f60b20bc798eabf240a61722 100644 (file)
@@ -48,6 +48,7 @@ body {
 
 .grid-status .status-indicator {
     margin-top: 5px;
+    float: left;
 }
 
 .furl {
@@ -73,22 +74,42 @@ body {
     width: 50%;
 }
 
+.service-connection-status {
+    white-space: nowrap;
+}
+
+.service-connection-string {
+    display: inline-block;
+}
+
 .status-indicator {
-    float: left;
+    display: inline-block;
     width: 16px;
     height: 16px;
     border-radius: 50%;
     display: inline-block;
-    vertical-align: middle;
+    vertical-align: top;
     margin-right: 4px;
     margin-top: 2px;
 }
+.status-description {
+    display:inline-block;
+}
+
+.status-description .timestamp {
+    font-size: smaller;
+}
+
+a.timestamp {
+    color: inherit;
+    text-decoration:none;
+}
 
-.connected-yes {
+.connected-yes, .service-Connected {
     background: #468847;
 }
 
-.connected-no {
+.connected-no, .service-Disconnected {
     background: #B94A48;
 }
 
index 72a2be169e3b81023011b16a9c771eb0d51d95f5..de2bceb14fc0d84c30a2a486402ff8b0242c8241 100644 (file)
@@ -1,4 +1,5 @@
-<!DOCTYPE html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
+      Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html lang="en" xmlns:n="http://nevow.com/ns/nevow/0.1">
   <head>
     <meta charset="utf-8"/>
           <table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="services">
             <thead>
               <tr n:pattern="header">
+                <td><h3>Status</h3></td>
                 <td><h3>Nickname</h3></td>
                 <td><h3>Address</h3></td>
-                <td><h3>Service</h3></td>
-                <td><h3>Since</h3></td>
-                <td><h3>Announced</h3></td>
+                <td><h3>Last RX</h3></td>
                 <td><h3>Version</h3></td>
                 <td><h3>Available</h3></td>
               </tr>
             </thead>
             <tr n:pattern="item" n:render="service_row">
+              <td class="service-connection-status">
+              <div><n:attr name="class">status-indicator service-<n:slot name="service_connection_status"/></n:attr></div>
+<div class="status-description"><n:slot name="service_connection_status"/><br/><a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a></div></td>
               <td class="nickname-and-peerid">
-              <div><n:attr name="class">status-indicator connected-<n:slot name="connected"/></n:attr></div>
                 <div class="nickname"><n:slot name="nickname"/></div>
                 <div class="nodeid"><n:slot name="peerid"/></div>
               </td>
               <td class="address"><n:slot name="address"/></td>
-            <td class="service-service-name"><n:slot name="service_name"/></td>
-              <td class="service-since timestamp"><n:slot name="since"/></td>
-              <td class="service-announced timestamp"><n:slot name="announced"/></td>
+              <td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
               <td class="service-version"><n:slot name="version"/></td>
               <td class="service-available-space"><n:slot name="available_space"/></td>
             </tr>
-            <tr n:pattern="empty"><td>You are not presently connected to any peers</td></tr>
+            <tr n:pattern="empty"><td colspan="6">You are not presently connected to any peers</td></tr>
           </table>
         </div><!--/span-->
       </div><!--/row-->
index 813856cb02c53bdb81e06d3953281f20d803aae9..51286bdeedfb48615635d8fa64ac2d7f2049a6fa 100644 (file)
@@ -129,14 +129,14 @@ class WebishServer(service.MultiService):
     name = "webish"
 
     def __init__(self, client, webport, nodeurl_path=None, staticdir=None,
-                 clock=None):
+                 clock=None, now=None):
         service.MultiService.__init__(self)
         # the 'data' argument to all render() methods default to the Client
         # the 'clock' argument to root.Root is, if set, a
         # twisted.internet.task.Clock that is provided by the unit tests
         # so that they can test features that involve the passage of
         # time in a deterministic manner.
-        self.root = root.Root(client, clock)
+        self.root = root.Root(client, clock, now)
         self.buildServer(webport, nodeurl_path, staticdir)
         if self.root.child_operations:
             self.site.remember(self.root.child_operations, IOpHandleTable)