From 486dbfc7bd3c0bbba42a6df8e4564601120aec0e Mon Sep 17 00:00:00 2001 From: Leif Ryge Date: Sun, 23 Nov 2014 09:42:16 +0000 Subject: [PATCH] wui: improved columns in welcome page server list 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 | 1 - src/allmydata/storage_client.py | 9 ++--- src/allmydata/test/test_util.py | 38 ++++++++++++++++++++++ src/allmydata/test/test_web.py | 34 +++++++++++++++---- src/allmydata/util/time_format.py | 25 ++++++++++++++ src/allmydata/web/root.py | 33 ++++++++++--------- src/allmydata/web/static/css/new-tahoe.css | 29 ++++++++++++++--- src/allmydata/web/welcome.xhtml | 18 +++++----- src/allmydata/webish.py | 4 +-- 9 files changed, 149 insertions(+), 42 deletions(-) diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py index a8dbdfeb..f19415d6 100644 --- a/src/allmydata/interfaces.py +++ b/src/allmydata/interfaces.py @@ -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 diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index e532db1a..12eaca2c 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -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() diff --git a/src/allmydata/test/test_util.py b/src/allmydata/test/test_util.py index 2a5ba199..46fa16ee 100644 --- a/src/allmydata/test/test_util.py +++ b/src/allmydata/test/test_util.py @@ -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" diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 8c5ead4b..397356d5 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -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'fake_nickname \u263A', res_u) self.failUnlessIn(u'
other_nickname \u263B
', res_u) + self.failUnlessIn('Connected to 1\n of 2 known storage servers', res_u) + self.failUnlessIn('
\n
Connected
1d0h0m50s
', res_u) + self.failUnlessIn('
\n
Disconnected
1d0h0m35s
', res_u) + self.failUnlessIn('1d0h0m30s', res) + self.failUnlessIn('1d0h0m25s', res) self.failUnlessIn(u'\u00A9 Tahoe-LAFS Software Foundation', res_u) self.failUnlessIn('

Available

', res) self.failUnlessIn('123.5kB', res) diff --git a/src/allmydata/util/time_format.py b/src/allmydata/util/time_format.py index 0f8f2f38..0820bd8b 100644 --- a/src/allmydata/util/time_format.py +++ b/src/allmydata/util/time_format.py @@ -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 diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 5f1d2367..7e666b76 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -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 diff --git a/src/allmydata/web/static/css/new-tahoe.css b/src/allmydata/web/static/css/new-tahoe.css index fa1269a1..58000827 100644 --- a/src/allmydata/web/static/css/new-tahoe.css +++ b/src/allmydata/web/static/css/new-tahoe.css @@ -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; } diff --git a/src/allmydata/web/welcome.xhtml b/src/allmydata/web/welcome.xhtml index 72a2be16..de2bceb1 100644 --- a/src/allmydata/web/welcome.xhtml +++ b/src/allmydata/web/welcome.xhtml @@ -1,4 +1,5 @@ - + @@ -167,29 +168,28 @@ + - - - + + - - - + - +

Status

Nickname

Address

Service

Since

Announced

Last RX

Version

Available

+
status-indicator service-
+

-
status-indicator connected-
You are not presently connected to any peers
You are not presently connected to any peers
diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index 813856cb..51286bde 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -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) -- 2.45.2